From 4107d5fedb0fcea044c5c50de248e36afda9e955 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 27 Jun 2022 14:40:40 -0500 Subject: [PATCH 001/519] Fix weird layout on refreshing create page --- web/pages/create.tsx | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index e4cba4e0..ebbb6f65 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -26,6 +26,7 @@ import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' import { CATEGORIES } from 'common/categories' +import { User } from 'common/user' export default function Create() { const [question, setQuestion] = useState('') @@ -33,7 +34,13 @@ export default function Create() { const router = useRouter() const { groupId } = router.query as { groupId: string } useTracking('view create page') - if (!router.isReady) return
+ const creator = useUser() + + useEffect(() => { + if (creator === null) router.push('/') + }, [creator, router]) + + if (!router.isReady || !creator) return
return ( @@ -58,7 +65,11 @@ export default function Create() {
- +
@@ -66,14 +77,12 @@ export default function Create() { } // Allow user to create a new contract -export function NewContract(props: { question: string; groupId?: string }) { - const { question, groupId } = props - const creator = useUser() - - useEffect(() => { - if (creator === null) router.push('/') - }, [creator]) - +export function NewContract(props: { + creator: User + question: string + groupId?: string +}) { + const { creator, question, groupId } = props const [outcomeType, setOutcomeType] = useState('BINARY') const [initialProb] = useState(50) const [minString, setMinString] = useState('') From 0b585d1c9897db273acfa19c019dc700a4c4f9f8 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Mon, 27 Jun 2022 13:32:24 -0700 Subject: [PATCH 002/519] Typescript project references take 2 (#586) * More liberal .gitignores on TS output directories * Use project references for Typescript functions project * Use /dist dir for Cloud Functions deployment payload * Fix Github actions functions tsc job --- .github/workflows/check.yml | 2 +- common/.gitignore | 5 ++--- common/tsconfig.json | 2 ++ firebase.json | 4 ++-- functions/.gitignore | 6 ++++-- functions/package.json | 5 +++-- functions/tsconfig.json | 8 +++++++- web/tsconfig.json | 1 - 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index dcf81c44..e441edcf 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -52,4 +52,4 @@ jobs: - name: Run Typescript checker on cloud functions if: ${{ success() || failure() }} working-directory: functions - run: tsc --pretty --project tsconfig.json --noEmit + run: tsc -b -v --pretty diff --git a/common/.gitignore b/common/.gitignore index e0ba0181..11320851 100644 --- a/common/.gitignore +++ b/common/.gitignore @@ -1,6 +1,5 @@ # Compiled JavaScript files -lib/**/*.js -lib/**/*.js.map +lib/ # TypeScript v1 declaration files typings/ @@ -10,4 +9,4 @@ node_modules/ package-lock.json ui-debug.log -firebase-debug.log \ No newline at end of file +firebase-debug.log diff --git a/common/tsconfig.json b/common/tsconfig.json index 158a5218..62a5c745 100644 --- a/common/tsconfig.json +++ b/common/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { "baseUrl": "../", + "composite": true, + "module": "commonjs", "moduleResolution": "node", "noImplicitReturns": true, "outDir": "lib", diff --git a/firebase.json b/firebase.json index de1e19b7..25f9b61f 100644 --- a/firebase.json +++ b/firebase.json @@ -1,8 +1,8 @@ { "functions": { - "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", + "predeploy": "cd functions && yarn build", "runtime": "nodejs16", - "source": "functions" + "source": "functions/dist" }, "firestore": { "rules": "firestore.rules", diff --git a/functions/.gitignore b/functions/.gitignore index 7aeaedd4..2aeae30c 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -2,9 +2,11 @@ .env* .runtimeconfig.json +# GCP deployment artifact +dist/ + # Compiled JavaScript files -lib/**/*.js -lib/**/*.js.map +lib/ # TypeScript v1 declaration files typings/ diff --git a/functions/package.json b/functions/package.json index 7b5c30b0..45ddcac2 100644 --- a/functions/package.json +++ b/functions/package.json @@ -5,7 +5,8 @@ "firestore": "dev-mantic-markets.appspot.com" }, "scripts": { - "build": "tsc", + "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist", + "compile": "tsc -b", "watch": "tsc -w", "shell": "yarn build && firebase functions:shell", "start": "yarn shell", @@ -18,7 +19,7 @@ "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "verify": "(cd .. && yarn verify)" }, - "main": "lib/functions/src/index.js", + "main": "functions/src/index.js", "dependencies": { "@amplitude/node": "1.10.0", "fetch": "1.1.0", diff --git a/functions/tsconfig.json b/functions/tsconfig.json index e183bb44..9496b9cb 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "baseUrl": "../", + "composite": true, "module": "commonjs", "noImplicitReturns": true, "outDir": "lib", @@ -8,6 +9,11 @@ "strict": true, "target": "es2017" }, + "references": [ + { + "path": "../common" + } + ], "compileOnSave": true, - "include": ["src", "../common/**/*.ts"] + "include": ["src"] } diff --git a/web/tsconfig.json b/web/tsconfig.json index 96cf1311..2f31aa8c 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -16,7 +16,6 @@ "jsx": "preserve", "incremental": true }, - "watchOptions": { "excludeDirectories": [".next"] }, From 2f434c849d53bf4c75cf0f525477b259c3e93c66 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 28 Jun 2022 11:03:14 -0500 Subject: [PATCH 003/519] Remove portfolio link; user icon links to portfolio --- web/components/nav/profile-menu.tsx | 2 +- web/components/nav/sidebar.tsx | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/web/components/nav/profile-menu.tsx b/web/components/nav/profile-menu.tsx index 397f6e4e..9e869c40 100644 --- a/web/components/nav/profile-menu.tsx +++ b/web/components/nav/profile-menu.tsx @@ -8,7 +8,7 @@ import { trackCallback } from 'web/lib/service/analytics' export function ProfileSummary(props: { user: User }) { const { user } = props return ( - + Date: Tue, 28 Jun 2022 11:18:55 -0500 Subject: [PATCH 004/519] Tweak nav items around --- web/components/nav/sidebar.tsx | 40 ++++++++++++---------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 6c3d4fc4..e5f3cd5c 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -57,12 +57,10 @@ function getMoreNavigation(user?: User | null) { } return [ + { name: 'Send M$', href: '/links' }, { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Charity', href: '/charity' }, - { name: 'Blog', href: 'https://news.manifold.markets' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, - { name: 'Statistics', href: '/stats' }, { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, { name: 'Sign out', @@ -100,6 +98,18 @@ const signedInMobileNavigation = [ ...signedOutMobileNavigation, ] +function getMoreMobileNav() { + return [ + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'Statistics', href: '/stats' }, + { + name: 'Sign out', + href: '#', + onClick: withTracking(firebaseLogout, 'sign out'), + }, + ] +} + export type Item = { name: string href: string @@ -211,29 +221,7 @@ export default function Sidebar(props: { className?: string }) { {user && ( } /> )} From 7f9b0557c4db38ca7a01e3e1bd6ff48d3a81c087 Mon Sep 17 00:00:00 2001 From: Forrest Wolf Date: Tue, 28 Jun 2022 14:46:25 -0500 Subject: [PATCH 005/519] Reorganize verify scripts (#589) * Update verify to match check for functions * Give each subdirectory a verify:dir script --- common/package.json | 3 ++- functions/package.json | 3 ++- package.json | 2 +- web/package.json | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/common/package.json b/common/package.json index 1bd67851..c8115d84 100644 --- a/common/package.json +++ b/common/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "private": true, "scripts": { - "verify": "(cd .. && yarn verify)" + "verify": "(cd .. && yarn verify)", + "verify:dir": "npx eslint . --max-warnings 0" }, "sideEffects": false, "dependencies": { diff --git a/functions/package.json b/functions/package.json index 45ddcac2..eb6c7151 100644 --- a/functions/package.json +++ b/functions/package.json @@ -17,7 +17,8 @@ "db:backup-local": "firebase emulators:export --force ./firestore_export", "db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", - "verify": "(cd .. && yarn verify)" + "verify": "(cd .. && yarn verify)", + "verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty" }, "main": "functions/src/index.js", "dependencies": { diff --git a/package.json b/package.json index a5c1e29e..e4aee3fd 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "web" ], "scripts": { - "verify": "(cd web && npx prettier --check .; yarn lint --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit); (cd common && npx eslint . --max-warnings 0); (cd functions && npx eslint . --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit)" + "verify": "(cd web && yarn verify:dir); (cd functions && yarn verify:dir)" }, "dependencies": {}, "devDependencies": { diff --git a/web/package.json b/web/package.json index cde76121..454db57c 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,8 @@ "lint": "next lint", "format": "npx prettier --write .", "postbuild": "next-sitemap", - "verify": "(cd .. && yarn verify)" + "verify": "(cd .. && yarn verify)", + "verify:dir": "npx prettier --check .; yarn lint --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit" }, "dependencies": { "@amplitude/analytics-browser": "0.4.1", From 63528aa0f3d92e78aa42e91d9582287d2be4dc2e Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:19:58 -0500 Subject: [PATCH 006/519] Add CES charity (#591) Added CES charity to the charity page. --- common/charity.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/common/charity.ts b/common/charity.ts index 249bcc51..0d8a0aa6 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -516,6 +516,22 @@ The American Civil Liberties Union is our nation's guardian of liberty, working The U.S. Constitution and the Bill of Rights trumpet our aspirations for the kind of society that we want to be. But for much of our history, our nation failed to fulfill the promise of liberty for whole groups of people.`, }, + { + name: 'The Center for Election Science', + website: 'https://electionscience.org/', + photo: 'https://i.imgur.com/WvdHHZa.png', + preview: + 'The Center for Election Science is a nonpartisan nonprofit dedicated to empowering voters with voting methods that strengthen democracy. We believe you deserve a vote that empowers you to impact the world you live in.', + description: `Founded in 2011, The Center for Election Science is a national, nonpartisan nonprofit focused on voting reform. + +Our Mission — To empower people with voting methods that strengthen democracy. + +Our Vision — A world where democracies thrive because voters’ voices are heard. + +With an emphasis on approval voting, we bring better elections to people across the country through both advocacy and research. + +The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that don’t represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`, + }, ].map((charity) => { const slug = charity.name.toLowerCase().replace(/\s/g, '-') return { From 8c3c30c70743cfb89281eab332268493dc4b882f Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Wed, 29 Jun 2022 11:00:43 -0500 Subject: [PATCH 007/519] Show groups on user page, allow to join/leave (#594) * Show groups on user page, allow to join/leave * Link to groups * Unused var --- web/components/groups/group-selector.tsx | 2 +- web/components/groups/groups-button.tsx | 144 +++++++++++++++++++++++ web/components/nav/sidebar.tsx | 2 +- web/components/user-page.tsx | 2 + web/hooks/use-group.ts | 6 +- web/lib/firebase/groups.ts | 21 ++++ web/pages/groups.tsx | 15 +++ 7 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 web/components/groups/groups-button.tsx diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index 6bc943dc..ea1597f2 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -22,7 +22,7 @@ export function GroupSelector(props: { const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const [query, setQuery] = useState('') - const memberGroups = useMemberGroups(creator) + const memberGroups = useMemberGroups(creator?.id) const filteredGroups = memberGroups ? query === '' ? memberGroups diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx new file mode 100644 index 00000000..e6ee217d --- /dev/null +++ b/web/components/groups/groups-button.tsx @@ -0,0 +1,144 @@ +import clsx from 'clsx' +import { User } from 'common/user' +import { useState } from 'react' +import { useUser } from 'web/hooks/use-user' +import { withTracking } from 'web/lib/service/analytics' +import { Row } from 'web/components/layout/row' +import { useMemberGroups } from 'web/hooks/use-group' +import { TextButton } from 'web/components/text-button' +import { Group } from 'common/group' +import { Modal } from 'web/components/layout/modal' +import { Col } from 'web/components/layout/col' +import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' +import { firebaseLogin } from 'web/lib/firebase/users' +import { GroupLink } from 'web/pages/groups' + +export function GroupsButton(props: { user: User }) { + const { user } = props + const [isOpen, setIsOpen] = useState(false) + const groups = useMemberGroups(user.id) + + return ( + <> + setIsOpen(true)}> + {groups?.length ?? ''} Groups + + + + + ) +} + +function GroupsDialog(props: { + user: User + groups: Group[] + isOpen: boolean + setIsOpen: (isOpen: boolean) => void +}) { + const { user, groups, isOpen, setIsOpen } = props + + return ( + + +
{user.name}
+
@{user.username}
+ + +
+ ) +} + +function GroupsList(props: { groups: Group[] }) { + const { groups } = props + return ( + + {groups.length === 0 && ( +
No groups yet...
+ )} + {groups + .sort((group1, group2) => group2.createdTime - group1.createdTime) + .map((group) => ( + + ))} + + ) +} + +function GroupItem(props: { group: Group; className?: string }) { + const { group, className } = props + return ( + + + + + + + ) +} + +export function JoinOrLeaveGroupButton(props: { + group: Group + small?: boolean + className?: string +}) { + const { group, small, className } = props + const currentUser = useUser() + const isFollowing = currentUser + ? group.memberIds.includes(currentUser.id) + : false + const onJoinGroup = () => { + if (!currentUser) return + joinGroup(group, currentUser.id) + } + const onLeaveGroup = () => { + if (!currentUser) return + leaveGroup(group, currentUser.id) + } + + const smallStyle = + 'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500' + + if (!currentUser || isFollowing === undefined) { + if (!group.anyoneCanJoin) + return
Closed
+ return ( + + ) + } + + if (isFollowing) { + return ( + + ) + } + + if (!group.anyoneCanJoin) + return
Closed
+ return ( + + ) +} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index e5f3cd5c..0b3d9393 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -185,7 +185,7 @@ export default function Sidebar(props: { className?: string }) { const mobileNavigationOptions = !user ? signedOutMobileNavigation : signedInMobileNavigation - const memberItems = (useMemberGroups(user) ?? []).map((group: Group) => ({ + const memberItems = (useMemberGroups(user?.id) ?? []).map((group: Group) => ({ name: group.name, href: groupPath(group.slug), })) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 2019a9de..246ed2aa 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -36,6 +36,7 @@ import { FollowersButton, FollowingButton } from './following-button' import { useFollows } from 'web/hooks/use-follows' import { FollowButton } from './follow-button' import { PortfolioMetrics } from 'common/user' +import { GroupsButton } from 'web/components/groups/groups-button' export function UserLink(props: { name: string @@ -197,6 +198,7 @@ export function UserPage(props: { + {user.website && ( diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index f73fd04e..41f84707 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -29,11 +29,11 @@ export const useGroups = () => { return groups } -export const useMemberGroups = (user: User | null | undefined) => { +export const useMemberGroups = (userId: string | null | undefined) => { const [memberGroups, setMemberGroups] = useState() useEffect(() => { - if (user) return listenForMemberGroups(user.id, setMemberGroups) - }, [user]) + if (userId) return listenForMemberGroups(userId, setMemberGroups) + }, [userId]) return memberGroups } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 1438dd4c..d7244f98 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -94,3 +94,24 @@ export async function getGroupsWithContractId( const groups = await getValues(q) setGroups(groups) } + +export async function joinGroup(group: Group, userId: string): Promise { + const { memberIds } = group + if (memberIds.includes(userId)) { + return group + } + const newMemberIds = [...memberIds, userId] + const newGroup = { ...group, memberIds: newMemberIds } + await updateGroup(newGroup, { memberIds: newMemberIds }) + return newGroup +} +export async function leaveGroup(group: Group, userId: string): Promise { + const { memberIds } = group + if (!memberIds.includes(userId)) { + return group + } + const newMemberIds = memberIds.filter((id) => id !== userId) + const newGroup = { ...group, memberIds: newMemberIds } + await updateGroup(newGroup, { memberIds: newMemberIds }) + return newGroup +} diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index c8f08b25..a8f99b23 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -15,6 +15,8 @@ import { getUser, User } from 'web/lib/firebase/users' import { Tabs } from 'web/components/layout/tabs' import { GroupMembersList } from 'web/pages/group/[...slugs]' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' +import { SiteLink } from 'web/components/site-link' +import clsx from 'clsx' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -202,3 +204,16 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) { ) } + +export function GroupLink(props: { group: Group; className?: string }) { + const { group, className } = props + + return ( + + {group.name} + + ) +} From 2d79d7f8dbf98eeee28d99cbad4418cb13d8f63f Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 29 Jun 2022 12:33:20 -0500 Subject: [PATCH 008/519] Rework nav to show list of groups (#596) * Rework nav to show list of groups * Fix lint * Replace Portfolio with Profile link * Lint: remove unused vars --- web/components/nav/nav-bar.tsx | 55 +++++++------- web/components/nav/sidebar.tsx | 133 ++++++++++++++++----------------- 2 files changed, 90 insertions(+), 98 deletions(-) diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 23c9ab38..5a997b46 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -3,7 +3,6 @@ import Link from 'next/link' import { HomeIcon, MenuAlt3Icon, - PresentationChartLineIcon, SearchIcon, XIcon, } from '@heroicons/react/outline' @@ -19,14 +18,9 @@ import NotificationsIcon from 'web/components/notifications-icon' import { useIsIframe } from 'web/hooks/use-is-iframe' import { trackCallback } from 'web/lib/service/analytics' -function getNavigation(username: string) { +function getNavigation() { return [ { name: 'Home', href: '/home', icon: HomeIcon }, - { - name: 'Portfolio', - href: `/${username}?tab=bets`, - icon: PresentationChartLineIcon, - }, { name: 'Notifications', href: `/notifications`, @@ -55,38 +49,39 @@ export function BottomNavBar() { } const navigationOptions = - user === null - ? signedOutNavigation - : getNavigation(user?.username || 'error') + user === null ? signedOutNavigation : getNavigation() return (
From 3b4666ba3e239d4c0545aea64d00cf795688f5b9 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 29 Jun 2022 12:21:40 -0700 Subject: [PATCH 010/519] Add Firebase schema collection helpers (kind of an RFC) (#583) * Add Firebase schema collection helpers * Decentralize definitions from schema file (James feedback) * Add lint comment --- web/hooks/use-contract.ts | 6 +-- web/hooks/use-user.ts | 6 +-- web/lib/firebase/contracts.ts | 83 +++++++++++++++-------------------- web/lib/firebase/groups.ts | 42 +++++++++--------- web/lib/firebase/manalinks.ts | 26 ++++------- web/lib/firebase/txns.ts | 19 ++++---- web/lib/firebase/users.ts | 81 ++++++++++++++++------------------ web/lib/firebase/utils.ts | 7 +++ 8 files changed, 124 insertions(+), 146 deletions(-) diff --git a/web/hooks/use-contract.ts b/web/hooks/use-contract.ts index 9810d9d4..acaf7730 100644 --- a/web/hooks/use-contract.ts +++ b/web/hooks/use-contract.ts @@ -2,16 +2,16 @@ import { useEffect } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { Contract, - contractDocRef, + contracts, listenForContract, } from 'web/lib/firebase/contracts' import { useStateCheckEquality } from './use-state-check-equality' -import { DocumentData } from 'firebase/firestore' +import { doc, DocumentData } from 'firebase/firestore' export const useContract = (contractId: string) => { const result = useFirestoreDocumentData( ['contracts', contractId], - contractDocRef(contractId), + doc(contracts, contractId), { subscribe: true, includeMetadataChanges: true } ) diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index c4d1dff9..158235ca 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { QueryClient } from 'react-query' -import { DocumentData } from 'firebase/firestore' +import { doc, DocumentData } from 'firebase/firestore' import { PrivateUser } from 'common/user' import { getUser, @@ -10,7 +10,7 @@ import { listenForPrivateUser, listenForUser, User, - userDocRef, + users, } from 'web/lib/firebase/users' import { useStateCheckEquality } from './use-state-check-equality' import { identifyUser, setUserProperty } from 'web/lib/service/analytics' @@ -49,7 +49,7 @@ export const usePrivateUser = (userId?: string) => { export const useUserById = (userId: string) => { const result = useFirestoreDocumentData( ['users', userId], - userDocRef(userId), + doc(users, userId), { subscribe: true, includeMetadataChanges: true } ) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index f177d841..d5fb85cb 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -1,6 +1,5 @@ import dayjs from 'dayjs' import { - getFirestore, doc, setDoc, deleteDoc, @@ -16,8 +15,7 @@ import { } from 'firebase/firestore' import { sortBy, sum } from 'lodash' -import { app } from './init' -import { getValues, listenForValue, listenForValues } from './utils' +import { coll, getValues, listenForValue, listenForValues } from './utils' import { BinaryContract, Contract } from 'common/contract' import { getDpmProbability } from 'common/calculate-dpm' import { createRNG, shuffle } from 'common/util/random' @@ -28,6 +26,9 @@ import { MAX_FEED_CONTRACTS } from 'common/recommended-contracts' import { Bet } from 'common/bet' import { Comment } from 'common/comment' import { ENV_CONFIG } from 'common/envs/constants' + +export const contracts = coll('contracts') + export type { Contract } export function contractPath(contract: Contract) { @@ -86,83 +87,72 @@ export function tradingAllowed(contract: Contract) { ) } -const db = getFirestore(app) -export const contractCollection = collection(db, 'contracts') -export const contractDocRef = (contractId: string) => - doc(db, 'contracts', contractId) - // Push contract to Firestore export async function setContract(contract: Contract) { - const docRef = doc(db, 'contracts', contract.id) - await setDoc(docRef, contract) + await setDoc(doc(contracts, contract.id), contract) } export async function updateContract( contractId: string, update: Partial ) { - const docRef = doc(db, 'contracts', contractId) - await updateDoc(docRef, update) + await updateDoc(doc(contracts, contractId), update) } export async function getContractFromId(contractId: string) { - const docRef = doc(db, 'contracts', contractId) - const result = await getDoc(docRef) - - return result.exists() ? (result.data() as Contract) : undefined + const result = await getDoc(doc(contracts, contractId)) + return result.exists() ? result.data() : undefined } export async function getContractFromSlug(slug: string) { - const q = query(contractCollection, where('slug', '==', slug)) + const q = query(contracts, where('slug', '==', slug)) const snapshot = await getDocs(q) - - return snapshot.empty ? undefined : (snapshot.docs[0].data() as Contract) + return snapshot.empty ? undefined : snapshot.docs[0].data() } export async function deleteContract(contractId: string) { - const docRef = doc(db, 'contracts', contractId) - await deleteDoc(docRef) + await deleteDoc(doc(contracts, contractId)) } export async function listContracts(creatorId: string): Promise { const q = query( - contractCollection, + contracts, where('creatorId', '==', creatorId), orderBy('createdTime', 'desc') ) const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export async function listTaggedContractsCaseInsensitive( tag: string ): Promise { const q = query( - contractCollection, + contracts, where('lowercaseTags', 'array-contains', tag.toLowerCase()), orderBy('createdTime', 'desc') ) const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export async function listAllContracts( n: number, before?: string ): Promise { - let q = query(contractCollection, orderBy('createdTime', 'desc'), limit(n)) + let q = query(contracts, orderBy('createdTime', 'desc'), limit(n)) if (before != null) { - const snap = await getDoc(doc(db, 'contracts', before)) + const snap = await getDoc(doc(contracts, before)) q = query(q, startAfter(snap)) } const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export function listenForContracts( setContracts: (contracts: Contract[]) => void ) { - const q = query(contractCollection, orderBy('createdTime', 'desc')) + const q = query(contracts, orderBy('createdTime', 'desc')) return listenForValues(q, setContracts) } @@ -171,7 +161,7 @@ export function listenForUserContracts( setContracts: (contracts: Contract[]) => void ) { const q = query( - contractCollection, + contracts, where('creatorId', '==', creatorId), orderBy('createdTime', 'desc') ) @@ -179,7 +169,7 @@ export function listenForUserContracts( } const activeContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), where('volume7Days', '>', 0) @@ -196,7 +186,7 @@ export function listenForActiveContracts( } const inactiveContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('closeTime', '>', Date.now()), where('visibility', '==', 'public'), @@ -214,7 +204,7 @@ export function listenForInactiveContracts( } const newContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('volume7Days', '==', 0), where('createdTime', '>', Date.now() - 7 * DAY_MS) @@ -230,7 +220,7 @@ export function listenForContract( contractId: string, setContract: (contract: Contract | null) => void ) { - const contractRef = doc(contractCollection, contractId) + const contractRef = doc(contracts, contractId) return listenForValue(contractRef, setContract) } @@ -242,7 +232,7 @@ function chooseRandomSubset(contracts: Contract[], count: number) { } const hotContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), orderBy('volume24Hours', 'desc'), @@ -262,22 +252,22 @@ export function listenForHotContracts( } export async function getHotContracts() { - const contracts = await getValues(hotContractsQuery) + const data = await getValues(hotContractsQuery) return sortBy( - chooseRandomSubset(contracts, 10), + chooseRandomSubset(data, 10), (contract) => -1 * contract.volume24Hours ) } export async function getContractsBySlugs(slugs: string[]) { - const q = query(contractCollection, where('slug', 'in', slugs)) + const q = query(contracts, where('slug', 'in', slugs)) const snapshot = await getDocs(q) - const contracts = snapshot.docs.map((doc) => doc.data() as Contract) - return sortBy(contracts, (contract) => -1 * contract.volume24Hours) + const data = snapshot.docs.map((doc) => doc.data()) + return sortBy(data, (contract) => -1 * contract.volume24Hours) } const topWeeklyQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), orderBy('volume7Days', 'desc'), limit(MAX_FEED_CONTRACTS) @@ -287,7 +277,7 @@ export async function getTopWeeklyContracts() { } const closingSoonQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), where('closeTime', '>', Date.now()), @@ -296,15 +286,12 @@ const closingSoonQuery = query( ) export async function getClosingSoonContracts() { - const contracts = await getValues(closingSoonQuery) - return sortBy( - chooseRandomSubset(contracts, 2), - (contract) => contract.closeTime - ) + const data = await getValues(closingSoonQuery) + return sortBy(chooseRandomSubset(data, 2), (contract) => contract.closeTime) } export async function getRecentBetsAndComments(contract: Contract) { - const contractDoc = doc(db, 'contracts', contract.id) + const contractDoc = doc(contracts, contract.id) const [recentBets, recentComments] = await Promise.all([ getValues( diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index d7244f98..36b05452 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -1,7 +1,7 @@ import { - collection, deleteDoc, doc, + getDocs, query, updateDoc, where, @@ -9,11 +9,16 @@ import { import { sortBy } from 'lodash' import { Group } from 'common/group' import { getContractFromId } from './contracts' -import { db } from './init' -import { getValue, getValues, listenForValue, listenForValues } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { filterDefined } from 'common/util/array' -const groupCollection = collection(db, 'groups') +export const groups = coll('groups') export function groupPath( groupSlug: string, @@ -23,30 +28,29 @@ export function groupPath( } export function updateGroup(group: Group, updates: Partial) { - return updateDoc(doc(groupCollection, group.id), updates) + return updateDoc(doc(groups, group.id), updates) } export function deleteGroup(group: Group) { - return deleteDoc(doc(groupCollection, group.id)) + return deleteDoc(doc(groups, group.id)) } export async function listAllGroups() { - return getValues(groupCollection) + return getValues(groups) } export function listenForGroups(setGroups: (groups: Group[]) => void) { - return listenForValues(groupCollection, setGroups) + return listenForValues(groups, setGroups) } export function getGroup(groupId: string) { - return getValue(doc(groupCollection, groupId)) + return getValue(doc(groups, groupId)) } export async function getGroupBySlug(slug: string) { - const q = query(groupCollection, where('slug', '==', slug)) - const groups = await getValues(q) - - return groups.length === 0 ? null : groups[0] + const q = query(groups, where('slug', '==', slug)) + const docs = (await getDocs(q)).docs + return docs.length === 0 ? null : docs[0].data() } export async function getGroupContracts(group: Group) { @@ -68,14 +72,14 @@ export function listenForGroup( groupId: string, setGroup: (group: Group | null) => void ) { - return listenForValue(doc(groupCollection, groupId), setGroup) + return listenForValue(doc(groups, groupId), setGroup) } export function listenForMemberGroups( userId: string, setGroups: (groups: Group[]) => void ) { - const q = query(groupCollection, where('memberIds', 'array-contains', userId)) + const q = query(groups, where('memberIds', 'array-contains', userId)) return listenForValues(q, (groups) => { const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) @@ -87,12 +91,8 @@ export async function getGroupsWithContractId( contractId: string, setGroups: (groups: Group[]) => void ) { - const q = query( - groupCollection, - where('contractIds', 'array-contains', contractId) - ) - const groups = await getValues(q) - setGroups(groups) + const q = query(groups, where('contractIds', 'array-contains', contractId)) + setGroups(await getValues(q)) } export async function joinGroup(group: Group, userId: string): Promise { diff --git a/web/lib/firebase/manalinks.ts b/web/lib/firebase/manalinks.ts index 67c7a00a..532534df 100644 --- a/web/lib/firebase/manalinks.ts +++ b/web/lib/firebase/manalinks.ts @@ -1,18 +1,12 @@ -import { - collection, - getDoc, - orderBy, - query, - setDoc, - where, -} from 'firebase/firestore' +import { getDoc, orderBy, query, setDoc, where } from 'firebase/firestore' import { doc } from 'firebase/firestore' import { Manalink } from '../../../common/manalink' -import { db } from './init' import { customAlphabet } from 'nanoid' -import { listenForValues } from './utils' +import { coll, listenForValues } from './utils' import { useEffect, useState } from 'react' +export const manalinks = coll('manalinks') + export async function createManalink(data: { fromId: string amount: number @@ -45,29 +39,25 @@ export async function createManalink(data: { message, } - const ref = doc(db, 'manalinks', slug) - await setDoc(ref, manalink) + await setDoc(doc(manalinks, slug), manalink) return slug } -const manalinkCol = collection(db, 'manalinks') - // TODO: This required an index, make sure to also set up in prod function listUserManalinks(fromId?: string) { return query( - manalinkCol, + manalinks, where('fromId', '==', fromId), orderBy('createdTime', 'desc') ) } export async function getManalink(slug: string) { - const docSnap = await getDoc(doc(db, 'manalinks', slug)) - return docSnap.data() as Manalink + return (await getDoc(doc(manalinks, slug))).data() } export function useManalink(slug: string) { - const [manalink, setManalink] = useState(null) + const [manalink, setManalink] = useState(undefined) useEffect(() => { if (slug) { getManalink(slug).then(setManalink) diff --git a/web/lib/firebase/txns.ts b/web/lib/firebase/txns.ts index c4c8aa93..17e9a09b 100644 --- a/web/lib/firebase/txns.ts +++ b/web/lib/firebase/txns.ts @@ -1,15 +1,14 @@ -import { ManalinkTxn, DonationTxn, TipTxn } from 'common/txn' -import { collection, orderBy, query, where } from 'firebase/firestore' -import { db } from './init' -import { getValues, listenForValues } from './utils' +import { ManalinkTxn, DonationTxn, TipTxn, Txn } from 'common/txn' +import { orderBy, query, where } from 'firebase/firestore' +import { coll, getValues, listenForValues } from './utils' import { useState, useEffect } from 'react' import { orderBy as _orderBy } from 'lodash' -const txnCollection = collection(db, 'txns') +export const txns = coll('txns') const getCharityQuery = (charityId: string) => query( - txnCollection, + txns, where('toType', '==', 'CHARITY'), where('toId', '==', charityId), orderBy('createdTime', 'desc') @@ -22,7 +21,7 @@ export function listenForCharityTxns( return listenForValues(getCharityQuery(charityId), setTxns) } -const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY')) +const charitiesQuery = query(txns, where('toType', '==', 'CHARITY')) export function getAllCharityTxns() { return getValues(charitiesQuery) @@ -30,7 +29,7 @@ export function getAllCharityTxns() { const getTipsQuery = (contractId: string) => query( - txnCollection, + txns, where('category', '==', 'TIP'), where('data.contractId', '==', contractId) ) @@ -50,13 +49,13 @@ export function useManalinkTxns(userId: string) { useEffect(() => { // TODO: Need to instantiate these indexes too const fromQuery = query( - txnCollection, + txns, where('fromId', '==', userId), where('category', '==', 'MANALINK'), orderBy('createdTime', 'desc') ) const toQuery = query( - txnCollection, + txns, where('toId', '==', userId), where('category', '==', 'MANALINK'), orderBy('createdTime', 'desc') diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index e9fcbb93..40be6741 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -1,5 +1,4 @@ import { - getFirestore, doc, setDoc, getDoc, @@ -23,58 +22,62 @@ import { } from 'firebase/auth' import { throttle, zip } from 'lodash' -import { app } from './init' +import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' import { createUser } from './fn-call' -import { getValue, getValues, listenForValue, listenForValues } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { feed } from 'common/feed' import { CATEGORY_LIST } from 'common/categories' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' +export const users = coll('users') +export const privateUsers = coll('private-users') + export type { User } export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime' -const db = getFirestore(app) export const auth = getAuth(app) -export const userDocRef = (userId: string) => doc(db, 'users', userId) - export async function getUser(userId: string) { - const docSnap = await getDoc(userDocRef(userId)) - return docSnap.data() as User + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return (await getDoc(doc(users, userId))).data()! } export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. - const userCollection = collection(db, 'users') - const q = query(userCollection, where('username', '==', username), limit(1)) - const docs = await getDocs(q) - const users = docs.docs.map((doc) => doc.data() as User) - return users[0] || null + const q = query(users, where('username', '==', username), limit(1)) + const docs = (await getDocs(q)).docs + return docs.length > 0 ? docs[0].data() : null } export async function setUser(userId: string, user: User) { - await setDoc(doc(db, 'users', userId), user) + await setDoc(doc(users, userId), user) } export async function updateUser(userId: string, update: Partial) { - await updateDoc(doc(db, 'users', userId), { ...update }) + await updateDoc(doc(users, userId), { ...update }) } export async function updatePrivateUser( userId: string, update: Partial ) { - await updateDoc(doc(db, 'private-users', userId), { ...update }) + await updateDoc(doc(privateUsers, userId), { ...update }) } export function listenForUser( userId: string, setUser: (user: User | null) => void ) { - const userRef = doc(db, 'users', userId) + const userRef = doc(users, userId) return listenForValue(userRef, setUser) } @@ -82,7 +85,7 @@ export function listenForPrivateUser( userId: string, setPrivateUser: (privateUser: PrivateUser | null) => void ) { - const userRef = doc(db, 'private-users', userId) + const userRef = doc(privateUsers, userId) return listenForValue(userRef, setPrivateUser) } @@ -152,36 +155,29 @@ export async function listUsers(userIds: string[]) { if (userIds.length > 10) { throw new Error('Too many users requested at once; Firestore limits to 10') } - const userCollection = collection(db, 'users') - const q = query(userCollection, where('id', 'in', userIds)) - const docs = await getDocs(q) - return docs.docs.map((doc) => doc.data() as User) + const q = query(users, where('id', 'in', userIds)) + const docs = (await getDocs(q)).docs + return docs.map((doc) => doc.data()) } export async function listAllUsers() { - const userCollection = collection(db, 'users') - const q = query(userCollection) - const docs = await getDocs(q) - return docs.docs.map((doc) => doc.data() as User) + const docs = (await getDocs(users)).docs + return docs.map((doc) => doc.data()) } export function listenForAllUsers(setUsers: (users: User[]) => void) { - const userCollection = collection(db, 'users') - const q = query(userCollection) - listenForValues(q, setUsers) + listenForValues(users, setUsers) } export function listenForPrivateUsers( setUsers: (users: PrivateUser[]) => void ) { - const userCollection = collection(db, 'private-users') - const q = query(userCollection) - listenForValues(q, setUsers) + listenForValues(privateUsers, setUsers) } export function getTopTraders(period: Period) { const topTraders = query( - collection(db, 'users'), + users, orderBy('profitCached.' + period, 'desc'), limit(20) ) @@ -191,7 +187,7 @@ export function getTopTraders(period: Period) { export function getTopCreators(period: Period) { const topCreators = query( - collection(db, 'users'), + users, orderBy('creatorVolumeCached.' + period, 'desc'), limit(20) ) @@ -199,22 +195,21 @@ export function getTopCreators(period: Period) { } export async function getTopFollowed() { - const users = await getValues(topFollowedQuery) - return users.slice(0, 20) + return (await getValues(topFollowedQuery)).slice(0, 20) } const topFollowedQuery = query( - collection(db, 'users'), + users, orderBy('followerCountCached', 'desc'), limit(20) ) export function getUsers() { - return getValues(collection(db, 'users')) + return getValues(users) } export async function getUserFeed(userId: string) { - const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed') + const feedDoc = doc(privateUsers, userId, 'cache', 'feed') const userFeed = await getValue<{ feed: feed }>(feedDoc) @@ -222,7 +217,7 @@ export async function getUserFeed(userId: string) { } export async function getCategoryFeeds(userId: string) { - const cacheCollection = collection(db, 'private-users', userId, 'cache') + const cacheCollection = collection(privateUsers, userId, 'cache') const feedData = await Promise.all( CATEGORY_LIST.map((category) => getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`)) @@ -233,7 +228,7 @@ export async function getCategoryFeeds(userId: string) { } export async function follow(userId: string, followedUserId: string) { - const followDoc = doc(db, 'users', userId, 'follows', followedUserId) + const followDoc = doc(collection(users, userId, 'follows'), followedUserId) await setDoc(followDoc, { userId: followedUserId, timestamp: Date.now(), @@ -241,7 +236,7 @@ export async function follow(userId: string, followedUserId: string) { } export async function unfollow(userId: string, unfollowedUserId: string) { - const followDoc = doc(db, 'users', userId, 'follows', unfollowedUserId) + const followDoc = doc(collection(users, userId, 'follows'), unfollowedUserId) await deleteDoc(followDoc) } @@ -259,7 +254,7 @@ export function listenForFollows( userId: string, setFollowIds: (followIds: string[]) => void ) { - const follows = collection(db, 'users', userId, 'follows') + const follows = collection(users, userId, 'follows') return listenForValues<{ userId: string }>(follows, (docs) => setFollowIds(docs.map(({ userId }) => userId)) ) diff --git a/web/lib/firebase/utils.ts b/web/lib/firebase/utils.ts index 1a9e13c5..e63c2d96 100644 --- a/web/lib/firebase/utils.ts +++ b/web/lib/firebase/utils.ts @@ -1,10 +1,17 @@ import { + collection, getDoc, getDocs, onSnapshot, Query, + CollectionReference, DocumentReference, } from 'firebase/firestore' +import { db } from './init' + +export const coll = (path: string, ...rest: string[]) => { + return collection(db, path, ...rest) as CollectionReference +} export const getValue = async (doc: DocumentReference) => { const snap = await getDoc(doc) From 19d12c949a86bd2ae7145bd087cee1dd0bfceb55 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 29 Jun 2022 17:51:11 -0500 Subject: [PATCH 011/519] Add a line spacer on the sidebar --- web/components/nav/sidebar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index aa0acf05..402f5e12 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -230,7 +230,12 @@ export default function Sidebar(props: { className?: string }) { buttonContent={} /> - {memberItems.length > 0 && } + {/* Spacer if there are any groups */} + {memberItems.length > 0 && ( +
+
+
+ )}
From 7bbc4256901b4813b943166edb83c3acfda1da32 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 29 Jun 2022 17:54:08 -0500 Subject: [PATCH 012/519] Only show "My Groups" when there is at least 1 group --- web/pages/groups.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index a8f99b23..22fe7661 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -107,7 +107,7 @@ export default function Groups(props: { 0 ? [ { title: 'My Groups', From 2fbbc660297d8ed39c66e8dc53c1f687b72f6748 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 29 Jun 2022 16:31:53 -0700 Subject: [PATCH 013/519] Point v2 functions @ localhost during emulation (#597) --- web/lib/firebase/api-call.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index d46b3afa..7509a9f1 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -41,8 +41,13 @@ export async function call(url: string, method: string, params: any) { // one less hop export function getFunctionUrl(name: string) { - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app` + if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { + const { projectId, region } = ENV_CONFIG.firebaseConfig + return `http://localhost:5001/${projectId}/${region}/${name}` + } else { + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app` + } } export function createMarket(params: any) { From fc7f19e78512ab14db5160b8b0ec07d9dd70c3df Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 29 Jun 2022 16:47:06 -0700 Subject: [PATCH 014/519] Finalize v2 resolvemarket migration (#598) * Update resolve-market to be a v2 function * Cleanup API error responses * Update frontend to use v2 version of resolvemarket * Appease ESLint * Address review comments * Appease ESLint * Remove unnecessary auth check * Fix logic bug in FR market validation * Make it so you can specify runtime opts for v2 functions * Cleanup to resolve market API resolutions input, fixes * Fix up tiny lint * Last minute cleanup to resolvemarket FR API input validation Co-authored-by: Benjamin --- common/payouts.ts | 18 +- common/scoring.ts | 2 +- functions/src/api.ts | 17 +- functions/src/create-contract.ts | 2 +- functions/src/create-group.ts | 2 +- functions/src/health.ts | 2 +- functions/src/index.ts | 2 +- functions/src/place-bet.ts | 2 +- functions/src/resolve-market.ts | 314 ++++++++++-------- .../src/scripts/pay-out-contract-again.ts | 2 +- functions/src/sell-bet.ts | 2 +- functions/src/sell-shares.ts | 2 +- .../answers/answer-resolve-panel.tsx | 32 +- web/components/numeric-resolution-panel.tsx | 27 +- web/components/resolution-panel.tsx | 27 +- web/lib/firebase/api-call.ts | 4 + web/lib/firebase/fn-call.ts | 11 - 17 files changed, 255 insertions(+), 213 deletions(-) diff --git a/common/payouts.ts b/common/payouts.ts index a3f105cf..f2c8d271 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -48,12 +48,12 @@ export type PayoutInfo = { export const getPayouts = ( outcome: string | undefined, - resolutions: { - [outcome: string]: number - }, contract: Contract, bets: Bet[], liquidities: LiquidityProvision[], + resolutions?: { + [outcome: string]: number + }, resolutionProbability?: number ): PayoutInfo => { if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') { @@ -67,9 +67,9 @@ export const getPayouts = ( } return getDpmPayouts( outcome, - resolutions, contract, bets, + resolutions, resolutionProbability ) } @@ -100,11 +100,11 @@ export const getFixedPayouts = ( export const getDpmPayouts = ( outcome: string | undefined, - resolutions: { - [outcome: string]: number - }, contract: DPMContract, bets: Bet[], + resolutions?: { + [outcome: string]: number + }, resolutionProbability?: number ): PayoutInfo => { const openBets = bets.filter((b) => !b.isSold && !b.sale) @@ -115,8 +115,8 @@ export const getDpmPayouts = ( return getDpmStandardPayouts(outcome, contract, openBets) case 'MKT': - return contract.outcomeType === 'FREE_RESPONSE' - ? getPayoutsMultiOutcome(resolutions, contract, openBets) + return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ? getPayoutsMultiOutcome(resolutions!, contract, openBets) : getDpmMktPayouts(contract, openBets, resolutionProbability) case 'CANCEL': case undefined: diff --git a/common/scoring.ts b/common/scoring.ts index d4e40267..39a342fd 100644 --- a/common/scoring.ts +++ b/common/scoring.ts @@ -42,10 +42,10 @@ export function scoreUsersByContract(contract: Contract, bets: Bet[]) { ) const { payouts: resolvePayouts } = getPayouts( resolution as string, - {}, contract, openBets, [], + {}, resolutionProb ) diff --git a/functions/src/api.ts b/functions/src/api.ts index f7efab5a..290ea3d8 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -108,7 +108,12 @@ export const validate = (schema: T, val: unknown) => { } } -const DEFAULT_OPTS: HttpsOptions = { +interface EndpointOptions extends HttpsOptions { + methods?: string[] +} + +const DEFAULT_OPTS = { + methods: ['POST'], minInstances: 1, concurrency: 100, memory: '2GiB', @@ -116,12 +121,13 @@ const DEFAULT_OPTS: HttpsOptions = { cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], } -export const newEndpoint = (methods: [string], fn: Handler) => - onRequest(DEFAULT_OPTS, async (req, res) => { +export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { + const opts = Object.assign(endpointOpts, DEFAULT_OPTS) + return onRequest(opts, async (req, res) => { log('Request processing started.') try { - if (!methods.includes(req.method)) { - const allowed = methods.join(', ') + if (!opts.methods.includes(req.method)) { + const allowed = opts.methods.join(', ') throw new APIError(405, `This endpoint supports only ${allowed}.`) } const authedUser = await lookupUser(await parseCredentials(req)) @@ -140,3 +146,4 @@ export const newEndpoint = (methods: [string], fn: Handler) => } } }) +} diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 71d778b3..c9468fdc 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -50,7 +50,7 @@ const numericSchema = z.object({ max: z.number(), }) -export const createmarket = newEndpoint(['POST'], async (req, auth) => { +export const createmarket = newEndpoint({}, async (req, auth) => { const { question, description, tags, closeTime, outcomeType, groupId } = validate(bodySchema, req.body) diff --git a/functions/src/create-group.ts b/functions/src/create-group.ts index e7ee0cf5..a9626916 100644 --- a/functions/src/create-group.ts +++ b/functions/src/create-group.ts @@ -20,7 +20,7 @@ const bodySchema = z.object({ about: z.string().min(1).max(MAX_ABOUT_LENGTH).optional(), }) -export const creategroup = newEndpoint(['POST'], async (req, auth) => { +export const creategroup = newEndpoint({}, async (req, auth) => { const { name, about, memberIds, anyoneCanJoin } = validate( bodySchema, req.body diff --git a/functions/src/health.ts b/functions/src/health.ts index 6f4d73dc..938261db 100644 --- a/functions/src/health.ts +++ b/functions/src/health.ts @@ -1,6 +1,6 @@ import { newEndpoint } from './api' -export const health = newEndpoint(['GET'], async (_req, auth) => { +export const health = newEndpoint({ methods: ['GET'] }, async (_req, auth) => { return { message: 'Server is working.', uid: auth.uid, diff --git a/functions/src/index.ts b/functions/src/index.ts index dcd50e66..726aba15 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,7 +6,6 @@ admin.initializeApp() // export * from './keep-awake' export * from './claim-manalink' export * from './transact' -export * from './resolve-market' export * from './stripe' export * from './create-user' export * from './create-answer' @@ -37,3 +36,4 @@ export * from './sell-shares' export * from './create-contract' export * from './withdraw-liquidity' export * from './create-group' +export * from './resolve-market' diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 1b5dd8bc..06d27668 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -33,7 +33,7 @@ const numericSchema = z.object({ value: z.number(), }) -export const placebet = newEndpoint(['POST'], async (req, auth) => { +export const placebet = newEndpoint({}, async (req, auth) => { log('Inside endpoint handler.') const { amount, contractId } = validate(bodySchema, req.body) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 43cb4839..b36ec3ef 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -1,8 +1,8 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash' -import { Contract, resolution, RESOLUTIONS } from '../../common/contract' +import { Contract, RESOLUTIONS } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' import { getUser, isProd, payUser } from './utils' @@ -15,156 +15,150 @@ import { } from '../../common/payouts' import { removeUndefinedProps } from '../../common/util/object' import { LiquidityProvision } from '../../common/liquidity-provision' +import { APIError, newEndpoint, validate } from './api' -export const resolveMarket = functions - .runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) - .https.onCall( - async ( - data: { - outcome: resolution - value?: number - contractId: string - probabilityInt?: number - resolutions?: { [outcome: string]: number } - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + contractId: z.string(), +}) - const { outcome, contractId, probabilityInt, resolutions, value } = data +const binarySchema = z.object({ + outcome: z.enum(RESOLUTIONS), + probabilityInt: z.number().gte(0).lt(100).optional(), +}) - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await contractDoc.get() - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - const { creatorId, outcomeType, closeTime } = contract +const freeResponseSchema = z.union([ + z.object({ + outcome: z.literal('CANCEL'), + }), + z.object({ + outcome: z.literal('MKT'), + resolutions: z.array( + z.object({ + answer: z.number().int().nonnegative(), + pct: z.number().gte(0).lt(100), + }) + ), + }), + z.object({ + outcome: z.number().int().nonnegative(), + }), +]) - if (outcomeType === 'BINARY') { - if (!RESOLUTIONS.includes(outcome)) - return { status: 'error', message: 'Invalid outcome' } - } else if (outcomeType === 'FREE_RESPONSE') { - if ( - isNaN(+outcome) && - !(outcome === 'MKT' && resolutions) && - outcome !== 'CANCEL' - ) - return { status: 'error', message: 'Invalid outcome' } - } else if (outcomeType === 'NUMERIC') { - if (isNaN(+outcome) && outcome !== 'CANCEL') - return { status: 'error', message: 'Invalid outcome' } - } else { - return { status: 'error', message: 'Invalid contract outcomeType' } - } +const numericSchema = z.object({ + outcome: z.union([z.literal('CANCEL'), z.string()]), + value: z.number().optional(), +}) - if (value !== undefined && !isFinite(value)) - return { status: 'error', message: 'Invalid value' } +const opts = { secrets: ['MAILGUN_KEY'] } +export const resolvemarket = newEndpoint(opts, async (req, auth) => { + const { contractId } = validate(bodySchema, req.body) + const userId = auth.uid - if ( - outcomeType === 'BINARY' && - probabilityInt !== undefined && - (probabilityInt < 0 || - probabilityInt > 100 || - !isFinite(probabilityInt)) - ) - return { status: 'error', message: 'Invalid probability' } + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await contractDoc.get() + if (!contractSnap.exists) + throw new APIError(404, 'No contract exists with the provided ID') + const contract = contractSnap.data() as Contract + const { creatorId, outcomeType, closeTime } = contract - if (creatorId !== userId) - return { status: 'error', message: 'User not creator of contract' } - - if (contract.resolution) - return { status: 'error', message: 'Contract already resolved' } - - const creator = await getUser(creatorId) - if (!creator) return { status: 'error', message: 'Creator not found' } - - const resolutionProbability = - probabilityInt !== undefined ? probabilityInt / 100 : undefined - - const resolutionTime = Date.now() - const newCloseTime = closeTime - ? Math.min(closeTime, resolutionTime) - : closeTime - - const betsSnap = await firestore - .collection(`contracts/${contractId}/bets`) - .get() - - const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - - const liquiditiesSnap = await firestore - .collection(`contracts/${contractId}/liquidity`) - .get() - - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) - - const { payouts, creatorPayout, liquidityPayouts, collectedFees } = - getPayouts( - outcome, - resolutions ?? {}, - contract, - bets, - liquidities, - resolutionProbability - ) - - await contractDoc.update( - removeUndefinedProps({ - isResolved: true, - resolution: outcome, - resolutionValue: value, - resolutionTime, - closeTime: newCloseTime, - resolutionProbability, - resolutions, - collectedFees, - }) - ) - - console.log('contract ', contractId, 'resolved to:', outcome) - - const openBets = bets.filter((b) => !b.isSold && !b.sale) - const loanPayouts = getLoanPayouts(openBets) - - if (!isProd()) - console.log( - 'payouts:', - payouts, - 'creator payout:', - creatorPayout, - 'liquidity payout:' - ) - - if (creatorPayout) - await processPayouts( - [{ userId: creatorId, payout: creatorPayout }], - true - ) - - await processPayouts(liquidityPayouts, true) - - const result = await processPayouts([...payouts, ...loanPayouts]) - - const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) - - await sendResolutionEmails( - openBets, - userPayoutsWithoutLoans, - creator, - creatorPayout, - contract, - outcome, - resolutionProbability, - resolutions - ) - - return result - } + const { value, resolutions, probabilityInt, outcome } = getResolutionParams( + outcomeType, + req.body ) + if (creatorId !== userId) + throw new APIError(403, 'User is not creator of contract') + + if (contract.resolution) throw new APIError(400, 'Contract already resolved') + + const creator = await getUser(creatorId) + if (!creator) throw new APIError(500, 'Creator not found') + + const resolutionProbability = + probabilityInt !== undefined ? probabilityInt / 100 : undefined + + const resolutionTime = Date.now() + const newCloseTime = closeTime + ? Math.min(closeTime, resolutionTime) + : closeTime + + const betsSnap = await firestore + .collection(`contracts/${contractId}/bets`) + .get() + + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) + + const liquiditiesSnap = await firestore + .collection(`contracts/${contractId}/liquidity`) + .get() + + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision + ) + + const { payouts, creatorPayout, liquidityPayouts, collectedFees } = + getPayouts( + outcome, + contract, + bets, + liquidities, + resolutions, + resolutionProbability + ) + + const updatedContract = { + ...contract, + ...removeUndefinedProps({ + isResolved: true, + resolution: outcome, + resolutionValue: value, + resolutionTime, + closeTime: newCloseTime, + resolutionProbability, + resolutions, + collectedFees, + }), + } + + await contractDoc.update(updatedContract) + + console.log('contract ', contractId, 'resolved to:', outcome) + + const openBets = bets.filter((b) => !b.isSold && !b.sale) + const loanPayouts = getLoanPayouts(openBets) + + if (!isProd()) + console.log( + 'payouts:', + payouts, + 'creator payout:', + creatorPayout, + 'liquidity payout:' + ) + + if (creatorPayout) + await processPayouts([{ userId: creatorId, payout: creatorPayout }], true) + + await processPayouts(liquidityPayouts, true) + + await processPayouts([...payouts, ...loanPayouts]) + + const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) + + await sendResolutionEmails( + openBets, + userPayoutsWithoutLoans, + creator, + creatorPayout, + contract, + outcome, + resolutionProbability, + resolutions + ) + + return updatedContract +}) + const processPayouts = async (payouts: Payout[], isDeposit = false) => { const userPayouts = groupPayoutsByUser(payouts) @@ -221,4 +215,38 @@ const sendResolutionEmails = async ( ) } +function getResolutionParams(outcomeType: string, body: string) { + if (outcomeType === 'NUMERIC') { + return { + ...validate(numericSchema, body), + resolutions: undefined, + probabilityInt: undefined, + } + } else if (outcomeType === 'FREE_RESPONSE') { + const freeResponseParams = validate(freeResponseSchema, body) + const { outcome } = freeResponseParams + const resolutions = + 'resolutions' in freeResponseParams + ? Object.fromEntries( + freeResponseParams.resolutions.map((r) => [r.answer, r.pct]) + ) + : undefined + return { + // Free Response outcome IDs are numbers by convention, + // but treated as strings everywhere else. + outcome: outcome.toString(), + resolutions, + value: undefined, + probabilityInt: undefined, + } + } else if (outcomeType === 'BINARY') { + return { + ...validate(binarySchema, body), + value: undefined, + resolutions: undefined, + } + } + throw new APIError(500, `Invalid outcome type: ${outcomeType}`) +} + const firestore = admin.firestore() diff --git a/functions/src/scripts/pay-out-contract-again.ts b/functions/src/scripts/pay-out-contract-again.ts index 1686ebd9..a121889f 100644 --- a/functions/src/scripts/pay-out-contract-again.ts +++ b/functions/src/scripts/pay-out-contract-again.ts @@ -27,10 +27,10 @@ async function checkIfPayOutAgain(contractRef: DocRef, contract: Contract) { const { payouts } = getPayouts( resolution, - resolutions, contract, openBets, [], + resolutions, resolutionProbability ) diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 419206c0..b3362159 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -13,7 +13,7 @@ const bodySchema = z.object({ betId: z.string(), }) -export const sellbet = newEndpoint(['POST'], async (req, auth) => { +export const sellbet = newEndpoint({}, async (req, auth) => { const { contractId, betId } = validate(bodySchema, req.body) // run as transaction to prevent race conditions diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index dd4e2ec5..26374a16 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -16,7 +16,7 @@ const bodySchema = z.object({ outcome: z.enum(['YES', 'NO']), }) -export const sellshares = newEndpoint(['POST'], async (req, auth) => { +export const sellshares = newEndpoint({}, async (req, auth) => { const { contractId, shares, outcome } = validate(bodySchema, req.body) // Run as transaction to prevent race conditions. diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 81b94550..6b8e2885 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -1,10 +1,10 @@ import clsx from 'clsx' -import { sum, mapValues } from 'lodash' +import { sum } from 'lodash' import { useState } from 'react' import { Contract, FreeResponse } from 'common/contract' import { Col } from '../layout/col' -import { resolveMarket } from 'web/lib/firebase/fn-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api-call' import { Row } from '../layout/row' import { ChooseCancelSelector } from '../yes-no-selector' import { ResolveConfirmationButton } from '../confirmation-button' @@ -31,30 +31,34 @@ export function AnswerResolvePanel(props: { setIsSubmitting(true) const totalProb = sum(Object.values(chosenAnswers)) - const normalizedProbs = mapValues( - chosenAnswers, - (prob) => (100 * prob) / totalProb - ) + const resolutions = Object.entries(chosenAnswers).map(([i, p]) => { + return { answer: parseInt(i), pct: (100 * p) / totalProb } + }) const resolutionProps = removeUndefinedProps({ outcome: resolveOption === 'CHOOSE' - ? answers[0] + ? parseInt(answers[0]) : resolveOption === 'CHOOSE_MULTIPLE' ? 'MKT' : 'CANCEL', resolutions: - resolveOption === 'CHOOSE_MULTIPLE' ? normalizedProbs : undefined, + resolveOption === 'CHOOSE_MULTIPLE' ? resolutions : undefined, contractId: contract.id, }) - const result = await resolveMarket(resolutionProps).then((r) => r.data) - - console.log('resolved', resolutionProps, 'result:', result) - - if (result?.status !== 'success') { - setError(result?.message || 'Error resolving market') + try { + const result = await resolveMarket(resolutionProps) + console.log('resolved', resolutionProps, 'result:', result) + } catch (e) { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error resolving market') + } } + setResolveOption(undefined) setIsSubmitting(false) } diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index f05a1c0a..ebac68e5 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users' import { NumberCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' -import { resolveMarket } from 'web/lib/firebase/fn-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api-call' import { NumericContract } from 'common/contract' import { BucketInput } from './bucket-input' @@ -37,17 +37,22 @@ export function NumericResolutionPanel(props: { setIsSubmitting(true) - const result = await resolveMarket({ - outcome: finalOutcome, - value, - contractId: contract.id, - }).then((r) => r.data) - - console.log('resolved', outcome, 'result:', result) - - if (result?.status !== 'success') { - setError(result?.message || 'Error resolving market') + try { + const result = await resolveMarket({ + outcome: finalOutcome, + value, + contractId: contract.id, + }) + console.log('resolved', outcome, 'result:', result) + } catch (e) { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error resolving market') + } } + setIsSubmitting(false) } diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 8b453765..a46d9478 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users' import { YesNoCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' -import { resolveMarket } from 'web/lib/firebase/fn-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api-call' import { ProbabilitySelector } from './probability-selector' import { DPM_CREATOR_FEE } from 'common/fees' import { getProbability } from 'common/calculate' @@ -42,17 +42,22 @@ export function ResolutionPanel(props: { setIsSubmitting(true) - const result = await resolveMarket({ - outcome, - contractId: contract.id, - probabilityInt: prob, - }).then((r) => r.data) - - console.log('resolved', outcome, 'result:', result) - - if (result?.status !== 'success') { - setError(result?.message || 'Error resolving market') + try { + const result = await resolveMarket({ + outcome, + contractId: contract.id, + probabilityInt: prob, + }) + console.log('resolved', outcome, 'result:', result) + } catch (e) { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error resolving market') + } } + setIsSubmitting(false) } diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 7509a9f1..e02872ae 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -54,6 +54,10 @@ export function createMarket(params: any) { return call(getFunctionUrl('createmarket'), 'POST', params) } +export function resolveMarket(params: any) { + return call(getFunctionUrl('resolvemarket'), 'POST', params) +} + export function placeBet(params: any) { return call(getFunctionUrl('placebet'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index e99bf393..ce78ac3a 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -29,17 +29,6 @@ export const createAnswer = cloudFunction< } >('createAnswer') -export const resolveMarket = cloudFunction< - { - outcome: string - value?: number - contractId: string - probabilityInt?: number - resolutions?: { [outcome: string]: number } - }, - { status: 'error' | 'success'; message?: string } ->('resolveMarket') - export const createUser: () => Promise = () => { const local = safeLocalStorage() let deviceToken = local?.getItem('device-token') From a5a0a1370a23399446eeecf4d16233a94243e013 Mon Sep 17 00:00:00 2001 From: Ben Congdon Date: Thu, 30 Jun 2022 08:07:28 -0700 Subject: [PATCH 015/519] Remove daily free market text from docs (#601) --- docs/docs/market-details.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/docs/market-details.md b/docs/docs/market-details.md index f7eeb0f6..9836b850 100644 --- a/docs/docs/market-details.md +++ b/docs/docs/market-details.md @@ -19,7 +19,6 @@ for the pool to be sorted into. - Users can create a market on any question they want. - When a user creates a market, they must choose a close date, after which trading will halt. - They must also pay a M$100 market creation fee, which is used as liquidity to subsidize trading on the market. - - The creation fee for the first market created each day is provided by Manifold. - The market creator will earn a commission on all bets placed in the market. - The market creator is responsible for resolving each market in a timely manner. All fees earned as a commission will be paid out after resolution. - Creators can also resolve N/A to cancel all transactions and reverse all transactions made on the market - this includes profits from selling shares. From c5efd5b7d00770ed78ed572ff2b006b8143300fe Mon Sep 17 00:00:00 2001 From: Ben Congdon Date: Thu, 30 Jun 2022 15:11:45 -0700 Subject: [PATCH 016/519] Market Resolution API (#600) * Add market resolution API * Add additional free market resolution validation * Address review comments * Refactor resolution validation code somewhat Co-authored-by: Marshall Polaris --- docs/docs/api.md | 55 +++++++++++++++- functions/src/resolve-market.ts | 66 ++++++++++++++----- web/lib/api/proxy.ts | 7 +- .../api/v0/market/{[id].ts => [id]/index.ts} | 2 +- web/pages/api/v0/market/[id]/resolve.ts | 28 ++++++++ 5 files changed, 138 insertions(+), 20 deletions(-) rename web/pages/api/v0/market/{[id].ts => [id]/index.ts} (93%) create mode 100644 web/pages/api/v0/market/[id]/resolve.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index ffdaa65f..9172fc5a 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -456,7 +456,6 @@ Requires no authorization. } ``` - ### `POST /v0/bet` Places a new bet on behalf of the authorized user. @@ -514,6 +513,60 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat "initialProb":25}' ``` +### `POST /v0/market/[marketId]/resolve` + +Resolves a market on behalf of the authorized user. + +Parameters: + +For binary markets: + +- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`. +- `probabilityInt`: Optional. The probability to use for `MKT` resolution. + +For free response markets: + +- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the Free Response outcome ID. +- `resolutions`: A map of from outcome => number to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. + +For numeric markets: + +- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID. +- `value`: The value that the market may resolves to. + +Example request: + +``` +# Resolve a binary market +$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' + --data-raw '{"contractId":"{...}", \ + "outcome":"YES"}' + +# Resolve a binary market with a specified probability +$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' + --data-raw '{"contractId":"{...}", \ + "outcome":"MKT", + "probabilityInt": 75}' + +# Resolve a free response market with a single answer chosen +$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' + --data-raw '{"contractId":"{...}", \ + "outcome":"{...}"}' + +# Resolve a free response market with multiple answers chosen +$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' + --data-raw '{"contractId":"{...}", \ + "outcome":"MKT", + "resolutions": { + "{...}": 1, + "{...}": 2, + }}' +``` + ## Changelog - 2022-06-08: Add paging to markets endpoint diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index b36ec3ef..ee78dfec 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -2,7 +2,11 @@ import * as admin from 'firebase-admin' import { z } from 'zod' import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash' -import { Contract, RESOLUTIONS } from '../../common/contract' +import { + Contract, + FreeResponseContract, + RESOLUTIONS, +} from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' import { getUser, isProd, payUser } from './utils' @@ -59,10 +63,10 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => { if (!contractSnap.exists) throw new APIError(404, 'No contract exists with the provided ID') const contract = contractSnap.data() as Contract - const { creatorId, outcomeType, closeTime } = contract + const { creatorId, closeTime } = contract const { value, resolutions, probabilityInt, outcome } = getResolutionParams( - outcomeType, + contract, req.body ) @@ -215,7 +219,8 @@ const sendResolutionEmails = async ( ) } -function getResolutionParams(outcomeType: string, body: string) { +function getResolutionParams(contract: Contract, body: string) { + const { outcomeType } = contract if (outcomeType === 'NUMERIC') { return { ...validate(numericSchema, body), @@ -225,19 +230,39 @@ function getResolutionParams(outcomeType: string, body: string) { } else if (outcomeType === 'FREE_RESPONSE') { const freeResponseParams = validate(freeResponseSchema, body) const { outcome } = freeResponseParams - const resolutions = - 'resolutions' in freeResponseParams - ? Object.fromEntries( - freeResponseParams.resolutions.map((r) => [r.answer, r.pct]) - ) - : undefined - return { - // Free Response outcome IDs are numbers by convention, - // but treated as strings everywhere else. - outcome: outcome.toString(), - resolutions, - value: undefined, - probabilityInt: undefined, + switch (outcome) { + case 'CANCEL': + return { + outcome: outcome.toString(), + resolutions: undefined, + value: undefined, + probabilityInt: undefined, + } + case 'MKT': { + const { resolutions } = freeResponseParams + resolutions.forEach(({ answer }) => validateAnswer(contract, answer)) + const pctSum = sumBy(resolutions, ({ pct }) => pct) + if (Math.abs(pctSum - 100) > 0.1) { + throw new APIError(400, 'Resolution percentages must sum to 100') + } + return { + outcome: outcome.toString(), + resolutions: Object.fromEntries( + resolutions.map((r) => [r.answer, r.pct]) + ), + value: undefined, + probabilityInt: undefined, + } + } + default: { + validateAnswer(contract, outcome) + return { + outcome: outcome.toString(), + resolutions: undefined, + value: undefined, + probabilityInt: undefined, + } + } } } else if (outcomeType === 'BINARY') { return { @@ -249,4 +274,11 @@ function getResolutionParams(outcomeType: string, body: string) { throw new APIError(500, `Invalid outcome type: ${outcomeType}`) } +function validateAnswer(contract: FreeResponseContract, answer: number) { + const validIds = contract.answers.map((a) => a.id) + if (!validIds.includes(answer.toString())) { + throw new APIError(400, `${answer} is not a valid answer ID`) + } +} + const firestore = admin.firestore() diff --git a/web/lib/api/proxy.ts b/web/lib/api/proxy.ts index ec027518..294868ac 100644 --- a/web/lib/api/proxy.ts +++ b/web/lib/api/proxy.ts @@ -41,7 +41,12 @@ export const fetchBackend = (req: NextApiRequest, name: string) => { 'Origin', ]) const hasBody = req.method != 'HEAD' && req.method != 'GET' - const opts = { headers, method: req.method, body: hasBody ? req : undefined } + const body = req.body ? JSON.stringify(req.body) : req + const opts = { + headers, + method: req.method, + body: hasBody ? body : undefined, + } return fetch(url, opts) } diff --git a/web/pages/api/v0/market/[id].ts b/web/pages/api/v0/market/[id]/index.ts similarity index 93% rename from web/pages/api/v0/market/[id].ts rename to web/pages/api/v0/market/[id]/index.ts index d1d676a3..eb238dab 100644 --- a/web/pages/api/v0/market/[id].ts +++ b/web/pages/api/v0/market/[id]/index.ts @@ -3,7 +3,7 @@ import { listAllBets } from 'web/lib/firebase/bets' import { listAllComments } from 'web/lib/firebase/comments' import { getContractFromId } from 'web/lib/firebase/contracts' import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' -import { FullMarket, ApiError, toFullMarket } from '../_types' +import { FullMarket, ApiError, toFullMarket } from '../../_types' export default async function handler( req: NextApiRequest, diff --git a/web/pages/api/v0/market/[id]/resolve.ts b/web/pages/api/v0/market/[id]/resolve.ts new file mode 100644 index 00000000..1f291288 --- /dev/null +++ b/web/pages/api/v0/market/[id]/resolve.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, 'resolvemarket') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} From 7fc1ec6bd2f8f379277cd747008ecd92c275af11 Mon Sep 17 00:00:00 2001 From: Ben Congdon Date: Thu, 30 Jun 2022 15:13:59 -0700 Subject: [PATCH 017/519] Clear suggested FR answer after submission (#603) --- web/components/answers/create-answer-panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 6eeadf97..ed9012c9 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -58,6 +58,7 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) { setText('') setBetAmount(10) setAmountError(undefined) + setPossibleDuplicateAnswer(undefined) } else setAmountError(result.message) } } From b0b8c6e98bf50c767758a1db395cb82c30de5248 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 30 Jun 2022 15:25:32 -0700 Subject: [PATCH 018/519] Make the resolve API docs not obviously wrong (#604) --- docs/docs/api.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/docs/api.md b/docs/docs/api.md index 9172fc5a..a8ac18fe 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -526,8 +526,8 @@ For binary markets: For free response markets: -- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the Free Response outcome ID. -- `resolutions`: A map of from outcome => number to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. +- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index. +- `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. For numeric markets: @@ -538,33 +538,33 @@ Example request: ``` # Resolve a binary market -$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ - -H 'Authorization: Key {...}' - --data-raw '{"contractId":"{...}", \ - "outcome":"YES"}' +$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": "YES"}' # Resolve a binary market with a specified probability -$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ - -H 'Authorization: Key {...}' - --data-raw '{"contractId":"{...}", \ - "outcome":"MKT", +$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": "MKT", \ "probabilityInt": 75}' # Resolve a free response market with a single answer chosen -$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ - -H 'Authorization: Key {...}' - --data-raw '{"contractId":"{...}", \ - "outcome":"{...}"}' +$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": 2}' # Resolve a free response market with multiple answers chosen -$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ - -H 'Authorization: Key {...}' - --data-raw '{"contractId":"{...}", \ - "outcome":"MKT", - "resolutions": { - "{...}": 1, - "{...}": 2, - }}' +$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": "MKT", \ + "resolutions": [ \ + {"answer": 0, "pct": 50}, \ + {"answer": 2, "pct": 50} \ + ]}' ``` ## Changelog From 3165e421190de8208b42d454f633df2cc7d1913f Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Fri, 1 Jul 2022 07:47:19 -0600 Subject: [PATCH 019/519] Referrals (#592) * add trigger for updated user * Add referral bonuses and notifications for them * Cleanup * Add share group button, cleanup * Cleanup * Add referrals list to user profile * Remove unused * Referral bonus => constant * Refactor * Add referral txn to helper fn * Move reads into firebase transaction * Use effects to write referral info * Flex-wrap profile objects * Small ui changes * Restrict referral user to one update * Remove rogue semicolon * Note about group referral query details * Track referrals, add them to settings list --- common/notification.ts | 3 + common/txn.ts | 11 +- common/user.ts | 5 +- firestore.rules | 7 +- functions/src/create-notification.ts | 81 +++++---- functions/src/index.ts | 1 + functions/src/on-update-user.ts | 107 ++++++++++++ web/components/contract/contract-details.tsx | 8 + .../contract/contract-info-dialog.tsx | 14 +- web/components/groups/edit-group-button.tsx | 2 +- web/components/groups/group-chat.tsx | 11 +- web/components/referrals-button.tsx | 93 ++++++++++ web/components/share-icon-button.tsx | 70 ++++++++ web/components/user-page.tsx | 4 +- web/hooks/use-notifications.ts | 2 +- web/hooks/use-referrals.ts | 12 ++ web/lib/firebase/groups.ts | 16 +- web/lib/firebase/users.ts | 104 +++++++++++ web/pages/[username]/[contractSlug].tsx | 13 +- web/pages/group/[...slugs]/index.tsx | 61 +++++-- web/pages/notifications.tsx | 162 +++++------------- 21 files changed, 602 insertions(+), 185 deletions(-) create mode 100644 functions/src/on-update-user.ts create mode 100644 web/components/referrals-button.tsx create mode 100644 web/components/share-icon-button.tsx create mode 100644 web/hooks/use-referrals.ts diff --git a/common/notification.ts b/common/notification.ts index 919cf917..64a00a36 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -33,6 +33,7 @@ export type notification_source_types = | 'tip' | 'admin_message' | 'group' + | 'user' export type notification_source_update_types = | 'created' @@ -53,3 +54,5 @@ export type notification_reason_types = | 'on_new_follow' | 'you_follow_user' | 'added_you_to_group' + | 'you_referred_user' + | 'user_joined_to_bet_on_your_market' diff --git a/common/txn.ts b/common/txn.ts index 25d4a1c3..0e772e0d 100644 --- a/common/txn.ts +++ b/common/txn.ts @@ -1,6 +1,6 @@ // A txn (pronounced "texan") respresents a payment between two ids on Manifold // Shortened from "transaction" to distinguish from Firebase transactions (and save chars) -type AnyTxnType = Donation | Tip | Manalink +type AnyTxnType = Donation | Tip | Manalink | Referral type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' export type Txn = { @@ -16,7 +16,7 @@ export type Txn = { amount: number token: 'M$' // | 'USD' | MarketOutcome - category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET' + category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET' // Any extra data data?: { [key: string]: any } @@ -46,6 +46,13 @@ type Manalink = { category: 'MANALINK' } +type Referral = { + fromType: 'BANK' + toType: 'USER' + category: 'REFERRAL' +} + export type DonationTxn = Txn & Donation export type TipTxn = Txn & Tip export type ManalinkTxn = Txn & Manalink +export type ReferralTxn = Txn & Referral diff --git a/common/user.ts b/common/user.ts index 298fee56..0a8565dd 100644 --- a/common/user.ts +++ b/common/user.ts @@ -33,11 +33,14 @@ export type User = { followerCountCached: number followedCategories?: string[] + + referredByUserId?: string + referredByContractId?: string } export const STARTING_BALANCE = 1000 export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person - +export const REFERRAL_AMOUNT = 500 export type PrivateUser = { id: string // same as User.id username: string // denormalized from User diff --git a/firestore.rules b/firestore.rules index 176cc71e..50df415a 100644 --- a/firestore.rules +++ b/firestore.rules @@ -20,7 +20,12 @@ service cloud.firestore { allow read; allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); + // only one referral allowed per user + allow update: if resource.data.id == request.auth.uid + && request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['referredByUserId']) + && !("referredByUserId" in resource.data); } match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index daf7e9d7..a32ed3bc 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -68,6 +68,7 @@ export const createNotification = async ( sourceContractCreatorUsername: sourceContract?.creatorUsername, // TODO: move away from sourceContractTitle to sourceTitle sourceContractTitle: sourceContract?.question, + // TODO: move away from sourceContractSlug to sourceSlug sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, @@ -252,44 +253,62 @@ export const createNotification = async ( } } + const notifyUserReceivedReferralBonus = async ( + userToReasonTexts: user_to_reason_texts, + relatedUserId: string + ) => { + if (shouldGetNotification(relatedUserId, userToReasonTexts)) + userToReasonTexts[relatedUserId] = { + // If the referrer is the market creator, just tell them they joined to bet on their market + reason: + sourceContract?.creatorId === relatedUserId + ? 'user_joined_to_bet_on_your_market' + : 'you_referred_user', + } + } + const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. - if (sourceContract) { - if ( - sourceType === 'comment' || - sourceType === 'answer' || - (sourceType === 'contract' && - (sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')) - ) { - if (sourceType === 'comment') { - if (relatedUserId && relatedSourceType) - await notifyRepliedUsers( - userToReasonTexts, - relatedUserId, - relatedSourceType - ) - if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText) - } - await notifyContractCreator(userToReasonTexts, sourceContract) - await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) - await notifyLiquidityProviders(userToReasonTexts, sourceContract) - await notifyBettorsOnContract(userToReasonTexts, sourceContract) - await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) - } else if (sourceType === 'contract' && sourceUpdateType === 'created') { - await notifyUsersFollowers(userToReasonTexts) - } else if (sourceType === 'contract' && sourceUpdateType === 'closed') { - await notifyContractCreator(userToReasonTexts, sourceContract, { - force: true, - }) - } else if (sourceType === 'liquidity' && sourceUpdateType === 'created') { - await notifyContractCreator(userToReasonTexts, sourceContract) - } - } else if (sourceType === 'follow' && relatedUserId) { + if (sourceType === 'follow' && relatedUserId) { await notifyFollowedUser(userToReasonTexts, relatedUserId) } else if (sourceType === 'group' && relatedUserId) { if (sourceUpdateType === 'created') await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) + } else if (sourceType === 'user' && relatedUserId) { + await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) + } + + // The following functions need sourceContract to be defined. + if (!sourceContract) return userToReasonTexts + if ( + sourceType === 'comment' || + sourceType === 'answer' || + (sourceType === 'contract' && + (sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')) + ) { + if (sourceType === 'comment') { + if (relatedUserId && relatedSourceType) + await notifyRepliedUsers( + userToReasonTexts, + relatedUserId, + relatedSourceType + ) + if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText) + } + await notifyContractCreator(userToReasonTexts, sourceContract) + await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) + await notifyLiquidityProviders(userToReasonTexts, sourceContract) + await notifyBettorsOnContract(userToReasonTexts, sourceContract) + await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) + } else if (sourceType === 'contract' && sourceUpdateType === 'created') { + await notifyUsersFollowers(userToReasonTexts) + } else if (sourceType === 'contract' && sourceUpdateType === 'closed') { + await notifyContractCreator(userToReasonTexts, sourceContract, { + force: true, + }) + } else if (sourceType === 'liquidity' && sourceUpdateType === 'created') { + await notifyContractCreator(userToReasonTexts, sourceContract) } return userToReasonTexts } diff --git a/functions/src/index.ts b/functions/src/index.ts index 726aba15..b643ff5e 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -27,6 +27,7 @@ export * from './on-unfollow-user' export * from './on-create-liquidity-provision' export * from './on-update-group' export * from './on-create-group' +export * from './on-update-user' // v2 export * from './health' diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts new file mode 100644 index 00000000..2e5e2145 --- /dev/null +++ b/functions/src/on-update-user.ts @@ -0,0 +1,107 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' +import { REFERRAL_AMOUNT, User } from '../../common/user' +import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' +import { createNotification } from './create-notification' +import { ReferralTxn } from '../../common/txn' +import { Contract } from '../../common/contract' +const firestore = admin.firestore() + +export const onUpdateUser = functions.firestore + .document('users/{userId}') + .onUpdate(async (change, context) => { + const prevUser = change.before.data() as User + const user = change.after.data() as User + const { eventId } = context + + if (prevUser.referredByUserId !== user.referredByUserId) { + await handleUserUpdatedReferral(user, eventId) + } + }) + +async function handleUserUpdatedReferral(user: User, eventId: string) { + // Only create a referral txn if the user has a referredByUserId + if (!user.referredByUserId) { + console.log(`Not set: referredByUserId ${user.referredByUserId}`) + return + } + const referredByUserId = user.referredByUserId + + await firestore.runTransaction(async (transaction) => { + // get user that referred this user + const referredByUserDoc = firestore.doc(`users/${referredByUserId}`) + const referredByUserSnap = await transaction.get(referredByUserDoc) + if (!referredByUserSnap.exists) { + console.log(`User ${referredByUserId} not found`) + return + } + const referredByUser = referredByUserSnap.data() as User + + let referredByContract: Contract | undefined = undefined + if (user.referredByContractId) { + const referredByContractDoc = firestore.doc( + `contracts/${user.referredByContractId}` + ) + referredByContract = await transaction + .get(referredByContractDoc) + .then((snap) => snap.data() as Contract) + } + console.log(`referredByContract: ${referredByContract}`) + + const txns = ( + await firestore + .collection('txns') + .where('toId', '==', referredByUserId) + .where('category', '==', 'REFERRAL') + .get() + ).docs.map((txn) => txn.ref) + const referralTxns = await transaction.getAll(...txns).catch((err) => { + console.error('error getting txns:', err) + throw err + }) + // If the referring user already has a referral txn due to referring this user, halt + if (referralTxns.map((txn) => txn.data()?.description).includes(user.id)) { + console.log('found referral txn with the same details, aborting') + return + } + console.log('creating referral txns') + const fromId = HOUSE_LIQUIDITY_PROVIDER_ID + + // if they're updating their referredId, create a txn for both + const txn: ReferralTxn = { + id: eventId, + createdTime: Date.now(), + fromId, + fromType: 'BANK', + toId: referredByUserId, + toType: 'USER', + amount: REFERRAL_AMOUNT, + token: 'M$', + category: 'REFERRAL', + description: `Referred new user id: ${user.id} for ${REFERRAL_AMOUNT}`, + } + + const txnDoc = await firestore.collection(`txns/`).doc(txn.id) + await transaction.set(txnDoc, txn) + console.log('created referral with txn id:', txn.id) + // We're currently not subtracting M$ from the house, not sure if we want to for accounting purposes. + transaction.update(referredByUserDoc, { + balance: referredByUser.balance + REFERRAL_AMOUNT, + totalDeposits: referredByUser.totalDeposits + REFERRAL_AMOUNT, + }) + + await createNotification( + user.id, + 'user', + 'updated', + user, + eventId, + txn.amount.toString(), + referredByContract, + 'user', + referredByUser.id, + referredByContract?.slug, + referredByContract?.question + ) + }) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 03925a35..3512efa2 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -29,6 +29,8 @@ import { groupPath } from 'web/lib/firebase/groups' import { SiteLink } from 'web/components/site-link' import { DAY_MS } from 'common/util/time' import { useGroupsWithContract } from 'web/hooks/use-group' +import { ShareIconButton } from 'web/components/share-icon-button' +import { useUser } from 'web/hooks/use-user' export type ShowTime = 'resolve-date' | 'close-date' @@ -130,6 +132,7 @@ export function ContractDetails(props: { const { volumeLabel, resolvedDate } = contractMetrics(contract) // Find a group that this contract id is in const groups = useGroupsWithContract(contract.id) + const user = useUser() return ( @@ -192,6 +195,11 @@ export function ContractDetails(props: {
{volumeLabel}
+ {!disabled && }
diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 7027d06a..12fd8dd9 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -13,7 +13,6 @@ import { getBinaryProbPercent, } from 'web/lib/firebase/contracts' import { LiquidityPanel } from '../liquidity-panel' -import { CopyLinkButton } from '../copy-link-button' import { Col } from '../layout/col' import { Modal } from '../layout/modal' import { Row } from '../layout/row' @@ -23,6 +22,9 @@ import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' import { TagsInput } from 'web/components/tags-input' +export const contractDetailsButtonClassName = + 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' + export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { const { contract, bets } = props @@ -48,13 +50,11 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { return ( <> @@ -66,10 +66,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
Share
-
updateOpen(!open)} > diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 5dedbc8f..114a9003 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -91,6 +91,9 @@ export function GroupChat(props: { setReplyToUsername('') inputRef?.focus() } + function focusInput() { + inputRef?.focus() + } return ( @@ -117,7 +120,13 @@ export function GroupChat(props: { ))} {messages.length === 0 && (
- No messages yet. 🦗... Why not say something? + No messages yet. Why not{' '} +
)} diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx new file mode 100644 index 00000000..c23958fc --- /dev/null +++ b/web/components/referrals-button.tsx @@ -0,0 +1,93 @@ +import clsx from 'clsx' +import { User } from 'common/user' +import { useEffect, useState } from 'react' +import { prefetchUsers, useUserById } from 'web/hooks/use-user' +import { Col } from './layout/col' +import { Modal } from './layout/modal' +import { Tabs } from './layout/tabs' +import { TextButton } from './text-button' +import { Row } from 'web/components/layout/row' +import { Avatar } from 'web/components/avatar' +import { UserLink } from 'web/components/user-page' +import { useReferrals } from 'web/hooks/use-referrals' + +export function ReferralsButton(props: { user: User }) { + const { user } = props + const [isOpen, setIsOpen] = useState(false) + const referralIds = useReferrals(user.id) + + return ( + <> + setIsOpen(true)}> + {referralIds?.length ?? ''}{' '} + Referrals + + + + + ) +} + +function ReferralsDialog(props: { + user: User + referralIds: string[] + isOpen: boolean + setIsOpen: (isOpen: boolean) => void +}) { + const { user, referralIds, isOpen, setIsOpen } = props + + useEffect(() => { + prefetchUsers(referralIds) + }, [referralIds]) + + return ( + + +
{user.name}
+
@{user.username}
+ , + }, + ]} + /> + +
+ ) +} + +function ReferralsList(props: { userIds: string[] }) { + const { userIds } = props + + return ( + + {userIds.length === 0 && ( +
No users yet...
+ )} + {userIds.map((userId) => ( + + ))} + + ) +} + +function UserReferralItem(props: { userId: string; className?: string }) { + const { userId, className } = props + const user = useUserById(userId) + + return ( + + + + {user && } + + + ) +} diff --git a/web/components/share-icon-button.tsx b/web/components/share-icon-button.tsx new file mode 100644 index 00000000..507d90c2 --- /dev/null +++ b/web/components/share-icon-button.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react' +import { ShareIcon } from '@heroicons/react/outline' +import clsx from 'clsx' + +import { Contract } from 'common/contract' +import { copyToClipboard } from 'web/lib/util/copy' +import { contractPath } from 'web/lib/firebase/contracts' +import { ENV_CONFIG } from 'common/envs/constants' +import { ToastClipboard } from 'web/components/toast-clipboard' +import { track } from 'web/lib/service/analytics' +import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog' +import { Group } from 'common/group' +import { groupPath } from 'web/lib/firebase/groups' + +function copyContractWithReferral(contract: Contract, username?: string) { + const postFix = + username && contract.creatorUsername !== username + ? '?referrer=' + username + : '' + copyToClipboard( + `https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}` + ) +} + +// Note: if a user arrives at a /group endpoint with a ?referral= query, they'll be added to the group automatically +function copyGroupWithReferral(group: Group, username?: string) { + const postFix = username ? '?referrer=' + username : '' + copyToClipboard( + `https://${ENV_CONFIG.domain}${groupPath(group.slug)}${postFix}` + ) +} + +export function ShareIconButton(props: { + contract?: Contract + group?: Group + buttonClassName?: string + toastClassName?: string + username?: string + children?: React.ReactNode +}) { + const { + contract, + buttonClassName, + toastClassName, + username, + group, + children, + } = props + const [showToast, setShowToast] = useState(false) + + return ( +
+ + + {showToast && } +
+ ) +} diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 246ed2aa..ac9fe8fd 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -36,6 +36,7 @@ import { FollowersButton, FollowingButton } from './following-button' import { useFollows } from 'web/hooks/use-follows' import { FollowButton } from './follow-button' import { PortfolioMetrics } from 'common/user' +import { ReferralsButton } from 'web/components/referrals-button' import { GroupsButton } from 'web/components/groups/groups-button' export function UserLink(props: { @@ -194,10 +195,11 @@ export function UserPage(props: { )} - + + diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index 051d6cbb..c947e8d0 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -117,7 +117,7 @@ function getAppropriateNotifications( return notifications.filter( (n) => n.reason && - // Show all contract notifications + // Show all contract notifications and any that aren't in the above list: (n.sourceType === 'contract' || !lessPriorityReasons.includes(n.reason)) ) if (notificationPreferences === 'none') return [] diff --git a/web/hooks/use-referrals.ts b/web/hooks/use-referrals.ts new file mode 100644 index 00000000..0feba62c --- /dev/null +++ b/web/hooks/use-referrals.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'react' +import { listenForReferrals } from 'web/lib/firebase/users' + +export const useReferrals = (userId: string | null | undefined) => { + const [referralIds, setReferralIds] = useState() + + useEffect(() => { + if (userId) return listenForReferrals(userId, setReferralIds) + }, [userId]) + + return referralIds +} diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 36b05452..506849ad 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -6,7 +6,7 @@ import { updateDoc, where, } from 'firebase/firestore' -import { sortBy } from 'lodash' +import { sortBy, uniq } from 'lodash' import { Group } from 'common/group' import { getContractFromId } from './contracts' import { @@ -95,6 +95,16 @@ export async function getGroupsWithContractId( setGroups(await getValues(q)) } +export async function addUserToGroupViaSlug(groupSlug: string, userId: string) { + // get group to get the member ids + const group = await getGroupBySlug(groupSlug) + if (!group) { + console.error(`Group not found: ${groupSlug}`) + return + } + return await joinGroup(group, userId) +} + export async function joinGroup(group: Group, userId: string): Promise { const { memberIds } = group if (memberIds.includes(userId)) { @@ -102,7 +112,7 @@ export async function joinGroup(group: Group, userId: string): Promise { } const newMemberIds = [...memberIds, userId] const newGroup = { ...group, memberIds: newMemberIds } - await updateGroup(newGroup, { memberIds: newMemberIds }) + await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) return newGroup } export async function leaveGroup(group: Group, userId: string): Promise { @@ -112,6 +122,6 @@ export async function leaveGroup(group: Group, userId: string): Promise { } const newMemberIds = memberIds.filter((id) => id !== userId) const newGroup = { ...group, memberIds: newMemberIds } - await updateGroup(newGroup, { memberIds: newMemberIds }) + await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) return newGroup } diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 40be6741..e72fe141 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -36,6 +36,10 @@ import { feed } from 'common/feed' import { CATEGORY_LIST } from 'common/categories' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' +import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' +import { removeUndefinedProps } from 'common/util/object' +import dayjs from 'dayjs' +import { track } from '@amplitude/analytics-browser' export const users = coll('users') export const privateUsers = coll('private-users') @@ -90,12 +94,92 @@ export function listenForPrivateUser( } const CACHED_USER_KEY = 'CACHED_USER_KEY' +const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' +const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' +const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY' // used to avoid weird race condition let createUserPromise: Promise | undefined = undefined const warmUpCreateUser = throttle(createUser, 5000 /* ms */) +export function writeReferralInfo( + defaultReferrerUsername: string, + contractId?: string, + referralUsername?: string, + groupSlug?: string +) { + const local = safeLocalStorage() + const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) + // Write the first referral username we see. + if (!cachedReferralUser) + local?.setItem( + CACHED_REFERRAL_USERNAME_KEY, + referralUsername || defaultReferrerUsername + ) + + // If an explicit referral query is passed, overwrite the cached referral username. + if (referralUsername) + local?.setItem(CACHED_REFERRAL_USERNAME_KEY, referralUsername) + + // Always write the most recent explicit group invite query value + if (groupSlug) local?.setItem(CACHED_REFERRAL_GROUP_SLUG_KEY, groupSlug) + + // Write the first contract id that we see. + const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY) + if (!cachedReferralContract && contractId) + local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId) +} + +async function setCachedReferralInfoForUser(user: User | null) { + if (!user || user.referredByUserId) return + // if the user wasn't created in the last minute, don't bother + const now = dayjs().utc() + const userCreatedTime = dayjs(user.createdTime) + if (now.diff(userCreatedTime, 'minute') > 1) return + + const local = safeLocalStorage() + const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) + const cachedReferralContractId = local?.getItem( + CACHED_REFERRAL_CONTRACT_ID_KEY + ) + const cachedReferralGroupSlug = local?.getItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + + // get user via username + if (cachedReferralUsername) + getUserByUsername(cachedReferralUsername).then((referredByUser) => { + if (!referredByUser) return + // update user's referralId + updateUser( + user.id, + removeUndefinedProps({ + referredByUserId: referredByUser.id, + referredByContractId: cachedReferralContractId + ? cachedReferralContractId + : undefined, + }) + ) + .catch((err) => { + console.log('error setting referral details', err) + }) + .then(() => { + track('Referral', { + userId: user.id, + referredByUserId: referredByUser.id, + referredByContractId: cachedReferralContractId, + referredByGroupSlug: cachedReferralGroupSlug, + }) + }) + }) + + if (cachedReferralGroupSlug) + addUserToGroupViaSlug(cachedReferralGroupSlug, user.id) + + local?.removeItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + local?.removeItem(CACHED_REFERRAL_USERNAME_KEY) + local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) +} + export function listenForLogin(onUser: (user: User | null) => void) { const local = safeLocalStorage() const cachedUser = local?.getItem(CACHED_USER_KEY) @@ -119,6 +203,7 @@ export function listenForLogin(onUser: (user: User | null) => void) { // Persist to local storage, to reduce login blink next time. // Note: Cap on localStorage size is ~5mb local?.setItem(CACHED_USER_KEY, JSON.stringify(user)) + setCachedReferralInfoForUser(user) } else { // User logged out; reset to null onUser(null) @@ -279,3 +364,22 @@ export function listenForFollowers( } ) } +export function listenForReferrals( + userId: string, + setReferralIds: (referralIds: string[]) => void +) { + const referralsQuery = query( + collection(db, 'users'), + where('referredByUserId', '==', userId) + ) + return onSnapshot( + referralsQuery, + { includeMetadataChanges: true }, + (snapshot) => { + if (snapshot.metadata.fromCache) return + + const values = snapshot.docs.map((doc) => doc.ref.id) + setReferralIds(filterDefined(values)) + } + ) +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 24982b4f..413de725 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -10,7 +10,7 @@ import { useUser } from 'web/hooks/use-user' import { ResolutionPanel } from 'web/components/resolution-panel' import { Title } from 'web/components/title' import { Spacer } from 'web/components/layout/spacer' -import { listUsers, User } from 'web/lib/firebase/users' +import { listUsers, User, writeReferralInfo } from 'web/lib/firebase/users' import { Contract, getContractFromSlug, @@ -42,6 +42,7 @@ import { useBets } from 'web/hooks/use-bets' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' +import { useRouter } from 'next/router' import { useLiquidity } from 'web/hooks/use-liquidity' export const getStaticProps = fromPropz(getStaticPropz) @@ -150,6 +151,16 @@ export function ContractPageContent( const ogCardProps = getOpenGraphProps(contract) + const router = useRouter() + + useEffect(() => { + const { referrer } = router.query as { + referrer?: string + } + if (!user && router.isReady) + writeReferralInfo(contract.creatorUsername, contract.id, referrer) + }, [user, contract, router]) + const rightSidebar = hasSidePanel ? ( {allowTrade && diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a3b99128..3a3db14d 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -13,7 +13,12 @@ import { } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' -import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' +import { + firebaseLogin, + getUser, + User, + writeReferralInfo, +} from 'web/lib/firebase/users' import { Spacer } from 'web/components/layout/spacer' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' @@ -40,6 +45,9 @@ import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' import ShortToggle from 'web/components/widgets/short-toggle' +import { ShareIconButton } from 'web/components/share-icon-button' +import { REFERRAL_AMOUNT } from 'common/user' +import { SiteLink } from 'web/components/site-link' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -150,6 +158,14 @@ export default function GroupPage(props: { }, [group]) const user = useUser() + useEffect(() => { + const { referrer } = router.query as { + referrer?: string + } + if (!user && router.isReady) + writeReferralInfo(creator.username, undefined, referrer, group?.slug) + }, [user, creator, group, router]) + if (group === null || !groupSubpages.includes(page) || slugs[2]) { return } @@ -257,7 +273,13 @@ export default function GroupPage(props: { ) : (
- No questions yet. 🦗... Why not add one? + No questions yet. Why not{' '} + + add one? +
) ) : ( @@ -321,18 +343,17 @@ function GroupOverview(props: { return ( - - About {group.name} - {isCreator && } - - -
Created by
- + +
+
Created by
+ +
+ {isCreator && }
Membership @@ -352,6 +373,20 @@ function GroupOverview(props: { )} + {anyoneCanJoin && user && ( + + Sharing + + + Invite a friend and get M${REFERRAL_AMOUNT} if they sign up! + + + + )} ) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index a3af0a9a..9b0216b6 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -14,9 +14,6 @@ import { Title } from 'web/components/title' import { doc, updateDoc } from 'firebase/firestore' import { db } from 'web/lib/firebase/init' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' -import { Answer } from 'common/answer' -import { Comment } from 'web/lib/firebase/comments' -import { getValue } from 'web/lib/firebase/utils' import Custom404 from 'web/pages/404' import { UserLink } from 'web/components/user-page' import { notification_subscribe_types, PrivateUser } from 'common/user' @@ -38,7 +35,6 @@ import { NotificationGroup, usePreferredGroupedNotifications, } from 'web/hooks/use-notifications' -import { getContractFromId } from 'web/lib/firebase/contracts' import { CheckIcon, XIcon } from '@heroicons/react/outline' import toast from 'react-hot-toast' import { formatMoney } from 'common/util/format' @@ -182,7 +178,7 @@ function NotificationGroupItem(props: { className?: string }) { const { notificationGroup, className } = props - const { sourceContractId, notifications } = notificationGroup + const { notifications } = notificationGroup const { sourceContractTitle, sourceContractSlug, @@ -191,28 +187,6 @@ function NotificationGroupItem(props: { const numSummaryLines = 3 const [expanded, setExpanded] = useState(false) - const [contract, setContract] = useState(undefined) - - useEffect(() => { - if ( - sourceContractTitle && - sourceContractSlug && - sourceContractCreatorUsername - ) - return - if (sourceContractId) { - getContractFromId(sourceContractId) - .then((contract) => { - if (contract) setContract(contract) - }) - .catch((e) => console.log(e)) - } - }, [ - sourceContractCreatorUsername, - sourceContractId, - sourceContractSlug, - sourceContractTitle, - ]) useEffect(() => { setNotificationsAsSeen(notifications) @@ -240,20 +214,20 @@ function NotificationGroupItem(props: { onClick={() => setExpanded(!expanded)} className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} > - {sourceContractTitle || contract ? ( + {sourceContractTitle ? ( {'Activity on '} - {sourceContractTitle || contract?.question} + {sourceContractTitle} ) : ( @@ -306,6 +280,7 @@ function NotificationGroupItem(props: { ) } +// TODO: where should we put referral bonus notifications? function NotificationSettings() { const user = useUser() const [notificationSettings, setNotificationSettings] = @@ -455,6 +430,10 @@ function NotificationSettings() { highlight={notificationSettings !== 'none'} label={"Activity on questions you're betting on"} /> + ('') - const [contract, setContract] = useState(null) - - useEffect(() => { - if ( - !sourceContractId || - (sourceContractSlug && sourceContractCreatorUsername) - ) - return - getContractFromId(sourceContractId) - .then((contract) => { - if (contract) setContract(contract) - }) - .catch((e) => console.log(e)) - }, [ - sourceContractCreatorUsername, - sourceContractId, - sourceContractSlug, - sourceContractTitle, - ]) useEffect(() => { if (sourceText) { setDefaultNotificationText(sourceText) - } else if (!contract || !sourceContractId || !sourceId) return - else if ( - sourceType === 'answer' || - sourceType === 'comment' || - sourceType === 'contract' - ) { - try { - parseOldStyleNotificationText( - sourceId, - sourceContractId, - sourceType, - sourceUpdateType, - setDefaultNotificationText, - contract - ) - } catch (err) { - console.error(err) - } } else if (reasonText) { // Handle arbitrary notifications with reason text here. setDefaultNotificationText(reasonText) } - }, [ - contract, - reasonText, - sourceContractId, - sourceId, - sourceText, - sourceType, - sourceUpdateType, - ]) + }, [reasonText, sourceText]) useEffect(() => { setNotificationsAsSeen([notification]) @@ -596,14 +529,16 @@ function NotificationItem(props: { function getSourceUrl() { if (sourceType === 'follow') return `/${sourceUserUsername}` if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + if ( + sourceContractCreatorUsername && + sourceContractSlug && + sourceType === 'user' + ) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}` if (sourceContractCreatorUsername && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( sourceId ?? '' )}` - if (!contract) return '' - return `/${contract.creatorUsername}/${ - contract.slug - }#${getSourceIdForLinkComponent(sourceId ?? '')}` } function getSourceIdForLinkComponent(sourceId: string) { @@ -619,38 +554,6 @@ function NotificationItem(props: { } } - async function parseOldStyleNotificationText( - sourceId: string, - sourceContractId: string, - sourceType: 'answer' | 'comment' | 'contract', - sourceUpdateType: notification_source_update_types | undefined, - setText: (text: string) => void, - contract: Contract - ) { - if (sourceType === 'contract') { - if ( - isNotificationAboutContractResolution( - sourceType, - sourceUpdateType, - contract - ) && - contract.resolution - ) - setText(contract.resolution) - else setText(contract.question) - } else if (sourceType === 'answer') { - const answer = await getValue( - doc(db, `contracts/${sourceContractId}/answers/`, sourceId) - ) - setText(answer?.text ?? '') - } else { - const comment = await getValue( - doc(db, `contracts/${sourceContractId}/comments/`, sourceId) - ) - setText(comment?.text ?? '') - } - } - if (justSummary) { return ( @@ -669,13 +572,13 @@ function NotificationItem(props: { sourceType, reason, sourceUpdateType, - contract, + undefined, true ).replace(' on', '')}
- {contract?.question || sourceContractTitle || sourceTitle} + {sourceContractTitle || sourceTitle}
)} @@ -752,7 +657,7 @@ function NotificationItem(props: {
@@ -811,6 +716,16 @@ function NotificationTextLabel(props: { ) } + } else if (sourceType === 'user' && sourceText) { + return ( + + As a thank you, we sent you{' '} + + {formatMoney(parseInt(sourceText))} + + ! + + ) } else if (sourceType === 'liquidity' && sourceText) { return ( {formatMoney(parseInt(sourceText))} @@ -829,7 +744,8 @@ function getReasonForShowingNotification( reason: notification_reason_types, sourceUpdateType: notification_source_update_types | undefined, contract: Contract | undefined | null, - simple?: boolean + simple?: boolean, + sourceSlug?: string ) { let reasonText: string switch (source) { @@ -883,6 +799,12 @@ function getReasonForShowingNotification( case 'group': reasonText = 'added you to the group' break + case 'user': + if (sourceSlug && reason === 'user_joined_to_bet_on_your_market') + reasonText = 'joined to bet on your market' + else if (sourceSlug) reasonText = 'joined because you shared' + else reasonText = 'joined because of you' + break default: reasonText = '' } From 5034a43c3ccb2f4b38043524af728bd885947bf6 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Fri, 1 Jul 2022 08:29:12 -0600 Subject: [PATCH 020/519] Filter for ian's deleted users --- functions/src/on-update-user.ts | 20 ++++++++++++-------- web/hooks/use-group.ts | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts index 2e5e2145..b6ba6e0b 100644 --- a/functions/src/on-update-user.ts +++ b/functions/src/on-update-user.ts @@ -55,14 +55,18 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { .where('category', '==', 'REFERRAL') .get() ).docs.map((txn) => txn.ref) - const referralTxns = await transaction.getAll(...txns).catch((err) => { - console.error('error getting txns:', err) - throw err - }) - // If the referring user already has a referral txn due to referring this user, halt - if (referralTxns.map((txn) => txn.data()?.description).includes(user.id)) { - console.log('found referral txn with the same details, aborting') - return + if (txns.length > 0) { + const referralTxns = await transaction.getAll(...txns).catch((err) => { + console.error('error getting txns:', err) + throw err + }) + // If the referring user already has a referral txn due to referring this user, halt + if ( + referralTxns.map((txn) => txn.data()?.description).includes(user.id) + ) { + console.log('found referral txn with the same details, aborting') + return + } } console.log('creating referral txns') const fromId = HOUSE_LIQUIDITY_PROVIDER_ID diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 41f84707..39f6f3f8 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -74,7 +74,9 @@ export function useMembers(group: Group) { } export async function listMembers(group: Group) { - return await Promise.all(group.memberIds.map(getUser)) + return (await Promise.all(group.memberIds.map(getUser))).filter( + (user) => user + ) } export const useGroupsWithContract = (contractId: string | undefined) => { From d29115b05aeefb789e4b76d5735f7c7f812b4c05 Mon Sep 17 00:00:00 2001 From: Ben Congdon Date: Fri, 1 Jul 2022 08:40:43 -0700 Subject: [PATCH 021/519] Nitpick on Manalinks claim page (#608) --- web/pages/link/[slug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 0b0186ed..60966756 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -31,7 +31,7 @@ export default function ClaimPage() { url="/send" />
- + <Title text={`Claim M$${manalink.amount} mana`} /> <ManalinkCard defaultMessage={fromUser?.name || 'Enjoy this mana!'} info={info} From cb68530e2a8af2c50836ab3a04b26bd1797ea2d1 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 1 Jul 2022 12:26:45 -0400 Subject: [PATCH 022/519] Use client side contract search for emulator --- web/components/contract-search.tsx | 2 +- web/components/outcome-label.tsx | 2 +- web/pages/contract-search-firestore.tsx | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index fac02d74..9a4da597 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -122,7 +122,7 @@ export function ContractSearch(props: { const indexName = `${indexPrefix}contracts-${sort}` - if (IS_PRIVATE_MANIFOLD) { + if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { return ( <ContractSearchFirestore querySortOptions={querySortOptions} diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 6daa855b..054ebfd2 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -74,7 +74,7 @@ export function FreeResponseOutcomeLabel(props: { if (resolution === 'CANCEL') return <CancelLabel /> if (resolution === 'MKT') return <MultiLabel /> - const chosen = contract.answers.find((answer) => answer.id === resolution) + const chosen = contract.answers?.find((answer) => answer.id === resolution) if (!chosen) return <AnswerNumberLabel number={resolution} /> return ( <FreeResponseAnswerToolTip text={chosen.text}> diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index c9a7a666..8cd80f7a 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -9,6 +9,8 @@ import { useInitialQueryAndSort, } from 'web/hooks/use-sort-and-query-params' +const MAX_CONTRACTS_RENDERED = 100 + export default function ContractSearchFirestore(props: { querySortOptions?: { defaultSort: Sort @@ -80,6 +82,8 @@ export default function ContractSearchFirestore(props: { } } + matches = matches.slice(0, MAX_CONTRACTS_RENDERED) + const showTime = ['close-date', 'closed'].includes(sort) ? 'close-date' : sort === 'resolve-date' From b9931e65dad9fe8d0d5de921e785a858a8a286b8 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 1 Jul 2022 16:37:30 -0600 Subject: [PATCH 023/519] Allow adding anyone's contract to a group --- firestore.rules | 11 ++- web/components/contract-search.tsx | 49 ++++++++--- web/components/contract/contract-details.tsx | 36 ++++++-- web/components/groups/edit-group-button.tsx | 3 +- web/components/groups/groups-button.tsx | 4 +- web/components/layout/modal.tsx | 11 ++- web/components/layout/tabs.tsx | 14 +-- web/components/nav/sidebar.tsx | 2 +- web/components/user-page.tsx | 2 +- web/hooks/use-group.ts | 4 +- web/lib/firebase/groups.ts | 18 +++- web/pages/create.tsx | 6 +- web/pages/group/[...slugs]/index.tsx | 93 +++++++------------- web/pages/links.tsx | 2 +- web/pages/notifications.tsx | 2 +- 15 files changed, 150 insertions(+), 107 deletions(-) diff --git a/firestore.rules b/firestore.rules index 50df415a..4645343d 100644 --- a/firestore.rules +++ b/firestore.rules @@ -21,11 +21,16 @@ service cloud.firestore { allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); - // only one referral allowed per user allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['referredByUserId']) - && !("referredByUserId" in resource.data); + .hasOnly(['referredByUserId']) + // only one referral allowed per user + && !("referredByUserId" in resource.data) + // user can't refer themselves + && (resource.data.id != request.resource.data.referredByUserId) + // user can't refer someone who referred them quid pro quo + && get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id; + } match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 9a4da597..2c7f5b62 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -9,7 +9,7 @@ import { useSortBy, } from 'react-instantsearch-hooks-web' -import { Contract } from '../../common/contract' +import { Contract } from 'common/contract' import { Sort, useInitialQueryAndSort, @@ -58,15 +58,24 @@ export function ContractSearch(props: { additionalFilter?: { creatorId?: string tag?: string + excludeContractIds?: string[] } showCategorySelector: boolean onContractClick?: (contract: Contract) => void + showPlaceHolder?: boolean + hideOrderSelector?: boolean + overrideGridClassName?: string + hideQuickBet?: boolean }) { const { querySortOptions, additionalFilter, showCategorySelector, onContractClick, + overrideGridClassName, + hideOrderSelector, + showPlaceHolder, + hideQuickBet, } = props const user = useUser() @@ -136,6 +145,7 @@ export function ContractSearch(props: { <Row className="gap-1 sm:gap-2"> <SearchBox className="flex-1" + placeholder={showPlaceHolder ? `Search ${filter} contracts` : ''} classNames={{ form: 'before:top-6', input: '!pl-10 !input !input-bordered shadow-none w-[100px]', @@ -153,13 +163,15 @@ export function ContractSearch(props: { <option value="resolved">Resolved</option> <option value="all">All</option> </select> - <SortBy - items={sortIndexes} - classNames={{ - select: '!select !select-bordered', - }} - onBlur={trackCallback('select search sort')} - /> + {!hideOrderSelector && ( + <SortBy + items={sortIndexes} + classNames={{ + select: '!select !select-bordered', + }} + onBlur={trackCallback('select search sort')} + /> + )} <Configure facetFilters={filters} numericFilters={numericFilters} @@ -187,6 +199,9 @@ export function ContractSearch(props: { <ContractSearchInner querySortOptions={querySortOptions} onContractClick={onContractClick} + overrideGridClassName={overrideGridClassName} + hideQuickBet={hideQuickBet} + excludeContractIds={additionalFilter?.excludeContractIds} /> )} </InstantSearch> @@ -199,8 +214,17 @@ export function ContractSearchInner(props: { shouldLoadFromStorage?: boolean } onContractClick?: (contract: Contract) => void + overrideGridClassName?: string + hideQuickBet?: boolean + excludeContractIds?: string[] }) { - const { querySortOptions, onContractClick } = props + const { + querySortOptions, + onContractClick, + overrideGridClassName, + hideQuickBet, + excludeContractIds, + } = props const { initialQuery } = useInitialQueryAndSort(querySortOptions) const { query, setQuery, setSort } = useUpdateQueryAndSort({ @@ -239,7 +263,7 @@ export function ContractSearchInner(props: { }, []) const { showMore, hits, isLastPage } = useInfiniteHits() - const contracts = hits as any as Contract[] + let contracts = hits as any as Contract[] if (isInitialLoad && contracts.length === 0) return <></> @@ -249,6 +273,9 @@ export function ContractSearchInner(props: { ? 'resolve-date' : undefined + if (excludeContractIds) + contracts = contracts.filter((c) => !excludeContractIds.includes(c.id)) + return ( <ContractsGrid contracts={contracts} @@ -256,6 +283,8 @@ export function ContractSearchInner(props: { hasMore={!isLastPage} showTime={showTime} onContractClick={onContractClick} + overrideGridClassName={overrideGridClassName} + hideQuickBet={hideQuickBet} /> ) } diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 3512efa2..f908918e 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -130,9 +130,32 @@ export function ContractDetails(props: { const { contract, bets, isCreator, disabled } = props const { closeTime, creatorName, creatorUsername, creatorId } = contract const { volumeLabel, resolvedDate } = contractMetrics(contract) - // Find a group that this contract id is in - const groups = useGroupsWithContract(contract.id) + + const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => { + return g2.createdTime - g1.createdTime + }) const user = useUser() + + const groupsUserIsMemberOf = groups + ? groups.filter((g) => g.memberIds.includes(contract.creatorId)) + : [] + const groupsUserIsCreatorOf = groups + ? groups.filter((g) => g.creatorId === contract.creatorId) + : [] + + // Priorities for which group the contract belongs to: + // In order of created most recently + // Group that the contract owner created + // Group the contract owner is a member of + // Any group the contract is in + const groupToDisplay = + groupsUserIsCreatorOf.length > 0 + ? groupsUserIsCreatorOf[0] + : groupsUserIsMemberOf.length > 0 + ? groupsUserIsMemberOf[0] + : groups + ? groups[0] + : undefined return ( <Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500"> <Row className="items-center gap-2"> @@ -153,14 +176,15 @@ export function ContractDetails(props: { )} {!disabled && <UserFollowButton userId={creatorId} small />} </Row> - {/*// TODO: we can add contracts to multiple groups but only show the first it was added to*/} - {groups && groups.length > 0 && ( + {groupToDisplay ? ( <Row className={'line-clamp-1 mt-1 max-w-[200px]'}> - <SiteLink href={`${groupPath(groups[0].slug)}`}> + <SiteLink href={`${groupPath(groupToDisplay.slug)}`}> <UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" /> - <span>{groups[0].name}</span> + <span>{groupToDisplay.name}</span> </SiteLink> </Row> + ) : ( + <div /> )} {(!!closeTime || !!resolvedDate) && ( diff --git a/web/components/groups/edit-group-button.tsx b/web/components/groups/edit-group-button.tsx index 6ad7237a..834af5ec 100644 --- a/web/components/groups/edit-group-button.tsx +++ b/web/components/groups/edit-group-button.tsx @@ -9,6 +9,7 @@ import { useRouter } from 'next/router' import { Modal } from 'web/components/layout/modal' import { FilterSelectUsers } from 'web/components/filter-select-users' import { User } from 'common/user' +import { uniq } from 'lodash' export function EditGroupButton(props: { group: Group; className?: string }) { const { group, className } = props @@ -35,7 +36,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) { await updateGroup(group, { name, about, - memberIds: [...memberIds, ...addMemberUsers.map((user) => user.id)], + memberIds: uniq([...memberIds, ...addMemberUsers.map((user) => user.id)]), }) setIsSubmitting(false) diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index e6ee217d..b81155d1 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -9,7 +9,7 @@ import { TextButton } from 'web/components/text-button' import { Group } from 'common/group' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' -import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' +import { addUserToGroup, leaveGroup } from 'web/lib/firebase/groups' import { firebaseLogin } from 'web/lib/firebase/users' import { GroupLink } from 'web/pages/groups' @@ -93,7 +93,7 @@ export function JoinOrLeaveGroupButton(props: { : false const onJoinGroup = () => { if (!currentUser) return - joinGroup(group, currentUser.id) + addUserToGroup(group, currentUser.id) } const onLeaveGroup = () => { if (!currentUser) return diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index d61a38dd..7a320f24 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -1,13 +1,15 @@ import { Fragment, ReactNode } from 'react' import { Dialog, Transition } from '@headlessui/react' +import clsx from 'clsx' // From https://tailwindui.com/components/application-ui/overlays/modals export function Modal(props: { children: ReactNode open: boolean setOpen: (open: boolean) => void + className?: string }) { - const { children, open, setOpen } = props + const { children, open, setOpen, className } = props return ( <Transition.Root show={open} as={Fragment}> @@ -45,7 +47,12 @@ export function Modal(props: { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - <div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle"> + <div + className={clsx( + 'inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle', + className + )} + > {children} </div> </Transition.Child> diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 69e8cfab..796f5dae 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -14,17 +14,17 @@ type Tab = { export function Tabs(props: { tabs: Tab[] defaultIndex?: number - className?: string + labelClassName?: string onClick?: (tabTitle: string, index: number) => void }) { - const { tabs, defaultIndex, className, onClick } = props + const { tabs, defaultIndex, labelClassName, onClick } = props const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case return ( - <div> + <> <div className="border-b border-gray-200"> - <nav className="-mb-px flex space-x-8" aria-label="Tabs"> + <nav className="-mb-px mb-4 flex space-x-8" aria-label="Tabs"> {tabs.map((tab, i) => ( <Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> <a @@ -42,7 +42,7 @@ export function Tabs(props: { ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', 'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium', - className + labelClassName )} aria-current={activeIndex === i ? 'page' : undefined} > @@ -56,7 +56,7 @@ export function Tabs(props: { </nav> </div> - <div className="mt-4">{activeTab?.content}</div> - </div> + {activeTab?.content} + </> ) } diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 402f5e12..8c3ceb02 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -254,7 +254,7 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) { <div className="mt-1 space-y-0.5"> {memberItems.map((item) => ( <a - key={item.name} + key={item.href} href={item.href} className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900" > diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index ac9fe8fd..ccacca04 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -258,7 +258,7 @@ export function UserPage(props: { {usersContracts !== 'loading' && commentsByContract != 'loading' ? ( <Tabs - className={'pb-2 pt-1 '} + labelClassName={'pb-2 pt-1 '} defaultIndex={ defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0 } diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 39f6f3f8..41f84707 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -74,9 +74,7 @@ export function useMembers(group: Group) { } export async function listMembers(group: Group) { - return (await Promise.all(group.memberIds.map(getUser))).filter( - (user) => user - ) + return await Promise.all(group.memberIds.map(getUser)) } export const useGroupsWithContract = (contractId: string | undefined) => { diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 506849ad..04a5bd44 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -102,10 +102,13 @@ export async function addUserToGroupViaSlug(groupSlug: string, userId: string) { console.error(`Group not found: ${groupSlug}`) return } - return await joinGroup(group, userId) + return await addUserToGroup(group, userId) } -export async function joinGroup(group: Group, userId: string): Promise<Group> { +export async function addUserToGroup( + group: Group, + userId: string +): Promise<Group> { const { memberIds } = group if (memberIds.includes(userId)) { return group @@ -125,3 +128,14 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> { await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) return newGroup } + +export async function addContractToGroup(group: Group, contractId: string) { + return await updateGroup(group, { + contractIds: uniq([...group.contractIds, contractId]), + }) + .then(() => group) + .catch((err) => { + console.error('error adding contract to group', err) + return err + }) +} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index ebbb6f65..7d645b04 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { getGroup, updateGroup } from 'web/lib/firebase/groups' +import { addContractToGroup, getGroup } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -186,9 +186,7 @@ export function NewContract(props: { isFree: false, }) if (result && selectedGroup) { - await updateGroup(selectedGroup, { - contractIds: [...selectedGroup.contractIds, result.id], - }) + await addContractToGroup(selectedGroup, result.id) } await router.push(contractPath(result as Contract)) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3a3db14d..8a8bc4c1 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -4,12 +4,14 @@ import { Group } from 'common/group' import { Page } from 'web/components/page' import { Title } from 'web/components/title' import { listAllBets } from 'web/lib/firebase/bets' -import { Contract, listenForUserContracts } from 'web/lib/firebase/contracts' +import { Contract } from 'web/lib/firebase/contracts' import { groupPath, getGroupBySlug, getGroupContracts, updateGroup, + addContractToGroup, + addUserToGroup, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' @@ -39,7 +41,6 @@ import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' -import { PlusIcon } from '@heroicons/react/outline' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' @@ -48,6 +49,7 @@ import ShortToggle from 'web/components/widgets/short-toggle' import { ShareIconButton } from 'web/components/share-icon-button' import { REFERRAL_AMOUNT } from 'common/user' import { SiteLink } from 'web/components/site-link' +import { ContractSearch } from 'web/components/contract-search' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -509,75 +511,46 @@ function GroupLeaderboards(props: { } function AddContractButton(props: { group: Group; user: User }) { - const { group, user } = props + const { group } = props const [open, setOpen] = useState(false) - const [contracts, setContracts] = useState<Contract[] | undefined>(undefined) - const [query, setQuery] = useState('') - useEffect(() => { - return listenForUserContracts(user.id, (contracts) => { - setContracts(contracts.filter((c) => !group.contractIds.includes(c.id))) - }) - }, [group.contractIds, user.id]) - - async function addContractToGroup(contract: Contract) { - await updateGroup(group, { - ...group, - contractIds: [...group.contractIds, contract.id], - }) + async function addContractToCurrentGroup(contract: Contract) { + await addContractToGroup(group, contract.id) setOpen(false) } - // TODO use find-active-contracts to sort by? - const matches = sortBy(contracts, [ - (contract) => -1 * contract.createdTime, - ]).filter( - (c) => - checkAgainstQuery(query, c.question) || - checkAgainstQuery(query, c.description) || - checkAgainstQuery(query, c.tags.flat().join(' ')) - ) - const debouncedQuery = debounce(setQuery, 50) return ( <> - <Modal open={open} setOpen={setOpen}> - <Col className={'max-h-[60vh] w-full gap-4 rounded-md bg-white p-8'}> + <Modal open={open} setOpen={setOpen} className={'sm:p-0'}> + <Col + className={ + 'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white p-8' + } + > <div className={'text-lg text-indigo-700'}> Add a question to your group </div> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search your questions" - className="input input-bordered mb-4 w-full" - /> - <div className={'overflow-y-scroll'}> - {contracts ? ( - <ContractsGrid - contracts={matches} - loadMore={() => {}} - hasMore={false} - onContractClick={(contract) => { - addContractToGroup(contract) - }} - overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'} - hideQuickBet={true} - /> - ) : ( - <LoadingIndicator /> - )} + <div className={'overflow-y-scroll p-1'}> + <ContractSearch + hideOrderSelector={true} + onContractClick={addContractToCurrentGroup} + showCategorySelector={false} + overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'} + showPlaceHolder={true} + hideQuickBet={true} + additionalFilter={{ excludeContractIds: group.contractIds }} + /> </div> </Col> </Modal> <Row className={'items-center justify-center'}> <button className={ - 'btn btn-sm btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case' + 'btn btn-md btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case' } onClick={() => setOpen(true)} > - <PlusIcon className="mr-1 h-5 w-5" /> - Add old questions to this group + Add an old question </button> </Row> </> @@ -591,17 +564,11 @@ function JoinGroupButton(props: { const { group, user } = props function joinGroup() { if (user && !group.memberIds.includes(user.id)) { - toast.promise( - updateGroup(group, { - ...group, - memberIds: [...group.memberIds, user.id], - }), - { - loading: 'Joining group...', - success: 'Joined group!', - error: "Couldn't join group", - } - ) + toast.promise(addUserToGroup(group, user.id), { + loading: 'Joining group...', + success: 'Joined group!', + error: "Couldn't join group", + }) } } return ( diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 08c99460..12cde274 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -64,7 +64,7 @@ export default function LinkPage() { <Col className="w-full px-8"> <Title text="Manalinks" /> <Tabs - className={'pb-2 pt-1 '} + labelClassName={'pb-2 pt-1 '} defaultIndex={0} tabs={[ { diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 9b0216b6..f3512c56 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -86,7 +86,7 @@ export default function Notifications() { <div className={'p-2 sm:p-4'}> <Title text={'Notifications'} className={'hidden md:block'} /> <Tabs - className={'pb-2 pt-1 '} + labelClassName={'pb-2 pt-1 '} defaultIndex={0} tabs={[ { From 2dce3e15a138bccc38063021003716127fcffa34 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 1 Jul 2022 17:03:26 -0600 Subject: [PATCH 024/519] Correct margin on tabs --- web/components/layout/tabs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 796f5dae..ac1c0fe3 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -23,8 +23,8 @@ export function Tabs(props: { return ( <> - <div className="border-b border-gray-200"> - <nav className="-mb-px mb-4 flex space-x-8" aria-label="Tabs"> + <div className="mb-4 border-b border-gray-200"> + <nav className="-mb-px flex space-x-8" aria-label="Tabs"> {tabs.map((tab, i) => ( <Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> <a From cc52bff05e4202fa5f0e1962cf5bec266226f476 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 1 Jul 2022 16:45:05 -0700 Subject: [PATCH 025/519] fix functions/README formatting --- functions/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/functions/README.md b/functions/README.md index 031cc4fa..8013fb20 100644 --- a/functions/README.md +++ b/functions/README.md @@ -23,8 +23,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started ### For local development 0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI -1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`): 0. `$ brew install java` - 1. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk` +1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`): + + 1. `$ brew install java` + 2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk` 2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud 3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options) 4. `$ mkdir firestore_export` to create a folder to store the exported database From 1a6afaf44fabdef277bb2837d7658554094469f3 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Sat, 2 Jul 2022 14:37:59 -0500 Subject: [PATCH 026/519] Pseudo numeric market (#609) * create pseudo-numeric contracts * graph and bet panel for pseudo numeric * pseudo numeric market layout, quick betting * Estimated value * sell panel * fix graph * pseudo numeric resolution * bets tab * redemption for pseudo numeric markets * create log scale market, validation * log scale * create: initial value can't be min or max * don't allow log scale for ranges with negative values (b/c of problem with graph library) * prettier delenda est * graph: handle min value of zero * bet labeling * validation * prettier * pseudo numeric embeds * update disclaimer * validation * validation --- common/calculate.ts | 31 ++-- common/contract.ts | 22 ++- common/new-bet.ts | 3 +- common/new-contract.ts | 24 ++- common/payouts.ts | 15 +- common/pseudo-numeric.ts | 45 ++++++ functions/src/create-contract.ts | 30 +++- functions/src/emails.ts | 18 ++- functions/src/place-bet.ts | 5 +- functions/src/redeem-shares.ts | 6 +- functions/src/resolve-market.ts | 22 ++- web/components/bet-panel.tsx | 54 +++++-- web/components/bet-row.tsx | 5 +- web/components/bets-list.tsx | 39 ++++- web/components/contract/contract-card.tsx | 58 +++++++- web/components/contract/contract-overview.tsx | 18 ++- .../contract/contract-prob-graph.tsx | 51 +++++-- web/components/contract/quick-bet.tsx | 82 +++++++---- web/components/feed/feed-bets.tsx | 10 +- web/components/numeric-resolution-panel.tsx | 27 +++- web/components/outcome-label.tsx | 25 +++- web/components/sell-button.tsx | 12 +- web/components/sell-modal.tsx | 4 +- web/components/sell-row.tsx | 4 +- web/components/yes-no-selector.tsx | 6 +- web/pages/[username]/[contractSlug].tsx | 9 +- web/pages/create.tsx | 139 +++++++++++++----- web/pages/embed/[username]/[contractSlug].tsx | 15 +- 28 files changed, 623 insertions(+), 156 deletions(-) create mode 100644 common/pseudo-numeric.ts diff --git a/common/calculate.ts b/common/calculate.ts index a0574c10..482a0ccf 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -18,15 +18,24 @@ import { getDpmProbabilityAfterSale, } from './calculate-dpm' import { calculateFixedPayout } from './calculate-fixed-payouts' -import { Contract, BinaryContract, FreeResponseContract } from './contract' +import { + Contract, + BinaryContract, + FreeResponseContract, + PseudoNumericContract, +} from './contract' -export function getProbability(contract: BinaryContract) { +export function getProbability( + contract: BinaryContract | PseudoNumericContract +) { return contract.mechanism === 'cpmm-1' ? getCpmmProbability(contract.pool, contract.p) : getDpmProbability(contract.totalShares) } -export function getInitialProbability(contract: BinaryContract) { +export function getInitialProbability( + contract: BinaryContract | PseudoNumericContract +) { if (contract.initialProbability) return contract.initialProbability if (contract.mechanism === 'dpm-2' || (contract as any).totalShares) @@ -65,7 +74,9 @@ export function calculateShares( } export function calculateSaleAmount(contract: Contract, bet: Bet) { - return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + return contract.mechanism === 'cpmm-1' && + (contract.outcomeType === 'BINARY' || + contract.outcomeType === 'PSEUDO_NUMERIC') ? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue : calculateDpmSaleAmount(contract, bet) } @@ -87,7 +98,9 @@ export function getProbabilityAfterSale( } export function calculatePayout(contract: Contract, bet: Bet, outcome: string) { - return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + return contract.mechanism === 'cpmm-1' && + (contract.outcomeType === 'BINARY' || + contract.outcomeType === 'PSEUDO_NUMERIC') ? calculateFixedPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome) } @@ -96,7 +109,9 @@ export function resolvedPayout(contract: Contract, bet: Bet) { const outcome = contract.resolution if (!outcome) throw new Error('Contract not resolved') - return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY' + return contract.mechanism === 'cpmm-1' && + (contract.outcomeType === 'BINARY' || + contract.outcomeType === 'PSEUDO_NUMERIC') ? calculateFixedPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome) } @@ -142,9 +157,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { const profit = payout + saleValue + redeemed - totalInvested const profitPercent = (profit / totalInvested) * 100 - const hasShares = Object.values(totalShares).some( - (shares) => shares > 0 - ) + const hasShares = Object.values(totalShares).some((shares) => shares > 0) return { invested: Math.max(0, currentInvested), diff --git a/common/contract.ts b/common/contract.ts index dc91a20e..3a90d01f 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -2,9 +2,10 @@ import { Answer } from './answer' import { Fees } from './fees' export type AnyMechanism = DPM | CPMM -export type AnyOutcomeType = Binary | FreeResponse | Numeric +export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric export type AnyContractType = | (CPMM & Binary) + | (CPMM & PseudoNumeric) | (DPM & Binary) | (DPM & FreeResponse) | (DPM & Numeric) @@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { isResolved: boolean resolutionTime?: number // When the contract creator resolved the market resolution?: string - resolutionProbability?: number, + resolutionProbability?: number closeEmailsSent?: number @@ -44,7 +45,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = { collectedFees: Fees } & T -export type BinaryContract = Contract & Binary +export type BinaryContract = Contract & Binary +export type PseudoNumericContract = Contract & PseudoNumeric export type NumericContract = Contract & Numeric export type FreeResponseContract = Contract & FreeResponse export type DPMContract = Contract & DPM @@ -75,6 +77,18 @@ export type Binary = { resolution?: resolution } +export type PseudoNumeric = { + outcomeType: 'PSEUDO_NUMERIC' + min: number + max: number + isLogScale: boolean + resolutionValue?: number + + // same as binary market; map everything to probability + initialProbability: number + resolutionProbability?: number +} + export type FreeResponse = { outcomeType: 'FREE_RESPONSE' answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'. @@ -94,7 +108,7 @@ export type Numeric = { export type outcomeType = AnyOutcomeType['outcomeType'] export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const -export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const +export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const export const MAX_QUESTION_LENGTH = 480 export const MAX_DESCRIPTION_LENGTH = 10000 diff --git a/common/new-bet.ts b/common/new-bet.ts index ba799624..236c0908 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -14,6 +14,7 @@ import { DPMBinaryContract, FreeResponseContract, NumericContract, + PseudoNumericContract, } from './contract' import { noFees } from './fees' import { addObjects } from './util/object' @@ -32,7 +33,7 @@ export type BetInfo = { export const getNewBinaryCpmmBetInfo = ( outcome: 'YES' | 'NO', amount: number, - contract: CPMMBinaryContract, + contract: CPMMBinaryContract | PseudoNumericContract, loanAmount: number ) => { const { shares, newPool, newP, fees } = calculateCpmmPurchase( diff --git a/common/new-contract.ts b/common/new-contract.ts index 0b7d294a..6c89c8c4 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -7,6 +7,7 @@ import { FreeResponse, Numeric, outcomeType, + PseudoNumeric, } from './contract' import { User } from './user' import { parseTags } from './util/parse' @@ -27,7 +28,8 @@ export function getNewContract( // used for numeric markets bucketCount: number, min: number, - max: number + max: number, + isLogScale: boolean ) { const tags = parseTags( `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` @@ -37,6 +39,8 @@ export function getNewContract( const propsByOutcomeType = outcomeType === 'BINARY' ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) + : outcomeType === 'PSEUDO_NUMERIC' + ? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale) : outcomeType === 'NUMERIC' ? getNumericProps(ante, bucketCount, min, max) : getFreeAnswerProps(ante) @@ -111,6 +115,24 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => { return system } +const getPseudoNumericCpmmProps = ( + initialProb: number, + ante: number, + min: number, + max: number, + isLogScale: boolean +) => { + const system: CPMM & PseudoNumeric = { + ...getBinaryCpmmProps(initialProb, ante), + outcomeType: 'PSEUDO_NUMERIC', + min, + max, + isLogScale, + } + + return system +} + const getFreeAnswerProps = (ante: number) => { const system: DPM & FreeResponse = { mechanism: 'dpm-2', diff --git a/common/payouts.ts b/common/payouts.ts index f2c8d271..1469cf4e 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -1,7 +1,12 @@ import { sumBy, groupBy, mapValues } from 'lodash' import { Bet, NumericBet } from './bet' -import { Contract, CPMMBinaryContract, DPMContract } from './contract' +import { + Contract, + CPMMBinaryContract, + DPMContract, + PseudoNumericContract, +} from './contract' import { Fees } from './fees' import { LiquidityProvision } from './liquidity-provision' import { @@ -56,7 +61,11 @@ export const getPayouts = ( }, resolutionProbability?: number ): PayoutInfo => { - if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') { + if ( + contract.mechanism === 'cpmm-1' && + (contract.outcomeType === 'BINARY' || + contract.outcomeType === 'PSEUDO_NUMERIC') + ) { return getFixedPayouts( outcome, contract, @@ -76,7 +85,7 @@ export const getPayouts = ( export const getFixedPayouts = ( outcome: string | undefined, - contract: CPMMBinaryContract, + contract: CPMMBinaryContract | PseudoNumericContract, bets: Bet[], liquidities: LiquidityProvision[], resolutionProbability?: number diff --git a/common/pseudo-numeric.ts b/common/pseudo-numeric.ts new file mode 100644 index 00000000..9a322e35 --- /dev/null +++ b/common/pseudo-numeric.ts @@ -0,0 +1,45 @@ +import { BinaryContract, PseudoNumericContract } from './contract' +import { formatLargeNumber, formatPercent } from './util/format' + +export function formatNumericProbability( + p: number, + contract: PseudoNumericContract +) { + const value = getMappedValue(contract)(p) + return formatLargeNumber(value) +} + +export const getMappedValue = + (contract: PseudoNumericContract | BinaryContract) => (p: number) => { + if (contract.outcomeType === 'BINARY') return p + + const { min, max, isLogScale } = contract + + if (isLogScale) { + const logValue = p * Math.log10(max - min) + return 10 ** logValue + min + } + + return p * (max - min) + min + } + +export const getFormattedMappedValue = + (contract: PseudoNumericContract | BinaryContract) => (p: number) => { + if (contract.outcomeType === 'BINARY') return formatPercent(p) + + const value = getMappedValue(contract)(p) + return formatLargeNumber(value) + } + +export const getPseudoProbability = ( + value: number, + min: number, + max: number, + isLogScale = false +) => { + if (isLogScale) { + return Math.log10(value - min) / Math.log10(max - min) + } + + return (value - min) / (max - min) +} diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index c9468fdc..0d78ab5c 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -28,6 +28,7 @@ import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { User } from '../../common/user' import { Group, MAX_ID_LENGTH } from '../../common/group' +import { getPseudoProbability } from '../../common/pseudo-numeric' const bodySchema = z.object({ question: z.string().min(1).max(MAX_QUESTION_LENGTH), @@ -45,19 +46,31 @@ const binarySchema = z.object({ initialProb: z.number().min(1).max(99), }) +const finite = () => z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER) + const numericSchema = z.object({ - min: z.number(), - max: z.number(), + min: finite(), + max: finite(), + initialValue: finite(), + isLogScale: z.boolean().optional(), }) export const createmarket = newEndpoint({}, async (req, auth) => { const { question, description, tags, closeTime, outcomeType, groupId } = validate(bodySchema, req.body) - let min, max, initialProb - if (outcomeType === 'NUMERIC') { - ;({ min, max } = validate(numericSchema, req.body)) - if (max - min <= 0.01) throw new APIError(400, 'Invalid range.') + let min, max, initialProb, isLogScale + + if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { + let initialValue + ;({ min, max, initialValue, isLogScale } = validate( + numericSchema, + req.body + )) + if (max - min <= 0.01 || initialValue < min || initialValue > max) + throw new APIError(400, 'Invalid range.') + + initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 } if (outcomeType === 'BINARY') { ;({ initialProb } = validate(binarySchema, req.body)) @@ -121,7 +134,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => { tags ?? [], NUMERIC_BUCKET_COUNT, min ?? 0, - max ?? 0 + max ?? 0, + isLogScale ?? false ) if (ante) await chargeUser(user.id, ante, true) @@ -130,7 +144,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { const providerId = user.id - if (outcomeType === 'BINARY') { + if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { const liquidityDoc = firestore .collection(`contracts/${contract.id}/liquidity`) .doc() diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 1ba8ca96..40e8900c 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -6,8 +6,13 @@ import { Comment } from '../../common/comment' import { Contract } from '../../common/contract' import { DPM_CREATOR_FEE } from '../../common/fees' import { PrivateUser, User } from '../../common/user' -import { formatMoney, formatPercent } from '../../common/util/format' +import { + formatLargeNumber, + formatMoney, + formatPercent, +} from '../../common/util/format' import { getValueFromBucket } from '../../common/calculate-dpm' +import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' @@ -101,6 +106,17 @@ const toDisplayResolution = ( return display || resolution } + if (contract.outcomeType === 'PSEUDO_NUMERIC') { + const { resolutionValue } = contract + + return resolutionValue + ? formatLargeNumber(resolutionValue) + : formatNumericProbability( + resolutionProbability ?? getProbability(contract), + contract + ) + } + if (resolution === 'MKT' && resolutions) return 'MULTI' if (resolution === 'CANCEL') return 'N/A' diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 06d27668..b6c7d267 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -70,7 +70,10 @@ export const placebet = newEndpoint({}, async (req, auth) => { if (outcomeType == 'BINARY' && mechanism == 'dpm-2') { const { outcome } = validate(binarySchema, req.body) return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount) - } else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') { + } else if ( + (outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') && + mechanism == 'cpmm-1' + ) { const { outcome } = validate(binarySchema, req.body) return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount) } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') { diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts index bdd3ab94..67922a65 100644 --- a/functions/src/redeem-shares.ts +++ b/functions/src/redeem-shares.ts @@ -16,7 +16,11 @@ export const redeemShares = async (userId: string, contractId: string) => { return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - if (contract.outcomeType !== 'BINARY' || contract.mechanism !== 'cpmm-1') + const { mechanism, outcomeType } = contract + if ( + !(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') || + mechanism !== 'cpmm-1' + ) return { status: 'success' } const betsSnap = await transaction.get( diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index ee78dfec..f8976cb3 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -27,7 +27,7 @@ const bodySchema = z.object({ const binarySchema = z.object({ outcome: z.enum(RESOLUTIONS), - probabilityInt: z.number().gte(0).lt(100).optional(), + probabilityInt: z.number().gte(0).lte(100).optional(), }) const freeResponseSchema = z.union([ @@ -39,7 +39,7 @@ const freeResponseSchema = z.union([ resolutions: z.array( z.object({ answer: z.number().int().nonnegative(), - pct: z.number().gte(0).lt(100), + pct: z.number().gte(0).lte(100), }) ), }), @@ -53,7 +53,19 @@ const numericSchema = z.object({ value: z.number().optional(), }) +const pseudoNumericSchema = z.union([ + z.object({ + outcome: z.literal('CANCEL'), + }), + z.object({ + outcome: z.literal('MKT'), + value: z.number(), + probabilityInt: z.number().gte(0).lte(100), + }), +]) + const opts = { secrets: ['MAILGUN_KEY'] } + export const resolvemarket = newEndpoint(opts, async (req, auth) => { const { contractId } = validate(bodySchema, req.body) const userId = auth.uid @@ -221,12 +233,18 @@ const sendResolutionEmails = async ( function getResolutionParams(contract: Contract, body: string) { const { outcomeType } = contract + if (outcomeType === 'NUMERIC') { return { ...validate(numericSchema, body), resolutions: undefined, probabilityInt: undefined, } + } else if (outcomeType === 'PSEUDO_NUMERIC') { + return { + ...validate(pseudoNumericSchema, body), + resolutions: undefined, + } } else if (outcomeType === 'FREE_RESPONSE') { const freeResponseParams = validate(freeResponseSchema, body) const { outcome } = freeResponseParams diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 73055872..f76117b9 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -3,7 +3,11 @@ import React, { useEffect, useState } from 'react' import { partition, sumBy } from 'lodash' import { useUser } from 'web/hooks/use-user' -import { BinaryContract, CPMMBinaryContract } from 'common/contract' +import { + BinaryContract, + CPMMBinaryContract, + PseudoNumericContract, +} from 'common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' @@ -21,7 +25,7 @@ import { APIError, placeBet } from 'web/lib/firebase/api-call' import { sellShares } from 'web/lib/firebase/api-call' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' -import { BinaryOutcomeLabel } from './outcome-label' +import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' import { calculatePayoutAfterCorrectBet, calculateShares, @@ -35,6 +39,7 @@ import { getCpmmProbability, getCpmmLiquidityFee, } from 'common/calculate-cpmm' +import { getFormattedMappedValue } from 'common/pseudo-numeric' import { SellRow } from './sell-row' import { useSaveShares } from './use-save-shares' import { SignUpPrompt } from './sign-up-prompt' @@ -42,7 +47,7 @@ import { isIOS } from 'web/lib/util/device' import { track } from 'web/lib/service/analytics' export function BetPanel(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract className?: string }) { const { contract, className } = props @@ -81,7 +86,7 @@ export function BetPanel(props: { } export function BetPanelSwitcher(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract className?: string title?: string // Set if BetPanel is on a feed modal selected?: 'YES' | 'NO' @@ -89,7 +94,8 @@ export function BetPanelSwitcher(props: { }) { const { contract, className, title, selected, onBetSuccess } = props - const { mechanism } = contract + const { mechanism, outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) @@ -122,7 +128,12 @@ export function BetPanelSwitcher(props: { <Row className="items-center justify-between gap-2"> <div> You have {formatWithCommas(floorShares)}{' '} - <BinaryOutcomeLabel outcome={sharesOutcome} /> shares + {isPseudoNumeric ? ( + <PseudoNumericOutcomeLabel outcome={sharesOutcome} /> + ) : ( + <BinaryOutcomeLabel outcome={sharesOutcome} /> + )}{' '} + shares </div> {tradeType === 'BUY' && ( @@ -190,12 +201,13 @@ export function BetPanelSwitcher(props: { } function BuyPanel(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract user: User | null | undefined selected?: 'YES' | 'NO' onBuySuccess?: () => void }) { const { contract, user, selected, onBuySuccess } = props + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected) const [betAmount, setBetAmount] = useState<number | undefined>(undefined) @@ -302,6 +314,9 @@ function BuyPanel(props: { : 0) )} ${betChoice ?? 'YES'} shares` : undefined + + const format = getFormattedMappedValue(contract) + return ( <> <YesNoSelector @@ -309,6 +324,7 @@ function BuyPanel(props: { btnClassName="flex-1" selected={betChoice} onSelect={(choice) => onBetChoice(choice)} + isPseudoNumeric={isPseudoNumeric} /> <div className="my-3 text-left text-sm text-gray-500">Amount</div> <BuyAmountInput @@ -323,11 +339,13 @@ function BuyPanel(props: { <Col className="mt-3 w-full gap-3"> <Row className="items-center justify-between text-sm"> - <div className="text-gray-500">Probability</div> + <div className="text-gray-500"> + {isPseudoNumeric ? 'Estimated value' : 'Probability'} + </div> <div> - {formatPercent(initialProb)} + {format(initialProb)} <span className="mx-2">→</span> - {formatPercent(resultProb)} + {format(resultProb)} </div> </Row> @@ -340,6 +358,8 @@ function BuyPanel(props: { <br /> payout if{' '} <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> </> + ) : isPseudoNumeric ? ( + 'Max payout' ) : ( <> Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> @@ -389,7 +409,7 @@ function BuyPanel(props: { } export function SellPanel(props: { - contract: CPMMBinaryContract + contract: CPMMBinaryContract | PseudoNumericContract userBets: Bet[] shares: number sharesOutcome: 'YES' | 'NO' @@ -488,6 +508,10 @@ export function SellPanel(props: { } } + const { outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const format = getFormattedMappedValue(contract) + return ( <> <AmountInput @@ -511,11 +535,13 @@ export function SellPanel(props: { <span className="text-neutral">{formatMoney(saleValue)}</span> </Row> <Row className="items-center justify-between"> - <div className="text-gray-500">Probability</div> + <div className="text-gray-500"> + {isPseudoNumeric ? 'Estimated value' : 'Probability'} + </div> <div> - {formatPercent(initialProb)} + {format(initialProb)} <span className="mx-2">→</span> - {formatPercent(resultProb)} + {format(resultProb)} </div> </Row> </Col> diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index 9621f7a9..ae5e0b00 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx' import { BetPanelSwitcher } from './bet-panel' import { YesNoSelector } from './yes-no-selector' -import { BinaryContract } from 'common/contract' +import { BinaryContract, PseudoNumericContract } from 'common/contract' import { Modal } from './layout/modal' import { SellButton } from './sell-button' import { useUser } from 'web/hooks/use-user' @@ -12,7 +12,7 @@ import { useSaveShares } from './use-save-shares' // Inline version of a bet panel. Opens BetPanel in a new modal. export default function BetRow(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract className?: string btnClassName?: string betPanelClassName?: string @@ -32,6 +32,7 @@ export default function BetRow(props: { return ( <> <YesNoSelector + isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'} className={clsx('justify-end', className)} btnClassName={clsx('btn-sm w-24', btnClassName)} onSelect={(choice) => { diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index f41f89b6..b8fb7d31 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -8,6 +8,7 @@ import { useUserBets } from 'web/hooks/use-user-bets' import { Bet } from 'web/lib/firebase/bets' import { User } from 'web/lib/firebase/users' import { + formatLargeNumber, formatMoney, formatPercent, formatWithCommas, @@ -40,6 +41,7 @@ import { import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render' import { trackLatency } from 'web/lib/firebase/tracking' import { NumericContract } from 'common/contract' +import { formatNumericProbability } from 'common/pseudo-numeric' import { useUser } from 'web/hooks/use-user' import { SellSharesModal } from './sell-modal' @@ -366,6 +368,7 @@ export function BetsSummary(props: { 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 @@ -427,6 +430,25 @@ export function BetsSummary(props: { </div> </Col> </> + ) : isPseudoNumeric ? ( + <> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if {'>='} {formatLargeNumber(contract.max)} + </div> + <div className="whitespace-nowrap"> + {formatMoney(yesWinnings)} + </div> + </Col> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if {'<='} {formatLargeNumber(contract.min)} + </div> + <div className="whitespace-nowrap"> + {formatMoney(noWinnings)} + </div> + </Col> + </> ) : ( <Col> <div className="whitespace-nowrap text-sm text-gray-500"> @@ -507,13 +529,15 @@ export function ContractBetsTable(props: { const { isResolved, mechanism, outcomeType } = contract const isCPMM = mechanism === 'cpmm-1' const isNumeric = outcomeType === 'NUMERIC' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' return ( <div className={clsx('overflow-x-auto', className)}> {amountRedeemed > 0 && ( <> <div className="pl-2 text-sm text-gray-500"> - {amountRedeemed} YES shares and {amountRedeemed} NO shares + {amountRedeemed} {isPseudoNumeric ? 'HIGHER' : 'YES'} shares and{' '} + {amountRedeemed} {isPseudoNumeric ? 'LOWER' : 'NO'} shares automatically redeemed for {formatMoney(amountRedeemed)}. </div> <Spacer h={4} /> @@ -541,7 +565,7 @@ export function ContractBetsTable(props: { )} {!isCPMM && !isResolved && <th>Payout if chosen</th>} <th>Shares</th> - <th>Probability</th> + {!isPseudoNumeric && <th>Probability</th>} <th>Date</th> </tr> </thead> @@ -585,6 +609,7 @@ function BetRow(props: { const isCPMM = mechanism === 'cpmm-1' const isNumeric = outcomeType === 'NUMERIC' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const saleAmount = saleBet?.sale?.amount @@ -628,14 +653,18 @@ function BetRow(props: { truncate="short" /> )} + {isPseudoNumeric && + ' than ' + formatNumericProbability(bet.probAfter, contract)} </td> <td>{formatMoney(Math.abs(amount))}</td> {!isCPMM && !isNumeric && <td>{saleDisplay}</td>} {!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>} <td>{formatWithCommas(Math.abs(shares))}</td> - <td> - {formatPercent(probBefore)} → {formatPercent(probAfter)} - </td> + {!isPseudoNumeric && ( + <td> + {formatPercent(probBefore)} → {formatPercent(probAfter)} + </td> + )} <td>{dayjs(createdTime).format('MMM D, h:mma')}</td> </tr> ) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 87239465..c6cda43c 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -9,6 +9,7 @@ import { BinaryContract, FreeResponseContract, NumericContract, + PseudoNumericContract, } from 'common/contract' import { AnswerLabel, @@ -16,7 +17,11 @@ import { CancelLabel, FreeResponseOutcomeLabel, } from '../outcome-label' -import { getOutcomeProbability, getTopAnswer } from 'common/calculate' +import { + getOutcomeProbability, + getProbability, + getTopAnswer, +} from 'common/calculate' import { AvatarDetails, MiscDetails, ShowTime } from './contract-details' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' import { QuickBet, ProbBar, getColor } from './quick-bet' @@ -24,6 +29,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract' import { useUser } from 'web/hooks/use-user' import { track } from '@amplitude/analytics-browser' import { trackCallback } from 'web/lib/service/analytics' +import { formatNumericProbability } from 'common/pseudo-numeric' export function ContractCard(props: { contract: Contract @@ -131,6 +137,13 @@ export function ContractCard(props: { /> )} + {outcomeType === 'PSEUDO_NUMERIC' && ( + <PseudoNumericResolutionOrExpectation + className="items-center" + contract={contract} + /> + )} + {outcomeType === 'NUMERIC' && ( <NumericResolutionOrExpectation className="items-center" @@ -270,7 +283,9 @@ export function NumericResolutionOrExpectation(props: { {resolution === 'CANCEL' ? ( <CancelLabel /> ) : ( - <div className="text-blue-400">{resolutionValue}</div> + <div className="text-blue-400"> + {formatLargeNumber(resolutionValue)} + </div> )} </> ) : ( @@ -284,3 +299,42 @@ export function NumericResolutionOrExpectation(props: { </Col> ) } + +export function PseudoNumericResolutionOrExpectation(props: { + contract: PseudoNumericContract + className?: string +}) { + const { contract, className } = props + const { resolution, resolutionValue, resolutionProbability } = contract + const textColor = `text-blue-400` + + return ( + <Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}> + {resolution ? ( + <> + <div className={clsx('text-base text-gray-500')}>Resolved</div> + + {resolution === 'CANCEL' ? ( + <CancelLabel /> + ) : ( + <div className="text-blue-400"> + {resolutionValue + ? formatLargeNumber(resolutionValue) + : formatNumericProbability( + resolutionProbability ?? 0, + contract + )} + </div> + )} + </> + ) : ( + <> + <div className={clsx('text-3xl', textColor)}> + {formatNumericProbability(getProbability(contract), contract)} + </div> + <div className={clsx('text-base', textColor)}>expected</div> + </> + )} + </Col> + ) +} diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index a68f37be..897bef04 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -11,6 +11,7 @@ import { FreeResponseResolutionOrChance, BinaryResolutionOrChance, NumericResolutionOrExpectation, + PseudoNumericResolutionOrExpectation, } from './contract-card' import { Bet } from 'common/bet' import BetRow from '../bet-row' @@ -32,6 +33,7 @@ export const ContractOverview = (props: { const user = useUser() const isCreator = user?.id === creatorId const isBinary = outcomeType === 'BINARY' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' return ( <Col className={clsx('mb-6', className)}> @@ -49,6 +51,13 @@ export const ContractOverview = (props: { /> )} + {isPseudoNumeric && ( + <PseudoNumericResolutionOrExpectation + contract={contract} + className="hidden items-end xl:flex" + /> + )} + {outcomeType === 'NUMERIC' && ( <NumericResolutionOrExpectation contract={contract} @@ -61,6 +70,11 @@ export const ContractOverview = (props: { <Row className="items-center justify-between gap-4 xl:hidden"> <BinaryResolutionOrChance contract={contract} /> + {tradingAllowed(contract) && <BetRow contract={contract} />} + </Row> + ) : isPseudoNumeric ? ( + <Row className="items-center justify-between gap-4 xl:hidden"> + <PseudoNumericResolutionOrExpectation contract={contract} /> {tradingAllowed(contract) && <BetRow contract={contract} />} </Row> ) : ( @@ -86,7 +100,9 @@ export const ContractOverview = (props: { /> </Col> <Spacer h={4} /> - {isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '} + {(isBinary || isPseudoNumeric) && ( + <ContractProbGraph contract={contract} bets={bets} /> + )}{' '} {outcomeType === 'FREE_RESPONSE' && ( <AnswersGraph contract={contract} bets={bets} /> )} diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index 7386d748..a9d26e2e 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -5,16 +5,20 @@ import dayjs from 'dayjs' import { memo } from 'react' import { Bet } from 'common/bet' import { getInitialProbability } from 'common/calculate' -import { BinaryContract } from 'common/contract' +import { BinaryContract, PseudoNumericContract } from 'common/contract' import { useWindowSize } from 'web/hooks/use-window-size' +import { getMappedValue } from 'common/pseudo-numeric' +import { formatLargeNumber } from 'common/util/format' export const ContractProbGraph = memo(function ContractProbGraph(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract bets: Bet[] height?: number }) { const { contract, height } = props - const { resolutionTime, closeTime } = contract + const { resolutionTime, closeTime, outcomeType } = contract + const isBinary = outcomeType === 'BINARY' + const isLogScale = outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale const bets = props.bets.filter((bet) => !bet.isAnte && !bet.isRedemption) @@ -24,7 +28,10 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { contract.createdTime, ...bets.map((bet) => bet.createdTime), ].map((time) => new Date(time)) - const probs = [startProb, ...bets.map((bet) => bet.probAfter)] + + const f = getMappedValue(contract) + + const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f) const isClosed = !!closeTime && Date.now() > closeTime const latestTime = dayjs( @@ -39,7 +46,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { times.push(latestTime.toDate()) probs.push(probs[probs.length - 1]) - const yTickValues = [0, 25, 50, 75, 100] + const quartiles = [0, 25, 50, 75, 100] + + const yTickValues = isBinary + ? quartiles + : quartiles.map((x) => x / 100).map(f) const { width } = useWindowSize() @@ -55,9 +66,13 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { const totalPoints = width ? (width > 800 ? 300 : 50) : 1 const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints + const points: { x: Date; y: number }[] = [] + const s = isBinary ? 100 : 1 + const c = isLogScale && contract.min === 0 ? 1 : 0 + for (let i = 0; i < times.length - 1; i++) { - points[points.length] = { x: times[i], y: probs[i] * 100 } + points[points.length] = { x: times[i], y: s * probs[i] + c } const numPoints: number = Math.floor( dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep ) @@ -69,17 +84,23 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { x: dayjs(times[i]) .add(thisTimeStep * n, 'ms') .toDate(), - y: probs[i] * 100, + y: s * probs[i] + c, } } } } - const data = [{ id: 'Yes', data: points, color: '#11b981' }] + const data = [ + { id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' }, + ] const multiYear = !dayjs(startDate).isSame(latestTime, 'year') const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime) + const formatter = isBinary + ? formatPercent + : (x: DatumValue) => formatLargeNumber(+x.valueOf()) + return ( <div className="w-full overflow-visible" @@ -87,12 +108,20 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { > <ResponsiveLine data={data} - yScale={{ min: 0, max: 100, type: 'linear' }} - yFormat={formatPercent} + yScale={ + isBinary + ? { min: 0, max: 100, type: 'linear' } + : { + min: contract.min + c, + max: contract.max + c, + type: contract.isLogScale ? 'log' : 'linear', + } + } + yFormat={formatter} gridYValues={yTickValues} axisLeft={{ tickValues: yTickValues, - format: formatPercent, + format: formatter, }} xScale={{ type: 'time', diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 9ee8b165..adbcc456 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import { getOutcomeProbability, getOutcomeProbabilityAfterBet, + getProbability, getTopAnswer, } from 'common/calculate' import { getExpectedValue } from 'common/calculate-dpm' @@ -25,18 +26,18 @@ import { useSaveShares } from '../use-save-shares' import { sellShares } from 'web/lib/firebase/api-call' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { track } from 'web/lib/service/analytics' +import { formatNumericProbability } from 'common/pseudo-numeric' const BET_SIZE = 10 export function QuickBet(props: { contract: Contract; user: User }) { const { contract, user } = props - const isCpmm = contract.mechanism === 'cpmm-1' + const { mechanism, outcomeType } = contract + const isCpmm = mechanism === 'cpmm-1' const userBets = useUserContractBets(user.id, contract.id) const topAnswer = - contract.outcomeType === 'FREE_RESPONSE' - ? getTopAnswer(contract) - : undefined + outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined // TODO: yes/no from useSaveShares doesn't work on numeric contracts const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( @@ -45,9 +46,9 @@ export function QuickBet(props: { contract: Contract; user: User }) { topAnswer?.number.toString() || undefined ) const hasUpShares = - yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC') + yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC') const hasDownShares = - noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC' + noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC' const [upHover, setUpHover] = useState(false) const [downHover, setDownHover] = useState(false) @@ -130,25 +131,6 @@ export function QuickBet(props: { contract: Contract; user: User }) { }) } - function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') { - if (contract.outcomeType === 'BINARY') { - return direction === 'UP' ? 'YES' : 'NO' - } - if (contract.outcomeType === 'FREE_RESPONSE') { - // TODO: Implement shorting of free response answers - if (direction === 'DOWN') { - throw new Error("Can't bet against free response answers") - } - return getTopAnswer(contract)?.id - } - if (contract.outcomeType === 'NUMERIC') { - // TODO: Ideally an 'UP' bet would be a uniform bet between [current, max] - throw new Error("Can't quick bet on numeric markets") - } - } - - const textColor = `text-${getColor(contract)}` - return ( <Col className={clsx( @@ -173,14 +155,14 @@ export function QuickBet(props: { contract: Contract; user: User }) { <TriangleFillIcon className={clsx( 'mx-auto h-5 w-5', - upHover ? textColor : 'text-gray-400' + upHover ? 'text-green-500' : 'text-gray-400' )} /> ) : ( <TriangleFillIcon className={clsx( 'mx-auto h-5 w-5', - upHover ? textColor : 'text-gray-200' + upHover ? 'text-green-500' : 'text-gray-200' )} /> )} @@ -189,7 +171,7 @@ export function QuickBet(props: { contract: Contract; user: User }) { <QuickOutcomeView contract={contract} previewProb={previewProb} /> {/* Down bet triangle */} - {contract.outcomeType !== 'BINARY' ? ( + {outcomeType !== 'BINARY' && outcomeType !== 'PSEUDO_NUMERIC' ? ( <div> <div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div> <TriangleDownFillIcon @@ -254,6 +236,25 @@ export function ProbBar(props: { contract: Contract; previewProb?: number }) { ) } +function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') { + const { outcomeType } = contract + + if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { + return direction === 'UP' ? 'YES' : 'NO' + } + if (outcomeType === 'FREE_RESPONSE') { + // TODO: Implement shorting of free response answers + if (direction === 'DOWN') { + throw new Error("Can't bet against free response answers") + } + return getTopAnswer(contract)?.id + } + if (outcomeType === 'NUMERIC') { + // TODO: Ideally an 'UP' bet would be a uniform bet between [current, max] + throw new Error("Can't quick bet on numeric markets") + } +} + function QuickOutcomeView(props: { contract: Contract previewProb?: number @@ -261,9 +262,16 @@ function QuickOutcomeView(props: { }) { const { contract, previewProb, caption } = props const { outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + // If there's a preview prob, display that instead of the current prob const override = - previewProb === undefined ? undefined : formatPercent(previewProb) + previewProb === undefined + ? undefined + : isPseudoNumeric + ? formatNumericProbability(previewProb, contract) + : formatPercent(previewProb) + const textColor = `text-${getColor(contract)}` let display: string | undefined @@ -271,6 +279,9 @@ function QuickOutcomeView(props: { case 'BINARY': display = getBinaryProbPercent(contract) break + case 'PSEUDO_NUMERIC': + display = formatNumericProbability(getProbability(contract), contract) + break case 'NUMERIC': display = formatLargeNumber(getExpectedValue(contract)) break @@ -295,11 +306,15 @@ function QuickOutcomeView(props: { // Return a number from 0 to 1 for this contract // Resolved contracts are set to 1, for coloring purposes (even if NO) function getProb(contract: Contract) { - const { outcomeType, resolution } = contract - return resolution + const { outcomeType, resolution, resolutionProbability } = contract + return resolutionProbability + ? resolutionProbability + : resolution ? 1 : outcomeType === 'BINARY' ? getBinaryProb(contract) + : outcomeType === 'PSEUDO_NUMERIC' + ? getProbability(contract) : outcomeType === 'FREE_RESPONSE' ? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '') : outcomeType === 'NUMERIC' @@ -316,7 +331,8 @@ function getNumericScale(contract: NumericContract) { export function getColor(contract: Contract) { // TODO: Try injecting a gradient here // return 'primary' - const { resolution } = contract + const { resolution, outcomeType } = contract + if (resolution) { return ( OUTCOME_TO_COLOR[resolution as resolution] ?? @@ -325,6 +341,8 @@ export function getColor(contract: Contract) { ) } + if (outcomeType === 'PSEUDO_NUMERIC') return 'blue-400' + if ((contract.closeTime ?? Infinity) < Date.now()) { return 'gray-400' } diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index ae22b4b8..2ffdae8e 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -7,13 +7,14 @@ import { Row } from 'web/components/layout/row' import { Avatar, EmptyAvatar } from 'web/components/avatar' import clsx from 'clsx' import { UsersIcon } from '@heroicons/react/solid' -import { formatMoney } from 'common/util/format' +import { formatMoney, formatPercent } from 'common/util/format' import { OutcomeLabel } from 'web/components/outcome-label' import { RelativeTimestamp } from 'web/components/relative-timestamp' import React, { Fragment } from 'react' import { uniqBy, partition, sumBy, groupBy } from 'lodash' import { JoinSpans } from 'web/components/join-spans' import { UserLink } from '../user-page' +import { formatNumericProbability } from 'common/pseudo-numeric' export function FeedBet(props: { contract: Contract @@ -75,6 +76,8 @@ export function BetStatusText(props: { hideOutcome?: boolean }) { const { bet, contract, bettor, isSelf, hideOutcome } = props + const { outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const { amount, outcome, createdTime } = bet const bought = amount >= 0 ? 'bought' : 'sold' @@ -97,7 +100,10 @@ export function BetStatusText(props: { value={(bet as any).value} contract={contract} truncate="short" - /> + />{' '} + {isPseudoNumeric + ? ' than ' + formatNumericProbability(bet.probAfter, contract) + : ' at ' + formatPercent(bet.probAfter)} </> )} <RelativeTimestamp time={createdTime} /> diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index ebac68e5..cf111281 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -6,13 +6,14 @@ import { User } from 'web/lib/firebase/users' import { NumberCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' +import { NumericContract, PseudoNumericContract } from 'common/contract' import { APIError, resolveMarket } from 'web/lib/firebase/api-call' -import { NumericContract } from 'common/contract' import { BucketInput } from './bucket-input' +import { getPseudoProbability } from 'common/pseudo-numeric' export function NumericResolutionPanel(props: { creator: User - contract: NumericContract + contract: NumericContract | PseudoNumericContract className?: string }) { useEffect(() => { @@ -21,6 +22,7 @@ export function NumericResolutionPanel(props: { }, []) const { contract, className } = props + const { min, max, outcomeType } = contract const [outcomeMode, setOutcomeMode] = useState< 'NUMBER' | 'CANCEL' | undefined @@ -32,15 +34,32 @@ export function NumericResolutionPanel(props: { const [error, setError] = useState<string | undefined>(undefined) const resolve = async () => { - const finalOutcome = outcomeMode === 'NUMBER' ? outcome : 'CANCEL' + const finalOutcome = + outcomeMode === 'CANCEL' + ? 'CANCEL' + : outcomeType === 'PSEUDO_NUMERIC' + ? 'MKT' + : 'NUMBER' if (outcomeMode === undefined || finalOutcome === undefined) return setIsSubmitting(true) + const boundedValue = Math.max(Math.min(max, value ?? 0), min) + + const probabilityInt = + 100 * + getPseudoProbability( + boundedValue, + min, + max, + outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale + ) + try { const result = await resolveMarket({ outcome: finalOutcome, value, + probabilityInt, contractId: contract.id, }) console.log('resolved', outcome, 'result:', result) @@ -77,7 +96,7 @@ export function NumericResolutionPanel(props: { {outcomeMode === 'NUMBER' && ( <BucketInput - contract={contract} + contract={contract as any} isSubmitting={isSubmitting} onBucketChange={(v, o) => (setValue(v), setOutcome(o))} /> diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 054ebfd2..a94618e4 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -19,11 +19,15 @@ export function OutcomeLabel(props: { value?: number }) { const { outcome, contract, truncate, value } = props + const { outcomeType } = contract - if (contract.outcomeType === 'BINARY') + if (outcomeType === 'PSEUDO_NUMERIC') + return <PseudoNumericOutcomeLabel outcome={outcome as any} /> + + if (outcomeType === 'BINARY') return <BinaryOutcomeLabel outcome={outcome as any} /> - if (contract.outcomeType === 'NUMERIC') + if (outcomeType === 'NUMERIC') return ( <span className="text-blue-500"> {value ?? getValueFromBucket(outcome, contract)} @@ -49,6 +53,15 @@ export function BinaryOutcomeLabel(props: { outcome: resolution }) { return <CancelLabel /> } +export function PseudoNumericOutcomeLabel(props: { outcome: resolution }) { + const { outcome } = props + + if (outcome === 'YES') return <HigherLabel /> + if (outcome === 'NO') return <LowerLabel /> + if (outcome === 'MKT') return <ProbLabel /> + return <CancelLabel /> +} + export function BinaryContractOutcomeLabel(props: { contract: BinaryContract resolution: resolution @@ -98,6 +111,14 @@ export function YesLabel() { return <span className="text-primary">YES</span> } +export function HigherLabel() { + return <span className="text-primary">HIGHER</span> +} + +export function LowerLabel() { + return <span className="text-red-400">LOWER</span> +} + export function NoLabel() { return <span className="text-red-400">NO</span> } diff --git a/web/components/sell-button.tsx b/web/components/sell-button.tsx index 2b3734a5..51c88442 100644 --- a/web/components/sell-button.tsx +++ b/web/components/sell-button.tsx @@ -1,4 +1,4 @@ -import { BinaryContract } from 'common/contract' +import { BinaryContract, PseudoNumericContract } from 'common/contract' import { User } from 'common/user' import { useUserContractBets } from 'web/hooks/use-user-bets' import { useState } from 'react' @@ -7,7 +7,7 @@ import clsx from 'clsx' import { SellSharesModal } from './sell-modal' export function SellButton(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract user: User | null | undefined sharesOutcome: 'YES' | 'NO' | undefined shares: number @@ -16,7 +16,8 @@ export function SellButton(props: { const { contract, user, sharesOutcome, shares, panelClassName } = props const userBets = useUserContractBets(user?.id, contract.id) const [showSellModal, setShowSellModal] = useState(false) - const { mechanism } = contract + const { mechanism, outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' if (sharesOutcome && user && mechanism === 'cpmm-1') { return ( @@ -32,7 +33,10 @@ export function SellButton(props: { )} onClick={() => setShowSellModal(true)} > - {'Sell ' + sharesOutcome} + Sell{' '} + {isPseudoNumeric + ? { YES: 'HIGH', NO: 'LOW' }[sharesOutcome] + : sharesOutcome} </button> <div className={'mt-1 w-24 text-center text-sm text-gray-500'}> {'(' + Math.floor(shares) + ' shares)'} diff --git a/web/components/sell-modal.tsx b/web/components/sell-modal.tsx index f5a1af67..63cf79b2 100644 --- a/web/components/sell-modal.tsx +++ b/web/components/sell-modal.tsx @@ -1,4 +1,4 @@ -import { CPMMBinaryContract } from 'common/contract' +import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { Bet } from 'common/bet' import { User } from 'common/user' import { Modal } from './layout/modal' @@ -11,7 +11,7 @@ import clsx from 'clsx' export function SellSharesModal(props: { className?: string - contract: CPMMBinaryContract + contract: CPMMBinaryContract | PseudoNumericContract userBets: Bet[] shares: number sharesOutcome: 'YES' | 'NO' diff --git a/web/components/sell-row.tsx b/web/components/sell-row.tsx index 4fe2536f..a8cb2851 100644 --- a/web/components/sell-row.tsx +++ b/web/components/sell-row.tsx @@ -1,4 +1,4 @@ -import { BinaryContract } from 'common/contract' +import { BinaryContract, PseudoNumericContract } from 'common/contract' import { User } from 'common/user' import { useState } from 'react' import { Col } from './layout/col' @@ -10,7 +10,7 @@ import { useSaveShares } from './use-save-shares' import { SellSharesModal } from './sell-modal' export function SellRow(props: { - contract: BinaryContract + contract: BinaryContract | PseudoNumericContract user: User | null | undefined className?: string }) { diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index d040eba9..cac7bf74 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -12,6 +12,7 @@ export function YesNoSelector(props: { btnClassName?: string replaceYesButton?: React.ReactNode replaceNoButton?: React.ReactNode + isPseudoNumeric?: boolean }) { const { selected, @@ -20,6 +21,7 @@ export function YesNoSelector(props: { btnClassName, replaceNoButton, replaceYesButton, + isPseudoNumeric, } = props const commonClassNames = @@ -41,7 +43,7 @@ export function YesNoSelector(props: { )} onClick={() => onSelect('YES')} > - Bet YES + {isPseudoNumeric ? 'HIGHER' : 'Bet YES'} </button> )} {replaceNoButton ? ( @@ -58,7 +60,7 @@ export function YesNoSelector(props: { )} onClick={() => onSelect('NO')} > - Bet NO + {isPseudoNumeric ? 'LOWER' : 'Bet NO'} </button> )} </Row> diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 413de725..2576c2e3 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -144,10 +144,12 @@ export function ContractPageContent( const isCreator = user?.id === creatorId const isBinary = outcomeType === 'BINARY' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isNumeric = outcomeType === 'NUMERIC' const allowTrade = tradingAllowed(contract) const allowResolve = !isResolved && isCreator && !!user - const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve) + const hasSidePanel = + (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) const ogCardProps = getOpenGraphProps(contract) @@ -170,7 +172,7 @@ export function ContractPageContent( <BetPanel className="hidden xl:flex" contract={contract} /> ))} {allowResolve && - (isNumeric ? ( + (isNumeric || isPseudoNumeric ? ( <NumericResolutionPanel creator={user} contract={contract} /> ) : ( <ResolutionPanel creator={user} contract={contract} /> @@ -210,10 +212,11 @@ export function ContractPageContent( )} <ContractOverview contract={contract} bets={bets} /> + {isNumeric && ( <AlertBox title="Warning" - text="Numeric markets were introduced as an experimental feature and are now deprecated." + text="Distributional numeric markets were introduced as an experimental feature and are now deprecated." /> )} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 7d645b04..c7b8f02e 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -85,8 +85,12 @@ export function NewContract(props: { const { creator, question, groupId } = props const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY') const [initialProb] = useState(50) + const [minString, setMinString] = useState('') const [maxString, setMaxString] = useState('') + const [isLogScale, setIsLogScale] = useState(false) + const [initialValueString, setInitialValueString] = useState('') + const [description, setDescription] = useState('') // const [tagText, setTagText] = useState<string>(tag ?? '') // const tags = parseWordsAsTags(tagText) @@ -129,6 +133,18 @@ export function NewContract(props: { const min = minString ? parseFloat(minString) : undefined const max = maxString ? parseFloat(maxString) : undefined + const initialValue = initialValueString + ? parseFloat(initialValueString) + : undefined + + const adjustIsLog = () => { + if (min === undefined || max === undefined) return + const lengthDiff = Math.log10(max - min) + if (lengthDiff > 2) { + setIsLogScale(true) + } + } + // get days from today until the end of this year: const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') @@ -145,13 +161,16 @@ export function NewContract(props: { // closeTime must be in the future closeTime && closeTime > Date.now() && - (outcomeType !== 'NUMERIC' || + (outcomeType !== 'PSEUDO_NUMERIC' || (min !== undefined && max !== undefined && + initialValue !== undefined && isFinite(min) && isFinite(max) && min < max && - max - min > 0.01)) + max - min > 0.01 && + min < initialValue && + initialValue < max)) function setCloseDateInDays(days: number) { const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') @@ -175,6 +194,8 @@ export function NewContract(props: { closeTime, min, max, + initialValue, + isLogScale: (min ?? 0) < 0 ? false : isLogScale, groupId: selectedGroup?.id, tags: category ? [category] : undefined, }) @@ -220,6 +241,7 @@ export function NewContract(props: { choicesMap={{ 'Yes / No': 'BINARY', 'Free response': 'FREE_RESPONSE', + Numeric: 'PSEUDO_NUMERIC', }} isSubmitting={isSubmitting} className={'col-span-4'} @@ -232,38 +254,89 @@ export function NewContract(props: { <Spacer h={6} /> - {outcomeType === 'NUMERIC' && ( - <div className="form-control items-start"> - <label className="label gap-2"> - <span className="mb-1">Range</span> - <InfoTooltip text="The minimum and maximum numbers across the numeric range." /> - </label> + {outcomeType === 'PSEUDO_NUMERIC' && ( + <> + <div className="form-control mb-2 items-start"> + <label className="label gap-2"> + <span className="mb-1">Range</span> + <InfoTooltip text="The minimum and maximum numbers across the numeric range." /> + </label> - <Row className="gap-2"> - <input - type="number" - className="input input-bordered" - placeholder="MIN" - onClick={(e) => e.stopPropagation()} - onChange={(e) => setMinString(e.target.value)} - min={Number.MIN_SAFE_INTEGER} - max={Number.MAX_SAFE_INTEGER} - disabled={isSubmitting} - value={minString ?? ''} - /> - <input - type="number" - className="input input-bordered" - placeholder="MAX" - onClick={(e) => e.stopPropagation()} - onChange={(e) => setMaxString(e.target.value)} - min={Number.MIN_SAFE_INTEGER} - max={Number.MAX_SAFE_INTEGER} - disabled={isSubmitting} - value={maxString} - /> - </Row> - </div> + <Row className="gap-2"> + <input + type="number" + className="input input-bordered" + placeholder="MIN" + onClick={(e) => e.stopPropagation()} + onChange={(e) => setMinString(e.target.value)} + onBlur={adjustIsLog} + min={Number.MIN_SAFE_INTEGER} + max={Number.MAX_SAFE_INTEGER} + disabled={isSubmitting} + value={minString ?? ''} + /> + <input + type="number" + className="input input-bordered" + placeholder="MAX" + onClick={(e) => e.stopPropagation()} + onChange={(e) => setMaxString(e.target.value)} + onBlur={adjustIsLog} + min={Number.MIN_SAFE_INTEGER} + max={Number.MAX_SAFE_INTEGER} + disabled={isSubmitting} + value={maxString} + /> + </Row> + + {!(min !== undefined && min < 0) && ( + <Row className="mt-1 ml-2 mb-2 items-center"> + <span className="mr-2 text-sm">Log scale</span>{' '} + <input + type="checkbox" + checked={isLogScale} + onChange={() => setIsLogScale(!isLogScale)} + disabled={isSubmitting} + /> + </Row> + )} + + {min !== undefined && max !== undefined && min >= max && ( + <div className="mt-2 mb-2 text-sm text-red-500"> + The maximum value must be greater than the minimum. + </div> + )} + </div> + <div className="form-control mb-2 items-start"> + <label className="label gap-2"> + <span className="mb-1">Initial value</span> + <InfoTooltip text="The starting value for this market. Should be in between min and max values." /> + </label> + + <Row className="gap-2"> + <input + type="number" + className="input input-bordered" + placeholder="Initial value" + onClick={(e) => e.stopPropagation()} + onChange={(e) => setInitialValueString(e.target.value)} + max={Number.MAX_SAFE_INTEGER} + disabled={isSubmitting} + value={initialValueString ?? ''} + /> + </Row> + + {initialValue !== undefined && + min !== undefined && + max !== undefined && + min < max && + (initialValue <= min || initialValue >= max) && ( + <div className="mt-2 mb-2 text-sm text-red-500"> + Initial value must be in between {min} and {max}.{' '} + </div> + )} + </div> + </> )} <div className="form-control max-w-[265px] items-start"> diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 98bf37b2..93439be7 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -7,6 +7,7 @@ import { BinaryResolutionOrChance, FreeResponseResolutionOrChance, NumericResolutionOrExpectation, + PseudoNumericResolutionOrExpectation, } from 'web/components/contract/contract-card' import { ContractDetails } from 'web/components/contract/contract-details' import { ContractProbGraph } from 'web/components/contract/contract-prob-graph' @@ -79,6 +80,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const { question, outcomeType } = contract const isBinary = outcomeType === 'BINARY' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const href = `https://${DOMAIN}${contractPath(contract)}` @@ -110,13 +112,18 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { {isBinary && ( <Row className="items-center gap-4"> - {/* this fails typechecking, but it doesn't explode because we will - never */} - <BetRow contract={contract as any} betPanelClassName="scale-75" /> + <BetRow contract={contract} betPanelClassName="scale-75" /> <BinaryResolutionOrChance contract={contract} /> </Row> )} + {isPseudoNumeric && ( + <Row className="items-center gap-4"> + <BetRow contract={contract} betPanelClassName="scale-75" /> + <PseudoNumericResolutionOrExpectation contract={contract} /> + </Row> + )} + {outcomeType === 'FREE_RESPONSE' && ( <FreeResponseResolutionOrChance contract={contract} @@ -133,7 +140,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { </div> <div className="mx-1" style={{ paddingBottom }}> - {isBinary && ( + {(isBinary || isPseudoNumeric) && ( <ContractProbGraph contract={contract} bets={bets} From 218b18254cf33577369a3b2dbb2ee75145789ae2 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 2 Jul 2022 15:46:32 -0400 Subject: [PATCH 027/519] add liquidity: support pseudo numeric markets --- functions/src/add-liquidity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/src/add-liquidity.ts b/functions/src/add-liquidity.ts index 34d3f7c6..eca0a056 100644 --- a/functions/src/add-liquidity.ts +++ b/functions/src/add-liquidity.ts @@ -39,7 +39,8 @@ export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall( const contract = contractSnap.data() as Contract if ( contract.mechanism !== 'cpmm-1' || - contract.outcomeType !== 'BINARY' + (contract.outcomeType !== 'BINARY' && + contract.outcomeType !== 'PSEUDO_NUMERIC') ) return { status: 'error', message: 'Invalid contract' } From 18b87581916ace525dc256a176b12b0d9c96b886 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 2 Jul 2022 13:26:42 -0700 Subject: [PATCH 028/519] Remove code for obsolete feed updater backend jobs (#607) * Remove code for obsolete feed updater backend jobs * Kill two more obsolete guys --- functions/package.json | 1 - functions/src/call-cloud-function.ts | 17 -- functions/src/fetch.ts | 9 - functions/src/keep-awake.ts | 25 --- functions/src/scripts/update-feed.ts | 53 ------ functions/src/update-feed.ts | 220 ------------------------ functions/src/update-recommendations.ts | 70 -------- yarn.lock | 29 +--- 8 files changed, 1 insertion(+), 423 deletions(-) delete mode 100644 functions/src/call-cloud-function.ts delete mode 100644 functions/src/fetch.ts delete mode 100644 functions/src/keep-awake.ts delete mode 100644 functions/src/scripts/update-feed.ts delete mode 100644 functions/src/update-feed.ts delete mode 100644 functions/src/update-recommendations.ts diff --git a/functions/package.json b/functions/package.json index eb6c7151..ed12b4e7 100644 --- a/functions/package.json +++ b/functions/package.json @@ -23,7 +23,6 @@ "main": "functions/src/index.js", "dependencies": { "@amplitude/node": "1.10.0", - "fetch": "1.1.0", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/functions/src/call-cloud-function.ts b/functions/src/call-cloud-function.ts deleted file mode 100644 index 35191343..00000000 --- a/functions/src/call-cloud-function.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as admin from 'firebase-admin' - -import fetch from './fetch' - -export const callCloudFunction = (functionName: string, data: unknown = {}) => { - const projectId = admin.instanceId().app.options.projectId - - const url = `https://us-central1-${projectId}.cloudfunctions.net/${functionName}` - - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data }), - }).then((response) => response.json()) -} diff --git a/functions/src/fetch.ts b/functions/src/fetch.ts deleted file mode 100644 index 1b54dc6c..00000000 --- a/functions/src/fetch.ts +++ /dev/null @@ -1,9 +0,0 @@ -let fetchRequest: typeof fetch - -try { - fetchRequest = fetch -} catch { - fetchRequest = require('node-fetch') -} - -export default fetchRequest diff --git a/functions/src/keep-awake.ts b/functions/src/keep-awake.ts deleted file mode 100644 index 00799e65..00000000 --- a/functions/src/keep-awake.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as functions from 'firebase-functions' - -import { callCloudFunction } from './call-cloud-function' - -export const keepAwake = functions.pubsub - .schedule('every 1 minutes') - .onRun(async () => { - await Promise.all([ - callCloudFunction('placeBet'), - callCloudFunction('resolveMarket'), - callCloudFunction('sellBet'), - ]) - - await sleep(30) - - await Promise.all([ - callCloudFunction('placeBet'), - callCloudFunction('resolveMarket'), - callCloudFunction('sellBet'), - ]) - }) - -const sleep = (seconds: number) => { - return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) -} diff --git a/functions/src/scripts/update-feed.ts b/functions/src/scripts/update-feed.ts deleted file mode 100644 index c5cba142..00000000 --- a/functions/src/scripts/update-feed.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as admin from 'firebase-admin' - -import { initAdmin } from './script-init' -initAdmin() - -import { getValues } from '../utils' -import { User } from '../../../common/user' -import { batchedWaitAll } from '../../../common/util/promise' -import { Contract } from '../../../common/contract' -import { updateWordScores } from '../update-recommendations' -import { computeFeed } from '../update-feed' -import { getFeedContracts, getTaggedContracts } from '../get-feed-data' -import { CATEGORY_LIST } from '../../../common/categories' - -const firestore = admin.firestore() - -async function updateFeed() { - console.log('Updating feed') - - const contracts = await getValues<Contract>(firestore.collection('contracts')) - const feedContracts = await getFeedContracts() - const users = await getValues<User>( - firestore.collection('users').where('username', '==', 'JamesGrugett') - ) - - await batchedWaitAll( - users.map((user) => async () => { - console.log('Updating recs for', user.username) - await updateWordScores(user, contracts) - console.log('Updating feed for', user.username) - await computeFeed(user, feedContracts) - }) - ) - - console.log('Updating feed categories!') - - await batchedWaitAll( - users.map((user) => async () => { - for (const category of CATEGORY_LIST) { - const contracts = await getTaggedContracts(category) - const feed = await computeFeed(user, contracts) - await firestore - .collection(`private-users/${user.id}/cache`) - .doc(`feed-${category}`) - .set({ feed }) - } - }) - ) -} - -if (require.main === module) { - updateFeed().then(() => process.exit()) -} diff --git a/functions/src/update-feed.ts b/functions/src/update-feed.ts deleted file mode 100644 index f19fda92..00000000 --- a/functions/src/update-feed.ts +++ /dev/null @@ -1,220 +0,0 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' -import { flatten, shuffle, sortBy, uniq, zip, zipObject } from 'lodash' - -import { getValue, getValues } from './utils' -import { Contract } from '../../common/contract' -import { logInterpolation } from '../../common/util/math' -import { DAY_MS } from '../../common/util/time' -import { - getProbability, - getOutcomeProbability, - getTopAnswer, -} from '../../common/calculate' -import { User } from '../../common/user' -import { - getContractScore, - MAX_FEED_CONTRACTS, -} from '../../common/recommended-contracts' -import { callCloudFunction } from './call-cloud-function' -import { - getFeedContracts, - getRecentBetsAndComments, - getTaggedContracts, -} from './get-feed-data' -import { CATEGORY_LIST } from '../../common/categories' - -const firestore = admin.firestore() - -const BATCH_SIZE = 30 -const MAX_BATCHES = 50 - -const getUserBatches = async () => { - const users = shuffle(await getValues<User>(firestore.collection('users'))) - const userBatches: User[][] = [] - for (let i = 0; i < users.length; i += BATCH_SIZE) { - userBatches.push(users.slice(i, i + BATCH_SIZE)) - } - - console.log('updating feed batches', MAX_BATCHES, 'of', userBatches.length) - - return userBatches.slice(0, MAX_BATCHES) -} - -export const updateFeed = functions.pubsub - .schedule('every 60 minutes') - .onRun(async () => { - const userBatches = await getUserBatches() - - await Promise.all( - userBatches.map((users) => - callCloudFunction('updateFeedBatch', { users }) - ) - ) - - console.log('updating category feed') - - await Promise.all( - CATEGORY_LIST.map((category) => - callCloudFunction('updateCategoryFeed', { - category, - }) - ) - ) - }) - -export const updateFeedBatch = functions.https.onCall( - async (data: { users: User[] }) => { - const { users } = data - const contracts = await getFeedContracts() - const feeds = await getNewFeeds(users, contracts) - await Promise.all( - zip(users, feeds).map(([user, feed]) => - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - getUserCacheCollection(user!).doc('feed').set({ feed }) - ) - ) - } -) - -export const updateCategoryFeed = functions.https.onCall( - async (data: { category: string }) => { - const { category } = data - const userBatches = await getUserBatches() - - await Promise.all( - userBatches.map(async (users) => { - await callCloudFunction('updateCategoryFeedBatch', { - users, - category, - }) - }) - ) - } -) - -export const updateCategoryFeedBatch = functions.https.onCall( - async (data: { users: User[]; category: string }) => { - const { users, category } = data - const contracts = await getTaggedContracts(category) - const feeds = await getNewFeeds(users, contracts) - await Promise.all( - zip(users, feeds).map(([user, feed]) => - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - getUserCacheCollection(user!).doc(`feed-${category}`).set({ feed }) - ) - ) - } -) - -const getNewFeeds = async (users: User[], contracts: Contract[]) => { - const feeds = await Promise.all(users.map((u) => computeFeed(u, contracts))) - const contractIds = uniq(flatten(feeds).map((c) => c.id)) - const data = await Promise.all(contractIds.map(getRecentBetsAndComments)) - const dataByContractId = zipObject(contractIds, data) - return feeds.map((feed) => - feed.map((contract) => { - return { contract, ...dataByContractId[contract.id] } - }) - ) -} - -const getUserCacheCollection = (user: User) => - firestore.collection(`private-users/${user.id}/cache`) - -export const computeFeed = async (user: User, contracts: Contract[]) => { - const userCacheCollection = getUserCacheCollection(user) - - const [wordScores, lastViewedTime] = await Promise.all([ - getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')), - getValue<{ [contractId: string]: number }>( - userCacheCollection.doc('lastViewTime') - ), - ]).then((dicts) => dicts.map((dict) => dict ?? {})) - - const scoredContracts = contracts.map((contract) => { - const score = scoreContract( - contract, - wordScores, - lastViewedTime[contract.id] - ) - return [contract, score] as [Contract, number] - }) - - const sortedContracts = sortBy( - scoredContracts, - ([_, score]) => score - ).reverse() - - // console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score)) - - return sortedContracts.slice(0, MAX_FEED_CONTRACTS).map(([c]) => c) -} - -function scoreContract( - contract: Contract, - wordScores: { [word: string]: number }, - viewTime: number | undefined -) { - const recommendationScore = getContractScore(contract, wordScores) - const activityScore = getActivityScore(contract, viewTime) - // const lastViewedScore = getLastViewedScore(viewTime) - return recommendationScore * activityScore -} - -function getActivityScore(contract: Contract, viewTime: number | undefined) { - const { createdTime, lastBetTime, lastCommentTime, outcomeType } = contract - const hasNewComments = - lastCommentTime && (!viewTime || lastCommentTime > viewTime) - const newCommentScore = hasNewComments ? 1 : 0.5 - - const timeSinceLastComment = Date.now() - (lastCommentTime ?? createdTime) - const commentDaysAgo = timeSinceLastComment / DAY_MS - const commentTimeScore = - 0.25 + 0.75 * (1 - logInterpolation(0, 3, commentDaysAgo)) - - const timeSinceLastBet = Date.now() - (lastBetTime ?? createdTime) - const betDaysAgo = timeSinceLastBet / DAY_MS - const betTimeScore = 0.5 + 0.5 * (1 - logInterpolation(0, 3, betDaysAgo)) - - let prob = 0.5 - if (outcomeType === 'BINARY') { - prob = getProbability(contract) - } else if (outcomeType === 'FREE_RESPONSE') { - const topAnswer = getTopAnswer(contract) - if (topAnswer) - prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id)) - } - const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25 - const probScore = 0.5 + frac * 0.5 - - const { volume24Hours, volume7Days } = contract - const combinedVolume = Math.log(volume24Hours + 1) + Math.log(volume7Days + 1) - const volumeScore = 0.5 + 0.5 * logInterpolation(4, 20, combinedVolume) - - const score = - newCommentScore * commentTimeScore * betTimeScore * probScore * volumeScore - - // Map score to [0.5, 1] since no recent activty is not a deal breaker. - const mappedScore = 0.5 + 0.5 * score - const newMappedScore = 0.7 + 0.3 * score - - const isNew = Date.now() < contract.createdTime + DAY_MS - return isNew ? newMappedScore : mappedScore -} - -// function getLastViewedScore(viewTime: number | undefined) { -// if (viewTime === undefined) { -// return 1 -// } - -// const daysAgo = (Date.now() - viewTime) / DAY_MS - -// if (daysAgo < 0.5) { -// const frac = logInterpolation(0, 0.5, daysAgo) -// return 0.5 + 0.25 * frac -// } - -// const frac = logInterpolation(0.5, 14, daysAgo) -// return 0.75 + 0.25 * frac -// } diff --git a/functions/src/update-recommendations.ts b/functions/src/update-recommendations.ts deleted file mode 100644 index bc82291c..00000000 --- a/functions/src/update-recommendations.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' - -import { getValue, getValues } from './utils' -import { Contract } from '../../common/contract' -import { Bet } from '../../common/bet' -import { User } from '../../common/user' -import { ClickEvent } from '../../common/tracking' -import { getWordScores } from '../../common/recommended-contracts' -import { batchedWaitAll } from '../../common/util/promise' -import { callCloudFunction } from './call-cloud-function' - -const firestore = admin.firestore() - -export const updateRecommendations = functions.pubsub - .schedule('every 24 hours') - .onRun(async () => { - const users = await getValues<User>(firestore.collection('users')) - - const batchSize = 100 - const userBatches: User[][] = [] - for (let i = 0; i < users.length; i += batchSize) { - userBatches.push(users.slice(i, i + batchSize)) - } - - await Promise.all( - userBatches.map((batch) => - callCloudFunction('updateRecommendationsBatch', { users: batch }) - ) - ) - }) - -export const updateRecommendationsBatch = functions.https.onCall( - async (data: { users: User[] }) => { - const { users } = data - - const contracts = await getValues<Contract>( - firestore.collection('contracts') - ) - - await batchedWaitAll( - users.map((user) => () => updateWordScores(user, contracts)) - ) - } -) - -export const updateWordScores = async (user: User, contracts: Contract[]) => { - const [bets, viewCounts, clicks] = await Promise.all([ - getValues<Bet>( - firestore.collectionGroup('bets').where('userId', '==', user.id) - ), - - getValue<{ [contractId: string]: number }>( - firestore.doc(`private-users/${user.id}/cache/viewCounts`) - ), - - getValues<ClickEvent>( - firestore - .collection(`private-users/${user.id}/events`) - .where('type', '==', 'click') - ), - ]) - - const wordScores = getWordScores(contracts, viewCounts ?? {}, clicks, bets) - - const cachedCollection = firestore.collection( - `private-users/${user.id}/cache` - ) - await cachedCollection.doc('wordScores').set(wordScores) -} diff --git a/yarn.lock b/yarn.lock index 15cd3c51..c07d548f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3875,13 +3875,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -biskviit@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7" - integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w== - dependencies: - psl "^1.1.7" - bluebird@^3.7.1: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -5237,13 +5230,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= - dependencies: - iconv-lite "~0.4.13" - end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -5817,14 +5803,6 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4: node-domexception "^1.0.0" web-streams-polyfill "^3.0.3" -fetch@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e" - integrity sha1-CoJ58Gvjf58Ou1Z1YKMKSA2lmi4= - dependencies: - biskviit "1.0.1" - encoding "0.1.12" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -6782,7 +6760,7 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24, iconv-lite@~0.4.13: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -9151,11 +9129,6 @@ pseudomap@^1.0.1: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.7: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" From 90d7f55c6d7b80cb6395276b157f93fd4451e99c Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 2 Jul 2022 13:27:06 -0700 Subject: [PATCH 029/519] Fix backup DB job to actually backup most things, refactor (#605) * Make backup manually invokable and thereby testable * Add a shitload of missing stuff to our backups * Also backup follows as per James --- functions/src/backup-db.ts | 91 +++++++++++++++++----------- functions/src/scripts/backup-db.ts | 16 +++++ functions/src/scripts/script-init.ts | 19 +++--- 3 files changed, 81 insertions(+), 45 deletions(-) create mode 100644 functions/src/scripts/backup-db.ts diff --git a/functions/src/backup-db.ts b/functions/src/backup-db.ts index 5174f595..227c89e4 100644 --- a/functions/src/backup-db.ts +++ b/functions/src/backup-db.ts @@ -18,46 +18,63 @@ import * as functions from 'firebase-functions' import * as firestore from '@google-cloud/firestore' -const client = new firestore.v1.FirestoreAdminClient() +import { FirestoreAdminClient } from '@google-cloud/firestore/types/v1/firestore_admin_client' -const bucket = 'gs://manifold-firestore-backup' +export const backupDbCore = async ( + client: FirestoreAdminClient, + project: string, + bucket: string +) => { + const name = client.databasePath(project, '(default)') + const outputUriPrefix = `gs://${bucket}` + // Leave collectionIds empty to export all collections + // or set to a list of collection IDs to export, + // collectionIds: ['users', 'posts'] + // NOTE: Subcollections are not backed up by default + const collectionIds = [ + 'contracts', + 'groups', + 'private-users', + 'stripe-transactions', + 'transactions', + 'users', + 'bets', + 'comments', + 'follows', + 'followers', + 'answers', + 'txns', + 'manalinks', + 'liquidity', + 'stats', + 'cache', + 'latency', + 'views', + 'notifications', + 'portfolioHistory', + 'folds', + ] + return await client.exportDocuments({ name, outputUriPrefix, collectionIds }) +} export const backupDb = functions.pubsub .schedule('every 24 hours') - .onRun((_context) => { - const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT - if (projectId == null) { - throw new Error('No project ID environment variable set.') + .onRun(async (_context) => { + try { + const client = new firestore.v1.FirestoreAdminClient() + const project = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT + if (project == null) { + throw new Error('No project ID environment variable set.') + } + const responses = await backupDbCore( + client, + project, + 'manifold-firestore-backup' + ) + const response = responses[0] + console.log(`Operation Name: ${response['name']}`) + } catch (err) { + console.error(err) + throw new Error('Export operation failed') } - const databaseName = client.databasePath(projectId, '(default)') - - return client - .exportDocuments({ - name: databaseName, - outputUriPrefix: bucket, - // Leave collectionIds empty to export all collections - // or set to a list of collection IDs to export, - // collectionIds: ['users', 'posts'] - // NOTE: Subcollections are not backed up by default - collectionIds: [ - 'contracts', - 'groups', - 'private-users', - 'stripe-transactions', - 'users', - 'bets', - 'comments', - 'followers', - 'answers', - 'txns', - ], - }) - .then((responses) => { - const response = responses[0] - console.log(`Operation Name: ${response['name']}`) - }) - .catch((err) => { - console.error(err) - throw new Error('Export operation failed') - }) }) diff --git a/functions/src/scripts/backup-db.ts b/functions/src/scripts/backup-db.ts new file mode 100644 index 00000000..04c66438 --- /dev/null +++ b/functions/src/scripts/backup-db.ts @@ -0,0 +1,16 @@ +import * as firestore from '@google-cloud/firestore' +import { getServiceAccountCredentials } from './script-init' +import { backupDbCore } from '../backup-db' + +async function backupDb() { + const credentials = getServiceAccountCredentials() + const projectId = credentials.project_id + const client = new firestore.v1.FirestoreAdminClient({ credentials }) + const bucket = 'manifold-firestore-backup' + const resp = await backupDbCore(client, projectId, bucket) + console.log(`Operation: ${resp[0]['name']}`) +} + +if (require.main === module) { + backupDb().then(() => process.exit()) +} diff --git a/functions/src/scripts/script-init.ts b/functions/src/scripts/script-init.ts index 8f65e4be..cc17a620 100644 --- a/functions/src/scripts/script-init.ts +++ b/functions/src/scripts/script-init.ts @@ -47,26 +47,29 @@ const getFirebaseActiveProject = (cwd: string) => { } } -export const initAdmin = (env?: string) => { +export const getServiceAccountCredentials = (env?: string) => { env = env || getFirebaseActiveProject(process.cwd()) if (env == null) { - console.error( + throw new Error( "Couldn't find active Firebase project; did you do `firebase use <alias>?`" ) - return } const envVar = `GOOGLE_APPLICATION_CREDENTIALS_${env.toUpperCase()}` const keyPath = process.env[envVar] if (keyPath == null) { - console.error( + throw new Error( `Please set the ${envVar} environment variable to contain the path to your ${env} environment key file.` ) - return } - console.log(`Initializing connection to ${env} Firebase...`) /* eslint-disable-next-line @typescript-eslint/no-var-requires */ - const serviceAccount = require(keyPath) - admin.initializeApp({ + return require(keyPath) +} + +export const initAdmin = (env?: string) => { + const serviceAccount = getServiceAccountCredentials(env) + console.log(`Initializing connection to ${serviceAccount.project_id}...`) + return admin.initializeApp({ + projectId: serviceAccount.project_id, credential: admin.credential.cert(serviceAccount), }) } From 7dea9cbfa89336bcb2672800e4ac3418ac807280 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 2 Jul 2022 16:24:03 -0700 Subject: [PATCH 030/519] Use `getAll` Firestore technology to improve some code (#612) --- functions/src/place-bet.ts | 5 +---- functions/src/sell-bet.ts | 10 +++++----- functions/src/sell-shares.ts | 5 ++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index b6c7d267..43906f3c 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -41,10 +41,7 @@ export const placebet = newEndpoint({}, async (req, auth) => { log('Inside main transaction.') const contractDoc = firestore.doc(`contracts/${contractId}`) const userDoc = firestore.doc(`users/${auth.uid}`) - const [contractSnap, userSnap] = await Promise.all([ - trans.get(contractDoc), - trans.get(userDoc), - ]) + const [contractSnap, userSnap] = await trans.getAll(contractDoc, userDoc) if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') if (!userSnap.exists) throw new APIError(400, 'User not found.') log('Loaded user and contract snapshots.') diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index b3362159..18df4536 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -21,11 +21,11 @@ export const sellbet = newEndpoint({}, async (req, auth) => { const contractDoc = firestore.doc(`contracts/${contractId}`) const userDoc = firestore.doc(`users/${auth.uid}`) const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) - const [contractSnap, userSnap, betSnap] = await Promise.all([ - transaction.get(contractDoc), - transaction.get(userDoc), - transaction.get(betDoc), - ]) + const [contractSnap, userSnap, betSnap] = await transaction.getAll( + contractDoc, + userDoc, + betDoc + ) if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') if (!userSnap.exists) throw new APIError(400, 'User not found.') if (!betSnap.exists) throw new APIError(400, 'Bet not found.') diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 26374a16..a0c19f2c 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -24,9 +24,8 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const contractDoc = firestore.doc(`contracts/${contractId}`) const userDoc = firestore.doc(`users/${auth.uid}`) const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid) - const [contractSnap, userSnap, userBets] = await Promise.all([ - transaction.get(contractDoc), - transaction.get(userDoc), + const [[contractSnap, userSnap], userBets] = await Promise.all([ + transaction.getAll(contractDoc, userDoc), getValues<Bet>(betsQ), // TODO: why is this not in the transaction?? ]) if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') From 960f8a1b3d20e2c6a2829bbd97fb5ba56dad64e0 Mon Sep 17 00:00:00 2001 From: Pico2x <pico2x@gmail.com> Date: Sun, 3 Jul 2022 20:18:12 +0100 Subject: [PATCH 031/519] Toggle weekly leaderboard and daily/weekly/alltime portfolio graph (#616) * Toggle weekly leaderboard and daily/weekly/alltime portfolio graph * Formatmoney for tooltip value --- .../portfolio/portfolio-value-graph.tsx | 3 ++- .../portfolio/portfolio-value-section.tsx | 15 +++++++++++---- web/components/user-page.tsx | 11 +++++++---- web/pages/leaderboards.tsx | 4 +++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index 558fc5f6..50a6b59a 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -52,7 +52,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { margin={{ top: 20, right: 28, bottom: 22, left: 60 }} xScale={{ type: 'time', - min: points[0].x, + min: points[0]?.x, max: endDate, }} yScale={{ @@ -77,6 +77,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { enableGridY={true} enableSlices="x" animate={false} + yFormat={(value) => formatMoney(+value)} ></ResponsiveLine> </div> ) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index a992e87e..55260bb5 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -13,7 +13,7 @@ export const PortfolioValueSection = memo( }) { const { portfolioHistory } = props const lastPortfolioMetrics = last(portfolioHistory) - const [portfolioPeriod] = useState<Period>('allTime') + const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime') if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { return <div> No portfolio history data yet </div> @@ -33,9 +33,16 @@ export const PortfolioValueSection = memo( </div> </Col> </div> - { - //TODO: enable day/week/monthly as data becomes available - } + <select + className="select select-bordered self-start" + onChange={(e) => { + setPortfolioPeriod(e.target.value as Period) + }} + > + <option value="allTime">All time</option> + <option value="weekly">Weekly</option> + <option value="daily">Daily</option> + </select> </Row> <PortfolioValueGraph portfolioHistory={portfolioHistory} diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index ccacca04..d72a2a16 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,6 +38,7 @@ import { FollowButton } from './follow-button' import { PortfolioMetrics } from 'common/user' import { ReferralsButton } from 'web/components/referrals-button' import { GroupsButton } from 'web/components/groups/groups-button' +import { PortfolioValueSection } from './portfolio/portfolio-value-section' export function UserLink(props: { name: string @@ -75,7 +76,9 @@ export function UserPage(props: { 'loading' ) const [usersBets, setUsersBets] = useState<Bet[] | 'loading'>('loading') - const [, setUsersPortfolioHistory] = useState<PortfolioMetrics[]>([]) + const [portfolioHistory, setUsersPortfolioHistory] = useState< + PortfolioMetrics[] + >([]) const [commentsByContract, setCommentsByContract] = useState< Map<Contract, Comment[]> | 'loading' >('loading') @@ -297,9 +300,9 @@ export function UserPage(props: { title: 'Bets', content: ( <div> - { - // TODO: add portfolio-value-section here - } + <PortfolioValueSection + portfolioHistory={portfolioHistory} + /> <BetsList user={user} hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022} diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 44c0a65b..f306493b 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -67,7 +67,9 @@ export default function Leaderboards(props: { <Col className="mx-4 items-center gap-10 lg:flex-row"> {!isLoading ? ( <> - {period === 'allTime' || period === 'daily' ? ( //TODO: show other periods once they're available + {period === 'allTime' || + period == 'weekly' || + period === 'daily' ? ( //TODO: show other periods once they're available <Leaderboard title="🏅 Top bettors" users={topTradersState} From 8fdc44f7f3bd2eaa28bd3448b98c421b02abeda3 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 3 Jul 2022 15:37:22 -0400 Subject: [PATCH 032/519] Switch to firebase dev before serving firebase emulators --- functions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/package.json b/functions/package.json index ed12b4e7..ee7bc92d 100644 --- a/functions/package.json +++ b/functions/package.json @@ -12,7 +12,7 @@ "start": "yarn shell", "deploy": "firebase deploy --only functions", "logs": "firebase functions:log", - "serve": "yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", + "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export", "db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", From 9839b7b5a40fd349802145eb932f98b8864dbd3a Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 3 Jul 2022 16:45:52 -0700 Subject: [PATCH 033/519] Allow customizing starting balance & antes --- common/antes.ts | 7 ++----- common/user.ts | 7 +++++-- web/pages/create.tsx | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/antes.ts b/common/antes.ts index becc9b7e..d4cb2ff9 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -10,12 +10,9 @@ import { import { User } from './user' import { LiquidityProvision } from './liquidity-provision' import { noFees } from './fees' +import { ENV_CONFIG } from './envs/constants' -export const FIXED_ANTE = 100 - -// deprecated -export const PHANTOM_ANTE = 0.001 -export const MINIMUM_ANTE = 50 +export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100 export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id diff --git a/common/user.ts b/common/user.ts index 0a8565dd..d5dd0373 100644 --- a/common/user.ts +++ b/common/user.ts @@ -1,3 +1,5 @@ +import { ENV_CONFIG } from './envs/constants' + export type User = { id: string createdTime: number @@ -38,8 +40,9 @@ export type User = { referredByContractId?: string } -export const STARTING_BALANCE = 1000 -export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person +export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000 +// for sus users, i.e. multiple sign ups for same person +export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10 export const REFERRAL_AMOUNT = 500 export type PrivateUser = { id: string // same as User.id diff --git a/web/pages/create.tsx b/web/pages/create.tsx index c7b8f02e..6a5f96ae 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -7,7 +7,7 @@ import { Spacer } from 'web/components/layout/spacer' import { useUser } from 'web/hooks/use-user' import { Contract, contractPath } from 'web/lib/firebase/contracts' import { createMarket } from 'web/lib/firebase/api-call' -import { FIXED_ANTE, MINIMUM_ANTE } from 'common/antes' +import { FIXED_ANTE } from 'common/antes' import { InfoTooltip } from 'web/components/info-tooltip' import { Page } from 'web/components/page' import { Row } from 'web/components/layout/row' @@ -156,7 +156,6 @@ export function NewContract(props: { question.length > 0 && ante !== undefined && ante !== null && - ante >= MINIMUM_ANTE && ante <= balance && // closeTime must be in the future closeTime && From 579dcd81dc5e2b8b26b3442ad928be72036b6a41 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Fri, 1 Jul 2022 11:01:36 -0700 Subject: [PATCH 034/519] Update env config template --- common/envs/prod.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/envs/prod.ts b/common/envs/prod.ts index f5a0e55e..f8aaf4cc 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -18,13 +18,17 @@ export type EnvConfig = { faviconPath?: string // Should be a file in /public navbarLogoPath?: string newQuestionPlaceholders: string[] + + // Currency controls + fixedAnte?: number + startingBalance?: number } type FirebaseConfig = { apiKey: string authDomain: string projectId: string - region: string + region?: string storageBucket: string messagingSenderId: string appId: string From d78bbcb3df761eca0e42362577bbb1ebcc16a80a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 3 Jul 2022 23:43:18 -0400 Subject: [PATCH 035/519] fix navbar tracking --- web/components/nav/nav-bar.tsx | 4 +++- web/components/nav/sidebar.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 5a997b46..9f0f8ddd 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -63,6 +63,7 @@ export function BottomNavBar() { currentPage={currentPage} item={{ name: formatMoney(user.balance), + trackingEventName: 'profile', href: `/${user.username}?tab=bets`, icon: () => ( <Avatar @@ -94,6 +95,7 @@ export function BottomNavBar() { function NavBarItem(props: { item: Item; currentPage: string }) { const { item, currentPage } = props + const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`) return ( <Link href={item.href}> @@ -102,7 +104,7 @@ function NavBarItem(props: { item: Item; currentPage: string }) { 'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700', currentPage === item.href && 'bg-gray-200 text-indigo-700' )} - onClick={trackCallback('navbar: ' + item.name)} + onClick={track} > {item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />} {item.name} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 8c3ceb02..5ce9e239 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -120,6 +120,7 @@ function getMoreMobileNav() { export type Item = { name: string + trackingEventName?: string href: string icon?: React.ComponentType<{ className?: string }> } From e712ad82891e2d14d685442ae7689c7d51fea4e6 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 4 Jul 2022 07:49:41 -0600 Subject: [PATCH 036/519] Allow users to choose who referred them (#611) * Allow users to choose who referred them * Refactor * Rewording * Match list styles * Match empty text styles --- firestore.rules | 19 ++- web/components/filter-select-users.tsx | 194 +++++++++++++++---------- web/components/referrals-button.tsx | 96 +++++++++++- web/components/user-page.tsx | 2 +- web/pages/account.tsx | 41 ------ web/pages/link/[slug].tsx | 2 +- 6 files changed, 221 insertions(+), 133 deletions(-) delete mode 100644 web/pages/account.tsx diff --git a/firestore.rules b/firestore.rules index 4645343d..28ff4485 100644 --- a/firestore.rules +++ b/firestore.rules @@ -21,16 +21,15 @@ service cloud.firestore { allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); - allow update: if resource.data.id == request.auth.uid - && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['referredByUserId']) - // only one referral allowed per user - && !("referredByUserId" in resource.data) - // user can't refer themselves - && (resource.data.id != request.resource.data.referredByUserId) - // user can't refer someone who referred them quid pro quo - && get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id; - + allow update: if resource.data.id == request.auth.uid + && request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['referredByUserId']) + // only one referral allowed per user + && !("referredByUserId" in resource.data) + // user can't refer themselves + && !(resource.data.id == request.resource.data.referredByUserId); + // quid pro quos enabled (only once though so nbd) - bc I can't make this work: + // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); } match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { diff --git a/web/components/filter-select-users.tsx b/web/components/filter-select-users.tsx index 93badf20..8d2dbbae 100644 --- a/web/components/filter-select-users.tsx +++ b/web/components/filter-select-users.tsx @@ -1,4 +1,4 @@ -import { UserIcon } from '@heroicons/react/outline' +import { UserIcon, XIcon } from '@heroicons/react/outline' import { useUsers } from 'web/hooks/use-users' import { User } from 'common/user' import { Fragment, useMemo, useState } from 'react' @@ -6,13 +6,24 @@ import clsx from 'clsx' import { Menu, Transition } from '@headlessui/react' import { Avatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' +import { UserLink } from 'web/components/user-page' export function FilterSelectUsers(props: { setSelectedUsers: (users: User[]) => void selectedUsers: User[] ignoreUserIds: string[] + showSelectedUsersTitle?: boolean + selectedUsersClassName?: string + maxUsers?: number }) { - const { ignoreUserIds, selectedUsers, setSelectedUsers } = props + const { + ignoreUserIds, + selectedUsers, + setSelectedUsers, + showSelectedUsersTitle, + selectedUsersClassName, + maxUsers, + } = props const users = useUsers() const [query, setQuery] = useState('') const [filteredUsers, setFilteredUsers] = useState<User[]>([]) @@ -29,89 +40,118 @@ export function FilterSelectUsers(props: { }) ) }, [beginQuerying, users, selectedUsers, ignoreUserIds, query]) - + const shouldShow = maxUsers ? selectedUsers.length < maxUsers : true return ( <div> - <div className="relative mt-1 rounded-md"> - <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> - <UserIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> - </div> - <input - type="text" - name="user name" - id="user name" - value={query} - onChange={(e) => setQuery(e.target.value)} - className="input input-bordered block w-full pl-10 focus:border-gray-300 " - placeholder="Austin Chen" - /> - </div> - <Menu - as="div" - className={clsx( - 'relative inline-block w-full overflow-y-scroll text-right', - beginQuerying && 'h-36' - )} - > - {({}) => ( - <Transition - show={beginQuerying} - as={Fragment} - enter="transition ease-out duration-100" - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leave="transition ease-in duration-75" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" + {shouldShow && ( + <> + <div className="relative mt-1 rounded-md"> + <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> + <UserIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> + </div> + <input + type="text" + name="user name" + id="user name" + value={query} + onChange={(e) => setQuery(e.target.value)} + className="input input-bordered block w-full pl-10 focus:border-gray-300 " + placeholder="Austin Chen" + /> + </div> + <Menu + as="div" + className={clsx( + 'relative inline-block w-full overflow-y-scroll text-right', + beginQuerying && 'h-36' + )} > - <Menu.Items - static={true} - className="absolute right-0 mt-2 w-full origin-top-right cursor-pointer divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" - > - <div className="py-1"> - {filteredUsers.map((user: User) => ( - <Menu.Item key={user.id}> - {({ active }) => ( - <span - className={clsx( - active - ? 'bg-gray-100 text-gray-900' - : 'text-gray-700', - 'group flex items-center px-4 py-2 text-sm' + {({}) => ( + <Transition + show={beginQuerying} + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items + static={true} + className="absolute right-0 mt-2 w-full origin-top-right cursor-pointer divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" + > + <div className="py-1"> + {filteredUsers.map((user: User) => ( + <Menu.Item key={user.id}> + {({ active }) => ( + <span + className={clsx( + active + ? 'bg-gray-100 text-gray-900' + : 'text-gray-700', + 'group flex items-center px-4 py-2 text-sm' + )} + onClick={() => { + setQuery('') + setSelectedUsers([...selectedUsers, user]) + }} + > + <Avatar + username={user.username} + avatarUrl={user.avatarUrl} + size={'xs'} + className={'mr-2'} + /> + {user.name} + </span> )} - onClick={() => { - setQuery('') - setSelectedUsers([...selectedUsers, user]) - }} - > - <Avatar - username={user.username} - avatarUrl={user.avatarUrl} - size={'xs'} - className={'mr-2'} - /> - {user.name} - </span> - )} - </Menu.Item> - ))} - </div> - </Menu.Items> - </Transition> - )} - </Menu> + </Menu.Item> + ))} + </div> + </Menu.Items> + </Transition> + )} + </Menu> + </> + )} {selectedUsers.length > 0 && ( <> - <div className={'mb-2'}>Added members:</div> - <Row className="mt-0 grid grid-cols-6 gap-2"> + <div className={'mb-2'}> + {showSelectedUsersTitle && 'Added members:'} + </div> + <Row + className={clsx( + 'mt-0 grid grid-cols-6 gap-2', + selectedUsersClassName + )} + > {selectedUsers.map((user: User) => ( - <div key={user.id} className="col-span-2 flex items-center"> - <Avatar - username={user.username} - avatarUrl={user.avatarUrl} - size={'sm'} + <div + key={user.id} + className="col-span-2 flex flex-row items-center justify-between" + > + <Row className={'items-center'}> + <Avatar + username={user.username} + avatarUrl={user.avatarUrl} + size={'sm'} + /> + <UserLink + username={user.username} + className="ml-2" + name={user.name} + /> + </Row> + <XIcon + onClick={() => + setSelectedUsers([ + ...selectedUsers.filter((u) => u.id != user.id), + ]) + } + className=" h-5 w-5 cursor-pointer text-gray-400" + aria-hidden="true" /> - <span className="ml-2">{user.name}</span> </div> ))} </Row> diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index c23958fc..bb9e53cb 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -10,9 +10,11 @@ import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { useReferrals } from 'web/hooks/use-referrals' +import { FilterSelectUsers } from 'web/components/filter-select-users' +import { getUser, updateUser } from 'web/lib/firebase/users' -export function ReferralsButton(props: { user: User }) { - const { user } = props +export function ReferralsButton(props: { user: User; currentUser?: User }) { + const { user, currentUser } = props const [isOpen, setIsOpen] = useState(false) const referralIds = useReferrals(user.id) @@ -28,6 +30,7 @@ export function ReferralsButton(props: { user: User }) { referralIds={referralIds ?? []} isOpen={isOpen} setIsOpen={setIsOpen} + currentUser={currentUser} /> </> ) @@ -38,8 +41,26 @@ function ReferralsDialog(props: { referralIds: string[] isOpen: boolean setIsOpen: (isOpen: boolean) => void + currentUser?: User }) { - const { user, referralIds, isOpen, setIsOpen } = props + const { user, referralIds, isOpen, setIsOpen, currentUser } = props + const [referredBy, setReferredBy] = useState<User[]>([]) + const [isSubmitting, setIsSubmitting] = useState(false) + const [errorText, setErrorText] = useState('') + + const [referredByUser, setReferredByUser] = useState<User | null>() + useEffect(() => { + if ( + isOpen && + !referredByUser && + currentUser?.referredByUserId && + currentUser.id === user.id + ) { + getUser(currentUser.referredByUserId).then((user) => { + setReferredByUser(user) + }) + } + }, [currentUser, isOpen, referredByUser, user.id]) useEffect(() => { prefetchUsers(referralIds) @@ -56,6 +77,75 @@ function ReferralsDialog(props: { title: 'Referrals', content: <ReferralsList userIds={referralIds} />, }, + { + title: 'Referred by', + content: ( + <> + {user.id === currentUser?.id && !referredByUser ? ( + <> + <FilterSelectUsers + setSelectedUsers={setReferredBy} + selectedUsers={referredBy} + ignoreUserIds={[currentUser.id]} + showSelectedUsersTitle={false} + selectedUsersClassName={'grid-cols-2 '} + maxUsers={1} + /> + <Row className={'mt-0 justify-end'}> + <button + className={ + referredBy.length === 0 + ? 'hidden' + : 'btn btn-primary btn-md my-2 w-24 normal-case' + } + disabled={referredBy.length === 0 || isSubmitting} + onClick={() => { + setIsSubmitting(true) + updateUser(currentUser.id, { + referredByUserId: referredBy[0].id, + }) + .then(async () => { + setErrorText('') + setIsSubmitting(false) + setReferredBy([]) + setIsOpen(false) + }) + .catch((error) => { + setIsSubmitting(false) + setErrorText(error.message) + }) + }} + > + Save + </button> + </Row> + <span className={'text-warning'}> + {referredBy.length > 0 && + 'Careful: you can only set who referred you once!'} + </span> + <span className={'text-error'}>{errorText}</span> + </> + ) : ( + <div className="justify-center text-gray-700"> + {referredByUser ? ( + <Row className={'items-center gap-2 p-2'}> + <Avatar + username={referredByUser.username} + avatarUrl={referredByUser.avatarUrl} + /> + <UserLink + username={referredByUser.username} + name={referredByUser.name} + /> + </Row> + ) : ( + <span className={'text-gray-500'}>No one...</span> + )} + </div> + )} + </> + ), + }, ]} /> </Col> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index d72a2a16..0a1366c4 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -202,7 +202,7 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - <ReferralsButton user={user} /> + <ReferralsButton user={user} currentUser={currentUser} /> <GroupsButton user={user} /> </Row> diff --git a/web/pages/account.tsx b/web/pages/account.tsx deleted file mode 100644 index 59d938c3..00000000 --- a/web/pages/account.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import { Page } from 'web/components/page' -import { UserPage } from 'web/components/user-page' -import { useUser } from 'web/hooks/use-user' -import { firebaseLogin } from 'web/lib/firebase/users' - -function SignInCard() { - return ( - <div className="card glass sm:card-side text-neutral-content mx-4 my-12 max-w-sm bg-green-600 shadow-xl transition-all hover:bg-green-600 hover:shadow-xl sm:mx-auto"> - <div className="p-4"> - <img - src="/logo-bg-white.png" - className="h-20 w-20 rounded-lg shadow-lg" - /> - </div> - <div className="card-body max-w-md"> - <h2 className="card-title font-major-mono">Welcome!</h2> - <p>Sign in to get started</p> - <div className="card-actions"> - <button - className="btn glass rounded-full hover:bg-green-500" - onClick={firebaseLogin} - > - Sign in with Google - </button> - </div> - </div> - </div> - ) -} - -export default function Account() { - const user = useUser() - return user ? ( - <UserPage user={user} currentUser={user} /> - ) : ( - <Page> - <SignInCard /> - </Page> - ) -} diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 60966756..eed68e1a 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -46,7 +46,7 @@ export default function ClaimPage() { if (result.data.status == 'error') { throw new Error(result.data.message) } - router.push('/account?claimed-mana=yes') + user && router.push(`/${user.username}?claimed-mana=yes`) } catch (e) { console.log(e) const message = From 22f917e250cdb02de9846e19bbbe022449aa7a82 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 4 Jul 2022 08:32:51 -0600 Subject: [PATCH 037/519] Avatar sizes to 24, size 20 is broken --- web/components/user-page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 0a1366c4..07f722d7 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -159,7 +159,7 @@ export function UserPage(props: { <Avatar username={user.username} avatarUrl={user.avatarUrl} - size={20} + size={24} className="bg-white ring-4 ring-white" /> </div> From 790fdad1e3efa07bc214dab7d8fcedd4727e7088 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 4 Jul 2022 09:18:01 -0600 Subject: [PATCH 038/519] Display refered by publicly --- web/components/referrals-button.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index bb9e53cb..74fc113d 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -50,17 +50,12 @@ function ReferralsDialog(props: { const [referredByUser, setReferredByUser] = useState<User | null>() useEffect(() => { - if ( - isOpen && - !referredByUser && - currentUser?.referredByUserId && - currentUser.id === user.id - ) { - getUser(currentUser.referredByUserId).then((user) => { + if (isOpen && !referredByUser && user?.referredByUserId) { + getUser(user.referredByUserId).then((user) => { setReferredByUser(user) }) } - }, [currentUser, isOpen, referredByUser, user.id]) + }, [isOpen, referredByUser, user.referredByUserId]) useEffect(() => { prefetchUsers(referralIds) From af2b148b3415e863e1bcc4fc7a60a0e6be95b7a4 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Mon, 4 Jul 2022 13:25:44 -0700 Subject: [PATCH 039/519] show names on admin user table --- web/pages/admin.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/pages/admin.tsx b/web/pages/admin.tsx index db24996d..e709e875 100644 --- a/web/pages/admin.tsx +++ b/web/pages/admin.tsx @@ -62,13 +62,19 @@ function UsersTable() { class="hover:underline hover:decoration-indigo-400 hover:decoration-2" href="/${cell}">@${cell}</a>`), }, + { + id: 'name', + name: 'Name', + formatter: (cell) => + html(`<span class="whitespace-nowrap">${cell}</span>`), + }, { id: 'email', name: 'Email', }, { id: 'createdTime', - name: 'Created Time', + name: 'Created', formatter: (cell) => html( `<span class="whitespace-nowrap">${dayjs(cell as number).format( From c39e3aedfa024416d2135cc9915bd5bfee956fbf Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 4 Jul 2022 16:04:05 -0700 Subject: [PATCH 040/519] Also send .env file when deploy functions --- functions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/package.json b/functions/package.json index ee7bc92d..93bea621 100644 --- a/functions/package.json +++ b/functions/package.json @@ -5,7 +5,7 @@ "firestore": "dev-mantic-markets.appspot.com" }, "scripts": { - "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist", + "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env dist", "compile": "tsc -b", "watch": "tsc -w", "shell": "yarn build && firebase functions:shell", From 53b4a2894453a8e5a411e3bc29d4fefea03e14ee Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 4 Jul 2022 16:21:59 -0700 Subject: [PATCH 041/519] Check in .env to git --- functions/.env | 3 +++ functions/.gitignore | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 functions/.env diff --git a/functions/.env b/functions/.env new file mode 100644 index 00000000..0c4303df --- /dev/null +++ b/functions/.env @@ -0,0 +1,3 @@ +# This sets which EnvConfig is deployed to Firebase Cloud Functions + +NEXT_PUBLIC_FIREBASE_ENV=PROD diff --git a/functions/.gitignore b/functions/.gitignore index 2aeae30c..58f30dcb 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -1,5 +1,4 @@ # Secrets -.env* .runtimeconfig.json # GCP deployment artifact From b26648c1cec22af62b43a319e8f1fabb2cb1fc12 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 5 Jul 2022 11:29:26 -0600 Subject: [PATCH 042/519] Daily trading bonuses (#618) * first commit, WIP * Give trading bonuses & paginate notifications * Move read & update into transaction * Move request bonus logic to notifs icon --- common/notification.ts | 2 + common/numeric-constants.ts | 1 + common/txn.ts | 11 +- common/user.ts | 1 + functions/src/create-notification.ts | 15 + functions/src/get-daily-bonuses.ts | 139 ++++++++ functions/src/index.ts | 1 + web/components/nav/sidebar.tsx | 1 - web/components/notifications-icon.tsx | 20 +- web/hooks/use-notifications.ts | 39 ++- web/lib/firebase/api-call.ts | 4 + web/pages/notifications.tsx | 448 ++++++++++++++++++-------- 12 files changed, 525 insertions(+), 157 deletions(-) create mode 100644 functions/src/get-daily-bonuses.ts diff --git a/common/notification.ts b/common/notification.ts index 64a00a36..e90624a4 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -34,6 +34,7 @@ export type notification_source_types = | 'admin_message' | 'group' | 'user' + | 'bonus' export type notification_source_update_types = | 'created' @@ -56,3 +57,4 @@ export type notification_reason_types = | 'added_you_to_group' | 'you_referred_user' | 'user_joined_to_bet_on_your_market' + | 'unique_bettors_on_your_contract' diff --git a/common/numeric-constants.ts b/common/numeric-constants.ts index ef364b74..46885668 100644 --- a/common/numeric-constants.ts +++ b/common/numeric-constants.ts @@ -3,3 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005 export const NUMERIC_GRAPH_COLOR = '#5fa5f9' export const NUMERIC_TEXT_COLOR = 'text-blue-500' +export const UNIQUE_BETTOR_BONUS_AMOUNT = 5 diff --git a/common/txn.ts b/common/txn.ts index 0e772e0d..53b08501 100644 --- a/common/txn.ts +++ b/common/txn.ts @@ -1,6 +1,6 @@ // A txn (pronounced "texan") respresents a payment between two ids on Manifold // Shortened from "transaction" to distinguish from Firebase transactions (and save chars) -type AnyTxnType = Donation | Tip | Manalink | Referral +type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' export type Txn<T extends AnyTxnType = AnyTxnType> = { @@ -16,7 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = { amount: number token: 'M$' // | 'USD' | MarketOutcome - category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET' + category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS' + // Any extra data data?: { [key: string]: any } @@ -52,6 +53,12 @@ type Referral = { category: 'REFERRAL' } +type Bonus = { + fromType: 'BANK' + toType: 'USER' + category: 'UNIQUE_BETTOR_BONUS' +} + export type DonationTxn = Txn & Donation export type TipTxn = Txn & Tip export type ManalinkTxn = Txn & Manalink diff --git a/common/user.ts b/common/user.ts index d5dd0373..477139fd 100644 --- a/common/user.ts +++ b/common/user.ts @@ -57,6 +57,7 @@ export type PrivateUser = { initialIpAddress?: string apiKey?: string notificationPreferences?: notification_subscribe_types + lastTimeCheckedBonuses?: number } export type notification_subscribe_types = 'all' | 'less' | 'none' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index a32ed3bc..b63958f0 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -267,6 +267,15 @@ export const createNotification = async ( } } + const notifyContractCreatorOfUniqueBettorsBonus = async ( + userToReasonTexts: user_to_reason_texts, + userId: string + ) => { + userToReasonTexts[userId] = { + reason: 'unique_bettors_on_your_contract', + } + } + const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -309,6 +318,12 @@ export const createNotification = async ( }) } else if (sourceType === 'liquidity' && sourceUpdateType === 'created') { await notifyContractCreator(userToReasonTexts, sourceContract) + } else if (sourceType === 'bonus' && sourceUpdateType === 'created') { + // Note: the daily bonus won't have a contract attached to it + await notifyContractCreatorOfUniqueBettorsBonus( + userToReasonTexts, + sourceContract.creatorId + ) } return userToReasonTexts } diff --git a/functions/src/get-daily-bonuses.ts b/functions/src/get-daily-bonuses.ts new file mode 100644 index 00000000..c5c1a1b3 --- /dev/null +++ b/functions/src/get-daily-bonuses.ts @@ -0,0 +1,139 @@ +import { APIError, newEndpoint } from './api' +import { log } from './utils' +import * as admin from 'firebase-admin' +import { PrivateUser } from '../../common/lib/user' +import { uniq } from 'lodash' +import { Bet } from '../../common/lib/bet' +const firestore = admin.firestore() +import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' +import { runTxn, TxnData } from './transact' +import { createNotification } from './create-notification' +import { User } from '../../common/lib/user' +import { Contract } from '../../common/lib/contract' +import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants' + +const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime() +const QUERY_LIMIT_SECONDS = 60 + +export const getdailybonuses = newEndpoint({}, async (req, auth) => { + const { user, lastTimeCheckedBonuses } = await firestore.runTransaction( + async (trans) => { + const userSnap = await trans.get( + firestore.doc(`private-users/${auth.uid}`) + ) + if (!userSnap.exists) throw new APIError(400, 'User not found.') + const user = userSnap.data() as PrivateUser + const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0 + if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000) + throw new APIError( + 400, + `Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.` + ) + await trans.update(userSnap.ref, { + lastTimeCheckedBonuses: Date.now(), + }) + return { + user, + lastTimeCheckedBonuses, + } + } + ) + // TODO: switch to prod id + // const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account + const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account + const fromSnap = await firestore.doc(`users/${fromUserId}`).get() + if (!fromSnap.exists) throw new APIError(400, 'From user not found.') + const fromUser = fromSnap.data() as User + // Get all users contracts made since implementation time + const userContractsSnap = await firestore + .collection(`contracts`) + .where('creatorId', '==', user.id) + .where('createdTime', '>=', BONUS_START_DATE) + .get() + const userContracts = userContractsSnap.docs.map( + (doc) => doc.data() as Contract + ) + const nullReturn = { status: 'no bets', txn: null } + for (const contract of userContracts) { + const result = await firestore.runTransaction(async (trans) => { + const contractId = contract.id + // Get all bets made on user's contracts + const bets = ( + await firestore + .collection(`contracts/${contractId}/bets`) + .where('userId', '!=', user.id) + .get() + ).docs.map((bet) => bet.ref) + if (bets.length === 0) { + return nullReturn + } + const contractBetsSnap = await trans.getAll(...bets) + const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet) + + const uniqueBettorIdsBeforeLastResetTime = uniq( + contractBets + .filter((bet) => bet.createdTime < lastTimeCheckedBonuses) + .map((bet) => bet.userId) + ) + + // Filter users for ONLY those that have made bets since the last daily bonus received time + const uniqueBettorIdsWithBetsAfterLastResetTime = uniq( + contractBets + .filter((bet) => bet.createdTime > lastTimeCheckedBonuses) + .map((bet) => bet.userId) + ) + + // Filter for users only present in the above list + const newUniqueBettorIds = + uniqueBettorIdsWithBetsAfterLastResetTime.filter( + (userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId) + ) + newUniqueBettorIds.length > 0 && + log( + `Got ${newUniqueBettorIds.length} new unique bettors since last bonus` + ) + if (newUniqueBettorIds.length === 0) { + return nullReturn + } + // Create combined txn for all unique bettors + const bonusTxnDetails = { + contractId: contractId, + uniqueBettors: newUniqueBettorIds.length, + } + const bonusTxn: TxnData = { + fromId: fromUser.id, + fromType: 'BANK', + toId: user.id, + toType: 'USER', + amount: UNIQUE_BETTOR_BONUS_AMOUNT * newUniqueBettorIds.length, + token: 'M$', + category: 'UNIQUE_BETTOR_BONUS', + description: JSON.stringify(bonusTxnDetails), + } + return await runTxn(trans, bonusTxn) + }) + + if (result.status != 'success' || !result.txn) { + result.status != nullReturn.status && + log(`No bonus for user: ${user.id} - reason:`, result.status) + } else { + log(`Bonus txn for user: ${user.id} completed:`, result.txn?.id) + await createNotification( + result.txn.id, + 'bonus', + 'created', + fromUser, + result.txn.id, + result.txn.amount + '', + contract, + undefined, + // No need to set the user id, we'll use the contract creator id + undefined, + contract.slug, + contract.question + ) + } + } + + return { userId: user.id, message: 'success' } +}) diff --git a/functions/src/index.ts b/functions/src/index.ts index b643ff5e..e4a30761 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -38,3 +38,4 @@ export * from './create-contract' export * from './withdraw-liquidity' export * from './create-group' export * from './resolve-market' +export * from './get-daily-bonuses' diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 5ce9e239..ba46bd80 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -182,7 +182,6 @@ export default function Sidebar(props: { className?: string }) { const { className } = props const router = useRouter() const currentPage = router.pathname - const user = useUser() const navigationOptions = !user ? signedOutNavigation : getNavigation() const mobileNavigationOptions = !user diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index e2618870..ac4d772f 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -2,17 +2,29 @@ import { BellIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { Row } from 'web/components/layout/row' import { useEffect, useState } from 'react' -import { useUser } from 'web/hooks/use-user' +import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useRouter } from 'next/router' import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications' +import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' +import { requestBonuses } from 'web/lib/firebase/api-call' export default function NotificationsIcon(props: { className?: string }) { const user = useUser() - const notifications = usePreferredGroupedNotifications(user?.id, { + const privateUser = usePrivateUser(user?.id) + const notifications = usePreferredGroupedNotifications(privateUser?.id, { unseenOnly: true, }) const [seen, setSeen] = useState(false) + useEffect(() => { + if (!privateUser) return + + if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 60 * 1000) + requestBonuses({}).catch((error) => { + console.log("couldn't get bonuses:", error.message) + }) + }, [privateUser]) + const router = useRouter() useEffect(() => { if (router.pathname.endsWith('notifications')) return setSeen(true) @@ -24,7 +36,9 @@ export default function NotificationsIcon(props: { className?: string }) { <div className={'relative'}> {!seen && notifications && notifications.length > 0 && ( <div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2"> - {notifications.length} + {notifications.length > NOTIFICATIONS_PER_PAGE + ? `${NOTIFICATIONS_PER_PAGE}+` + : notifications.length} </div> )} <BellIcon className={clsx(props.className)} /> diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index c947e8d0..0a15754d 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -7,9 +7,10 @@ import { groupBy, map } from 'lodash' export type NotificationGroup = { notifications: Notification[] - sourceContractId: string + groupedById: string isSeen: boolean timePeriod: string + type: 'income' | 'normal' } export function usePreferredGroupedNotifications( @@ -37,25 +38,43 @@ export function groupNotifications(notifications: Notification[]) { new Date(notification.createdTime).toDateString() ) Object.keys(notificationGroupsByDay).forEach((day) => { - // Group notifications by contract: + const notificationsGroupedByDay = notificationGroupsByDay[day] + const bonusNotifications = notificationsGroupedByDay.filter( + (notification) => notification.sourceType === 'bonus' + ) + const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter( + (notification) => notification.sourceType !== 'bonus' + ) + if (bonusNotifications.length > 0) { + notificationGroups = notificationGroups.concat({ + notifications: bonusNotifications, + groupedById: 'income' + day, + isSeen: bonusNotifications[0].isSeen, + timePeriod: day, + type: 'income', + }) + } + // Group notifications by contract, filtering out bonuses: const groupedNotificationsByContractId = groupBy( - notificationGroupsByDay[day], + normalNotificationsGroupedByDay, (notification) => { return notification.sourceContractId } ) notificationGroups = notificationGroups.concat( map(groupedNotificationsByContractId, (notifications, contractId) => { + const notificationsForContractId = groupedNotificationsByContractId[ + contractId + ].sort((a, b) => { + return b.createdTime - a.createdTime + }) // Create a notification group for each contract within each day const notificationGroup: NotificationGroup = { - notifications: groupedNotificationsByContractId[contractId].sort( - (a, b) => { - return b.createdTime - a.createdTime - } - ), - sourceContractId: contractId, - isSeen: groupedNotificationsByContractId[contractId][0].isSeen, + notifications: notificationsForContractId, + groupedById: contractId, + isSeen: notificationsForContractId[0].isSeen, timePeriod: day, + type: 'normal', } return notificationGroup }) diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index e02872ae..db41e592 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -73,3 +73,7 @@ export function sellBet(params: any) { export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } + +export function requestBonuses(params: any) { + return call(getFunctionUrl('getdailybonuses'), 'POST', params) +} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index f3512c56..229e8c8d 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,12 +1,7 @@ import { Tabs } from 'web/components/layout/tabs' import { useUser } from 'web/hooks/use-user' import React, { useEffect, useState } from 'react' -import { - Notification, - notification_reason_types, - notification_source_types, - notification_source_update_types, -} from 'common/notification' +import { Notification } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' @@ -31,47 +26,40 @@ import { ProbPercentLabel, } from 'web/components/outcome-label' import { - groupNotifications, NotificationGroup, usePreferredGroupedNotifications, } from 'web/hooks/use-notifications' -import { CheckIcon, XIcon } from '@heroicons/react/outline' +import { CheckIcon, TrendingUpIcon, XIcon } from '@heroicons/react/outline' import toast from 'react-hot-toast' import { formatMoney } from 'common/util/format' import { groupPath } from 'web/lib/firebase/groups' +import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' +import { groupBy } from 'lodash' + +export const NOTIFICATIONS_PER_PAGE = 30 +export const HIGHLIGHT_DURATION = 30 * 1000 export default function Notifications() { const user = useUser() - const [unseenNotificationGroups, setUnseenNotificationGroups] = useState< - NotificationGroup[] | undefined - >(undefined) - const allNotificationGroups = usePreferredGroupedNotifications(user?.id, { + const [page, setPage] = useState(1) + + const groupedNotifications = usePreferredGroupedNotifications(user?.id, { unseenOnly: false, }) - + const [paginatedNotificationGroups, setPaginatedNotificationGroups] = + useState<NotificationGroup[]>([]) useEffect(() => { - if (!allNotificationGroups) return - // Don't re-add notifications that are visible right now or have been seen already. - const currentlyVisibleUnseenNotificationIds = Object.values( - unseenNotificationGroups ?? [] - ) - .map((n) => n.notifications.map((n) => n.id)) - .flat() - const unseenGroupedNotifications = groupNotifications( - allNotificationGroups - .map((notification: NotificationGroup) => notification.notifications) - .flat() - .filter( - (notification: Notification) => - !notification.isSeen || - currentlyVisibleUnseenNotificationIds.includes(notification.id) - ) - ) - setUnseenNotificationGroups(unseenGroupedNotifications) - - // We don't want unseenNotificationsGroup to be in the dependencies as we update it here. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allNotificationGroups]) + if (!groupedNotifications) return + const start = (page - 1) * NOTIFICATIONS_PER_PAGE + const end = start + NOTIFICATIONS_PER_PAGE + const maxNotificationsToShow = groupedNotifications.slice(start, end) + const remainingNotification = groupedNotifications.slice(end) + for (const notification of remainingNotification) { + if (notification.isSeen) break + else setNotificationsAsSeen(notification.notifications) + } + setPaginatedNotificationGroups(maxNotificationsToShow) + }, [groupedNotifications, page]) if (user === undefined) { return <LoadingIndicator /> @@ -80,7 +68,6 @@ export default function Notifications() { return <Custom404 /> } - // TODO: use infinite scroll return ( <Page> <div className={'p-2 sm:p-4'}> @@ -90,53 +77,74 @@ export default function Notifications() { defaultIndex={0} tabs={[ { - title: 'New Notifications', - content: unseenNotificationGroups ? ( + title: 'Notifications', + content: groupedNotifications ? ( <div className={''}> - {unseenNotificationGroups.length === 0 && - "You don't have any new notifications."} - {unseenNotificationGroups.map((notification) => + {paginatedNotificationGroups.length === 0 && + "You don't have any notifications. Try changing your settings to see more."} + {paginatedNotificationGroups.map((notification) => notification.notifications.length === 1 ? ( <NotificationItem notification={notification.notifications[0]} key={notification.notifications[0].id} /> + ) : notification.type === 'income' ? ( + <IncomeNotificationGroupItem + notificationGroup={notification} + key={notification.groupedById + notification.timePeriod} + /> ) : ( <NotificationGroupItem notificationGroup={notification} - key={ - notification.sourceContractId + - notification.timePeriod - } + key={notification.groupedById + notification.timePeriod} /> ) )} - </div> - ) : ( - <LoadingIndicator /> - ), - }, - { - title: 'All Notifications', - content: allNotificationGroups ? ( - <div className={''}> - {allNotificationGroups.length === 0 && - "You don't have any notifications. Try changing your settings to see more."} - {allNotificationGroups.map((notification) => - notification.notifications.length === 1 ? ( - <NotificationItem - notification={notification.notifications[0]} - key={notification.notifications[0].id} - /> - ) : ( - <NotificationGroupItem - notificationGroup={notification} - key={ - notification.sourceContractId + - notification.timePeriod - } - /> - ) + {groupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( + <nav + className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" + aria-label="Pagination" + > + <div className="hidden sm:block"> + <p className="text-sm text-gray-700"> + Showing{' '} + <span className="font-medium"> + {page === 1 + ? page + : (page - 1) * NOTIFICATIONS_PER_PAGE} + </span>{' '} + to{' '} + <span className="font-medium"> + {page * NOTIFICATIONS_PER_PAGE} + </span>{' '} + of{' '} + <span className="font-medium"> + {groupedNotifications.length} + </span>{' '} + results + </p> + </div> + <div className="flex flex-1 justify-between sm:justify-end"> + <a + href="#" + className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page > 1 && setPage(page - 1)} + > + Previous + </a> + <a + href="#" + className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => + page < + groupedNotifications?.length / + NOTIFICATIONS_PER_PAGE && setPage(page + 1) + } + > + Next + </a> + </div> + </nav> )} </div> ) : ( @@ -164,7 +172,6 @@ const setNotificationsAsSeen = (notifications: Notification[]) => { updateDoc( doc(db, `users/${notification.userId}/notifications/`, notification.id), { - ...notification, isSeen: true, viewTime: new Date(), } @@ -173,6 +180,152 @@ const setNotificationsAsSeen = (notifications: Notification[]) => { return notifications } +function IncomeNotificationGroupItem(props: { + notificationGroup: NotificationGroup + className?: string +}) { + const { notificationGroup, className } = props + const { notifications } = notificationGroup + const numSummaryLines = 3 + + const [expanded, setExpanded] = useState(false) + const [highlighted, setHighlighted] = useState(false) + useEffect(() => { + if (notifications.some((n) => !n.isSeen)) { + setHighlighted(true) + setTimeout(() => { + setHighlighted(false) + }, HIGHLIGHT_DURATION) + } + setNotificationsAsSeen(notifications) + }, [notifications]) + + useEffect(() => { + if (expanded) setHighlighted(false) + }, [expanded]) + + const totalIncome = notifications.reduce( + (acc, notification) => + acc + + (notification.sourceType && + notification.sourceText && + notification.sourceType === 'bonus' + ? parseInt(notification.sourceText) + : 0), + 0 + ) + // loop through the contracts and combine the notification items into one + function combineNotificationsByAddingSourceTextsAndReturningTheRest( + notifications: Notification[] + ) { + const newNotifications = [] + const groupedNotificationsByContractId = groupBy( + notifications, + (notification) => { + return notification.sourceContractId + } + ) + for (const contractId in groupedNotificationsByContractId) { + const notificationsForContractId = + groupedNotificationsByContractId[contractId] + let sum = 0 + notificationsForContractId.forEach( + (notification) => + notification.sourceText && + (sum = parseInt(notification.sourceText) + sum) + ) + + const newNotification = + notificationsForContractId.length === 1 + ? notificationsForContractId[0] + : { + ...notificationsForContractId[0], + sourceText: sum.toString(), + } + newNotifications.push(newNotification) + } + return newNotifications + } + + const combinedNotifs = + combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications) + + return ( + <div + className={clsx( + 'relative cursor-pointer bg-white px-2 pt-6 text-sm', + className, + !expanded ? 'hover:bg-gray-100' : '', + highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' + )} + onClick={() => setExpanded(!expanded)} + > + {expanded && ( + <span + className="absolute top-14 left-6 -ml-px h-[calc(100%-5rem)] w-0.5 bg-gray-200" + aria-hidden="true" + /> + )} + <Row className={'items-center text-gray-500 sm:justify-start'}> + <TrendingUpIcon className={'text-primary h-7 w-7'} /> + <div className={'flex-1 overflow-hidden pl-2 sm:flex'}> + <div + onClick={() => setExpanded(!expanded)} + className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} + > + <span> + {'Daily Income Summary: '} + <span className={'text-primary'}>{formatMoney(totalIncome)}</span> + </span> + </div> + <RelativeTimestamp time={notifications[0].createdTime} /> + </div> + </Row> + <div> + <div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}> + {' '} + <div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}> + {!expanded ? ( + <> + {combinedNotifs + .slice(0, numSummaryLines) + .map((notification) => { + return ( + <NotificationItem + notification={notification} + justSummary={true} + key={notification.id} + /> + ) + })} + <div className={'text-sm text-gray-500 hover:underline '}> + {combinedNotifs.length - numSummaryLines > 0 + ? 'And ' + + (combinedNotifs.length - numSummaryLines) + + ' more...' + : ''} + </div> + </> + ) : ( + <> + {combinedNotifs.map((notification) => ( + <NotificationItem + notification={notification} + key={notification.id} + justSummary={false} + /> + ))} + </> + )} + </div> + </div> + + <div className={'mt-6 border-b border-gray-300'} /> + </div> + </div> + ) +} + function NotificationGroupItem(props: { notificationGroup: NotificationGroup className?: string @@ -187,17 +340,28 @@ function NotificationGroupItem(props: { const numSummaryLines = 3 const [expanded, setExpanded] = useState(false) - + const [highlighted, setHighlighted] = useState(false) useEffect(() => { + if (notifications.some((n) => !n.isSeen)) { + setHighlighted(true) + setTimeout(() => { + setHighlighted(false) + }, HIGHLIGHT_DURATION) + } setNotificationsAsSeen(notifications) }, [notifications]) + useEffect(() => { + if (expanded) setHighlighted(false) + }, [expanded]) + return ( <div className={clsx( 'relative cursor-pointer bg-white px-2 pt-6 text-sm', className, - !expanded ? 'hover:bg-gray-100' : '' + !expanded ? 'hover:bg-gray-100' : '', + highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' )} onClick={() => setExpanded(!expanded)} > @@ -432,7 +596,7 @@ function NotificationSettings() { /> <NotificationSettingLine highlight={notificationSettings !== 'none'} - label={"Referral bonuses you've received"} + label={"Income & referral bonuses you've received"} /> <NotificationSettingLine label={"Activity on questions you've ever bet or commented on"} @@ -476,17 +640,6 @@ function NotificationSettings() { ) } -function isNotificationAboutContractResolution( - sourceType: notification_source_types | undefined, - sourceUpdateType: notification_source_update_types | undefined, - contract: Contract | null | undefined -) { - return ( - (sourceType === 'contract' && sourceUpdateType === 'resolved') || - (sourceType === 'contract' && !sourceUpdateType && contract?.resolution) - ) -} - function NotificationItem(props: { notification: Notification justSummary?: boolean @@ -522,6 +675,16 @@ function NotificationItem(props: { } }, [reasonText, sourceText]) + const [highlighted, setHighlighted] = useState(false) + useEffect(() => { + if (!notification.isSeen) { + setHighlighted(true) + setTimeout(() => { + setHighlighted(false) + }, HIGHLIGHT_DURATION) + } + }, [notification.isSeen]) + useEffect(() => { setNotificationsAsSeen([notification]) }, [notification]) @@ -559,22 +722,21 @@ function NotificationItem(props: { <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> <div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}> <div className={'flex pl-1 sm:pl-0'}> - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - /> + {sourceType != 'bonus' && ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-0 flex-shrink-0'} + /> + )} <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> <span className={'flex-shrink-0'}> {sourceType && reason && - getReasonForShowingNotification( - sourceType, - reason, - sourceUpdateType, - undefined, - true - ).replace(' on', '')} + getReasonForShowingNotification(notification, true).replace( + ' on', + '' + )} </span> <div className={'ml-1 text-black'}> <NotificationTextLabel @@ -593,37 +755,41 @@ function NotificationItem(props: { } return ( - <div className={'bg-white px-2 pt-6 text-sm sm:px-4'}> + <div + className={clsx( + 'bg-white px-2 pt-6 text-sm sm:px-4', + highlighted && 'bg-indigo-200' + )} + > <a href={getSourceUrl()}> <Row className={'items-center text-gray-500 sm:justify-start'}> - <Avatar - avatarUrl={sourceUserAvatarUrl} - size={'sm'} - className={'mr-2'} - username={sourceUserName} - /> + {sourceType != 'bonus' ? ( + <Avatar + avatarUrl={sourceUserAvatarUrl} + size={'sm'} + className={'mr-2'} + username={sourceUserName} + /> + ) : ( + <TrendingUpIcon className={'text-primary h-7 w-7'} /> + )} <div className={'flex-1 overflow-hidden sm:flex'}> <div className={ 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' } > - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - /> + {sourceType != 'bonus' && sourceUpdateType != 'closed' && ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-0 flex-shrink-0'} + /> + )} <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> {sourceType && reason && ( <div className={'inline truncate'}> - {getReasonForShowingNotification( - sourceType, - reason, - sourceUpdateType, - undefined, - false, - sourceSlug - )} + {getReasonForShowingNotification(notification, false)} <a href={ sourceContractCreatorUsername @@ -684,13 +850,7 @@ function NotificationTextLabel(props: { return <span>{contract?.question || sourceContractTitle}</span> if (!sourceText) return <div /> // Resolved contracts - if ( - isNotificationAboutContractResolution( - sourceType, - sourceUpdateType, - contract - ) - ) { + if (sourceType === 'contract' && sourceUpdateType === 'resolved') { { if (sourceText === 'YES' || sourceText == 'NO') { return <BinaryOutcomeLabel outcome={sourceText as any} /> @@ -730,6 +890,12 @@ function NotificationTextLabel(props: { return ( <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span> ) + } else if (sourceType === 'bonus' && sourceText) { + return ( + <span className="text-primary"> + {'+' + formatMoney(parseInt(sourceText))} + </span> + ) } // return default text return ( @@ -740,15 +906,13 @@ function NotificationTextLabel(props: { } function getReasonForShowingNotification( - source: notification_source_types, - reason: notification_reason_types, - sourceUpdateType: notification_source_update_types | undefined, - contract: Contract | undefined | null, - simple?: boolean, - sourceSlug?: string + notification: Notification, + simple?: boolean ) { + const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } = + notification let reasonText: string - switch (source) { + switch (sourceType) { case 'comment': if (reason === 'reply_to_users_answer') reasonText = !simple ? 'replied to your answer on' : 'replied' @@ -768,16 +932,9 @@ function getReasonForShowingNotification( break case 'contract': if (reason === 'you_follow_user') reasonText = 'created a new question' - else if ( - isNotificationAboutContractResolution( - source, - sourceUpdateType, - contract - ) - ) - reasonText = `resolved` + else if (sourceUpdateType === 'resolved') reasonText = `resolved` else if (sourceUpdateType === 'closed') - reasonText = `please resolve your question` + reasonText = `Please resolve your question` else reasonText = `updated` break case 'answer': @@ -805,6 +962,15 @@ function getReasonForShowingNotification( else if (sourceSlug) reasonText = 'joined because you shared' else reasonText = 'joined because of you' break + case 'bonus': + if (reason === 'unique_bettors_on_your_contract' && sourceText) + reasonText = !simple + ? `You had ${ + parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT + } unique bettors on` + : 'You earned Mana for unique bettors:' + else reasonText = 'You earned your daily manna' + break default: reasonText = '' } From 9bff858696a6a1502b8ff9bb3fec2868d3591524 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 12:25:44 -0700 Subject: [PATCH 043/519] Fix up lint configuration, lint line endings (#615) * Make sure we ignore all built code in common and functions * Add lint for Unix line endings * Fix line endings in withdraw-liquidity.ts --- common/.eslintrc.js | 2 + functions/.eslintrc.js | 3 +- functions/src/withdraw-liquidity.ts | 276 ++++++++++++++-------------- web/.eslintrc.js | 1 + 4 files changed, 143 insertions(+), 139 deletions(-) diff --git a/common/.eslintrc.js b/common/.eslintrc.js index 3d6cfa82..c6f9703e 100644 --- a/common/.eslintrc.js +++ b/common/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { plugins: ['lodash'], extends: ['eslint:recommended'], + ignorePatterns: ['lib'], env: { browser: true, node: true, @@ -31,6 +32,7 @@ module.exports = { rules: { 'no-extra-semi': 'off', 'no-constant-condition': ['error', { checkLoops: false }], + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, } diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js index 7f571610..2c607231 100644 --- a/functions/.eslintrc.js +++ b/functions/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { plugins: ['lodash'], extends: ['eslint:recommended'], - ignorePatterns: ['lib'], + ignorePatterns: ['dist', 'lib'], env: { node: true, }, @@ -30,6 +30,7 @@ module.exports = { }, ], rules: { + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, } diff --git a/functions/src/withdraw-liquidity.ts b/functions/src/withdraw-liquidity.ts index 4c48ce49..cc8c84cf 100644 --- a/functions/src/withdraw-liquidity.ts +++ b/functions/src/withdraw-liquidity.ts @@ -1,138 +1,138 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' - -import { CPMMContract } from '../../common/contract' -import { User } from '../../common/user' -import { subtractObjects } from '../../common/util/object' -import { LiquidityProvision } from '../../common/liquidity-provision' -import { getUserLiquidityShares } from '../../common/calculate-cpmm' -import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' -import { noFees } from '../../common/fees' - -import { APIError } from './api' -import { redeemShares } from './redeem-shares' - -export const withdrawLiquidity = functions - .runWith({ minInstances: 1 }) - .https.onCall( - async ( - data: { - contractId: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } - - const { contractId } = data - if (!contractId) - return { status: 'error', message: 'Missing contract id' } - - return await firestore - .runTransaction(async (trans) => { - const lpDoc = firestore.doc(`users/${userId}`) - const lpSnap = await trans.get(lpDoc) - if (!lpSnap.exists) throw new APIError(400, 'User not found.') - const lp = lpSnap.data() as User - - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await trans.get(contractDoc) - if (!contractSnap.exists) - throw new APIError(400, 'Contract not found.') - const contract = contractSnap.data() as CPMMContract - - const liquidityCollection = firestore.collection( - `contracts/${contractId}/liquidity` - ) - - const liquiditiesSnap = await trans.get(liquidityCollection) - - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) - - const userShares = getUserLiquidityShares( - userId, - contract, - liquidities - ) - - // zero all added amounts for now - // can add support for partial withdrawals in the future - liquiditiesSnap.docs - .filter( - (_, i) => - !liquidities[i].isAnte && liquidities[i].userId === userId - ) - .forEach((doc) => trans.update(doc.ref, { amount: 0 })) - - const payout = Math.min(...Object.values(userShares)) - if (payout <= 0) return {} - - const newBalance = lp.balance + payout - const newTotalDeposits = lp.totalDeposits + payout - trans.update(lpDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, - } as Partial<User>) - - const newPool = subtractObjects(contract.pool, userShares) - - const minPoolShares = Math.min(...Object.values(newPool)) - const adjustedTotal = contract.totalLiquidity - payout - - // total liquidity is a bogus number; use minPoolShares to prevent from going negative - const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) - - trans.update(contractDoc, { - pool: newPool, - totalLiquidity: newTotalLiquidity, - }) - - const prob = getProbability(contract) - - // surplus shares become user's bets - const bets = Object.entries(userShares) - .map(([outcome, shares]) => - shares - payout < 1 // don't create bet if less than 1 share - ? undefined - : ({ - userId: userId, - contractId: contract.id, - amount: - (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), - shares: shares - payout, - outcome, - probBefore: prob, - probAfter: prob, - createdTime: Date.now(), - isLiquidityProvision: true, - fees: noFees, - } as Omit<Bet, 'id'>) - ) - .filter((x) => x !== undefined) - - for (const bet of bets) { - const doc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - trans.create(doc, { id: doc.id, ...bet }) - } - - return userShares - }) - .then(async (result) => { - // redeem surplus bet with pre-existing bets - await redeemShares(userId, contractId) - - console.log('userid', userId, 'withdraws', result) - return { status: 'success', userShares: result } - }) - .catch((e) => { - return { status: 'error', message: e.message } - }) - } - ) - -const firestore = admin.firestore() +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { CPMMContract } from '../../common/contract' +import { User } from '../../common/user' +import { subtractObjects } from '../../common/util/object' +import { LiquidityProvision } from '../../common/liquidity-provision' +import { getUserLiquidityShares } from '../../common/calculate-cpmm' +import { Bet } from '../../common/bet' +import { getProbability } from '../../common/calculate' +import { noFees } from '../../common/fees' + +import { APIError } from './api' +import { redeemShares } from './redeem-shares' + +export const withdrawLiquidity = functions + .runWith({ minInstances: 1 }) + .https.onCall( + async ( + data: { + contractId: string + }, + context + ) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const { contractId } = data + if (!contractId) + return { status: 'error', message: 'Missing contract id' } + + return await firestore + .runTransaction(async (trans) => { + const lpDoc = firestore.doc(`users/${userId}`) + const lpSnap = await trans.get(lpDoc) + if (!lpSnap.exists) throw new APIError(400, 'User not found.') + const lp = lpSnap.data() as User + + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await trans.get(contractDoc) + if (!contractSnap.exists) + throw new APIError(400, 'Contract not found.') + const contract = contractSnap.data() as CPMMContract + + const liquidityCollection = firestore.collection( + `contracts/${contractId}/liquidity` + ) + + const liquiditiesSnap = await trans.get(liquidityCollection) + + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision + ) + + const userShares = getUserLiquidityShares( + userId, + contract, + liquidities + ) + + // zero all added amounts for now + // can add support for partial withdrawals in the future + liquiditiesSnap.docs + .filter( + (_, i) => + !liquidities[i].isAnte && liquidities[i].userId === userId + ) + .forEach((doc) => trans.update(doc.ref, { amount: 0 })) + + const payout = Math.min(...Object.values(userShares)) + if (payout <= 0) return {} + + const newBalance = lp.balance + payout + const newTotalDeposits = lp.totalDeposits + payout + trans.update(lpDoc, { + balance: newBalance, + totalDeposits: newTotalDeposits, + } as Partial<User>) + + const newPool = subtractObjects(contract.pool, userShares) + + const minPoolShares = Math.min(...Object.values(newPool)) + const adjustedTotal = contract.totalLiquidity - payout + + // total liquidity is a bogus number; use minPoolShares to prevent from going negative + const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) + + trans.update(contractDoc, { + pool: newPool, + totalLiquidity: newTotalLiquidity, + }) + + const prob = getProbability(contract) + + // surplus shares become user's bets + const bets = Object.entries(userShares) + .map(([outcome, shares]) => + shares - payout < 1 // don't create bet if less than 1 share + ? undefined + : ({ + userId: userId, + contractId: contract.id, + amount: + (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), + shares: shares - payout, + outcome, + probBefore: prob, + probAfter: prob, + createdTime: Date.now(), + isLiquidityProvision: true, + fees: noFees, + } as Omit<Bet, 'id'>) + ) + .filter((x) => x !== undefined) + + for (const bet of bets) { + const doc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + trans.create(doc, { id: doc.id, ...bet }) + } + + return userShares + }) + .then(async (result) => { + // redeem surplus bet with pre-existing bets + await redeemShares(userId, contractId) + + console.log('userid', userId, 'withdraws', result) + return { status: 'success', userShares: result } + }) + .catch((e) => { + return { status: 'error', message: e.message } + }) + } + ) + +const firestore = admin.firestore() diff --git a/web/.eslintrc.js b/web/.eslintrc.js index b55b3277..fec650f9 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { ], '@next/next/no-img-element': 'off', '@next/next/no-typos': 'off', + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, env: { From a9e74e71119d65b4d7f819370d450d0c4e1e2da3 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 12:25:58 -0700 Subject: [PATCH 044/519] Add functions framework as explicit dependency (#613) --- functions/package.json | 1 + yarn.lock | 208 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 194 insertions(+), 15 deletions(-) diff --git a/functions/package.json b/functions/package.json index 93bea621..4c9f4338 100644 --- a/functions/package.json +++ b/functions/package.json @@ -23,6 +23,7 @@ "main": "functions/src/index.js", "dependencies": { "@amplitude/node": "1.10.0", + "@google-cloud/functions-framework": "3.1.2", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/yarn.lock b/yarn.lock index c07d548f..0ee2aa0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2181,6 +2181,20 @@ google-gax "^2.24.1" protobufjs "^6.8.6" +"@google-cloud/functions-framework@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz#2cd92ce4307bf7f32555d028dca22e398473b410" + integrity sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg== + dependencies: + "@types/express" "4.17.13" + body-parser "^1.18.3" + cloudevents "^6.0.0" + express "^4.16.4" + minimist "^1.2.5" + on-finished "^2.3.0" + read-pkg-up "^7.0.1" + semver "^7.3.5" + "@google-cloud/paginator@^3.0.7": version "3.0.7" resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" @@ -2926,7 +2940,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.13": +"@types/express@*", "@types/express@4.17.13", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -3049,6 +3063,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -3498,7 +3517,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.11.0, ajv@^8.8.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -3750,6 +3769,11 @@ autoprefixer@^10.3.7, autoprefixer@^10.4.2: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + axe-core@^4.3.5: version "4.4.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c" @@ -3880,7 +3904,7 @@ bluebird@^3.7.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.0: +body-parser@1.20.0, body-parser@^1.18.3: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== @@ -4236,6 +4260,16 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +cloudevents@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/cloudevents/-/cloudevents-6.0.2.tgz#7b4990a92c6c30f6790eb4b59207b4d8949fca12" + integrity sha512-mn/4EZnAbhfb/TghubK2jPnxYM15JRjf8LnWJtXidiVKi5ZCkd+p9jyBZbL57w7nRm6oFAzJhjxRLsXd/DNaBQ== + dependencies: + ajv "^8.11.0" + ajv-formats "^2.1.1" + util "^0.12.4" + uuid "^8.3.2" + clsx@1.1.1, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" @@ -5277,7 +5311,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== @@ -5657,7 +5691,7 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.17.1, express@^4.17.3: +express@^4.16.4, express@^4.17.1, express@^4.17.3: version "4.18.1" resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== @@ -5871,7 +5905,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -5981,6 +6015,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.7: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.2" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340" @@ -6585,6 +6626,11 @@ hoist-non-react-statics@^3.1.0: dependencies: react-is "^16.7.0" +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -6945,6 +6991,14 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -6977,7 +7031,7 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.4: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -6989,7 +7043,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.2.0, is-core-module@^2.8.1: +is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== @@ -7028,6 +7082,13 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -7161,6 +7222,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.3, is-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67" + integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -8126,6 +8198,16 @@ nopt@1.0.10: dependencies: abbrev "1" +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -8252,7 +8334,7 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -9463,6 +9545,25 @@ react@17.0.2, react@^17.0.1: loose-envify "^1.1.0" object-assign "^4.1.1" +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + readable-stream@1.1.x: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -9767,6 +9868,15 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.10.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -9848,7 +9958,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9959,16 +10069,16 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.4.1, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -10223,6 +10333,32 @@ spawn-command@^0.0.2-1: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" + integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== + spdy-transport@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" @@ -10706,6 +10842,16 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-fest@^2.5.0: version "2.13.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb" @@ -10974,6 +11120,18 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.12.4: + version "0.12.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" + integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + safe-buffer "^5.1.2" + which-typed-array "^1.1.2" + utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -10999,6 +11157,14 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" @@ -11232,6 +11398,18 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-typed-array@^1.1.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f" + integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.9" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From f0fbdf1b42490a80e0899bcfcfded8d3c26aa61f Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 12:26:13 -0700 Subject: [PATCH 045/519] Add a missing index (#606) --- firestore.indexes.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/firestore.indexes.json b/firestore.indexes.json index 064f6f2f..e0cee632 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -337,6 +337,20 @@ "order": "DESCENDING" } ] + }, + { + "collectionGroup": "portfolioHistory", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "timestamp", + "order": "ASCENDING" + } + ] } ], "fieldOverrides": [ From 7f2bbdcb878477604d6c9c27b58d801054493e7a Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 12:26:51 -0700 Subject: [PATCH 046/519] Allow people to sell all their shares (#599) --- functions/src/sell-shares.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index a0c19f2c..62e43105 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -46,7 +46,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) const maxShares = sumBy(outcomeBets, (bet) => bet.shares) - if (shares > maxShares + 0.000000000001) + if (shares > maxShares) throw new APIError(400, `You can only sell up to ${maxShares} shares.`) const { newBet, newPool, newP, fees } = getCpmmSellBetInfo( From 4d1c50a6cca80ba20b674f0bd90dbda9e4a17aac Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 12:35:39 -0700 Subject: [PATCH 047/519] Redemption refactoring (#614) * Refactor share redemption code into a few sensible functions * Put very general share redemption code into common --- common/new-bet.ts | 10 ++-- common/redeem.ts | 54 ++++++++++++++++++++++ functions/src/redeem-shares.ts | 83 ++++++---------------------------- 3 files changed, 74 insertions(+), 73 deletions(-) create mode 100644 common/redeem.ts diff --git a/common/new-bet.ts b/common/new-bet.ts index 236c0908..57739af3 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -20,9 +20,9 @@ import { noFees } from './fees' import { addObjects } from './util/object' import { NUMERIC_FIXED_VAR } from './numeric-constants' -export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'> +export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'> export type BetInfo = { - newBet: CandidateBet<Bet> + newBet: CandidateBet newPool?: { [outcome: string]: number } newTotalShares?: { [outcome: string]: number } newTotalBets?: { [outcome: string]: number } @@ -46,7 +46,7 @@ export const getNewBinaryCpmmBetInfo = ( const probBefore = getCpmmProbability(pool, p) const probAfter = getCpmmProbability(newPool, newP) - const newBet: CandidateBet<Bet> = { + const newBet: CandidateBet = { contractId: contract.id, amount, shares, @@ -96,7 +96,7 @@ export const getNewBinaryDpmBetInfo = ( const probBefore = getDpmProbability(contract.totalShares) const probAfter = getDpmProbability(newTotalShares) - const newBet: CandidateBet<Bet> = { + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, @@ -133,7 +133,7 @@ export const getNewMultiBetInfo = ( const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) - const newBet: CandidateBet<Bet> = { + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, diff --git a/common/redeem.ts b/common/redeem.ts new file mode 100644 index 00000000..4a4080f6 --- /dev/null +++ b/common/redeem.ts @@ -0,0 +1,54 @@ +import { partition, sumBy } from 'lodash' + +import { Bet } from './bet' +import { getProbability } from './calculate' +import { CPMMContract } from './contract' +import { noFees } from './fees' +import { CandidateBet } from './new-bet' + +type RedeemableBet = Pick<Bet, 'outcome' | 'shares' | 'loanAmount'> + +export const getRedeemableAmount = (bets: RedeemableBet[]) => { + const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES') + const yesShares = sumBy(yesBets, (b) => b.shares) + const noShares = sumBy(noBets, (b) => b.shares) + const shares = Math.max(Math.min(yesShares, noShares), 0) + const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) + const loanPayment = Math.min(loanAmount, shares) + const netAmount = shares - loanPayment + return { shares, loanPayment, netAmount } +} + +export const getRedemptionBets = ( + shares: number, + loanPayment: number, + contract: CPMMContract +) => { + const p = getProbability(contract) + const createdTime = Date.now() + const yesBet: CandidateBet = { + contractId: contract.id, + amount: p * -shares, + shares: -shares, + loanAmount: loanPayment ? -loanPayment / 2 : 0, + outcome: 'YES', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + const noBet: CandidateBet = { + contractId: contract.id, + amount: (1 - p) * -shares, + shares: -shares, + loanAmount: loanPayment ? -loanPayment / 2 : 0, + outcome: 'NO', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + return [yesBet, noBet] +} diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts index 67922a65..32b1d433 100644 --- a/functions/src/redeem-shares.ts +++ b/functions/src/redeem-shares.ts @@ -1,96 +1,43 @@ import * as admin from 'firebase-admin' -import { partition, sumBy } from 'lodash' import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' +import { getRedeemableAmount, getRedemptionBets } from '../../common/redeem' import { Contract } from '../../common/contract' -import { noFees } from '../../common/fees' import { User } from '../../common/user' export const redeemShares = async (userId: string, contractId: string) => { - return await firestore.runTransaction(async (transaction) => { + return await firestore.runTransaction(async (trans) => { const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) + const contractSnap = await trans.get(contractDoc) if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { mechanism, outcomeType } = contract - if ( - !(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') || - mechanism !== 'cpmm-1' - ) - return { status: 'success' } + const { mechanism } = contract + if (mechanism !== 'cpmm-1') return { status: 'success' } - const betsSnap = await transaction.get( - firestore - .collection(`contracts/${contract.id}/bets`) - .where('userId', '==', userId) - ) + const betsColl = firestore.collection(`contracts/${contract.id}/bets`) + const betsSnap = await trans.get(betsColl.where('userId', '==', userId)) const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES') - const yesShares = sumBy(yesBets, (b) => b.shares) - const noShares = sumBy(noBets, (b) => b.shares) - - const amount = Math.min(yesShares, noShares) - if (amount <= 0) return - - const prevLoanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) - const loanPaid = Math.min(prevLoanAmount, amount) - const netAmount = amount - loanPaid - - const p = getProbability(contract) - const createdTime = Date.now() - - const yesDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() - const yesBet: Bet = { - id: yesDoc.id, - userId: userId, - contractId: contract.id, - amount: p * -amount, - shares: -amount, - loanAmount: loanPaid ? -loanPaid / 2 : 0, - outcome: 'YES', - probBefore: p, - probAfter: p, - createdTime, - isRedemption: true, - fees: noFees, - } - - const noDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() - const noBet: Bet = { - id: noDoc.id, - userId: userId, - contractId: contract.id, - amount: (1 - p) * -amount, - shares: -amount, - loanAmount: loanPaid ? -loanPaid / 2 : 0, - outcome: 'NO', - probBefore: p, - probAfter: p, - createdTime, - isRedemption: true, - fees: noFees, - } + const { shares, loanPayment, netAmount } = getRedeemableAmount(bets) + const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract) const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) + const userSnap = await trans.get(userDoc) if (!userSnap.exists) return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User - const newBalance = user.balance + netAmount if (!isFinite(newBalance)) { throw new Error('Invalid user balance for ' + user.username) } - transaction.update(userDoc, { balance: newBalance }) - - transaction.create(yesDoc, yesBet) - transaction.create(noDoc, noBet) + const yesDoc = betsColl.doc() + const noDoc = betsColl.doc() + trans.update(userDoc, { balance: newBalance }) + trans.create(yesDoc, { id: yesDoc.id, userId, ...yesBet }) + trans.create(noDoc, { id: noDoc.id, userId, ...noBet }) return { status: 'success' } }) From 5eca9def9d011c80e4ea0ea6452f10012b17054a Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 5 Jul 2022 14:01:57 -0700 Subject: [PATCH 048/519] Don't accidentally make meaningless zero bets (#619) --- functions/src/redeem-shares.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts index 32b1d433..0a69521f 100644 --- a/functions/src/redeem-shares.ts +++ b/functions/src/redeem-shares.ts @@ -21,6 +21,9 @@ export const redeemShares = async (userId: string, contractId: string) => { const betsSnap = await trans.get(betsColl.where('userId', '==', userId)) const bets = betsSnap.docs.map((doc) => doc.data() as Bet) const { shares, loanPayment, netAmount } = getRedeemableAmount(bets) + if (netAmount === 0) { + return { status: 'success' } + } const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract) const userDoc = firestore.doc(`users/${userId}`) From 270a5fc13911e63f0d6a8615a56c478868bc9547 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Tue, 5 Jul 2022 14:34:16 -0700 Subject: [PATCH 049/519] also filter by username when adding people --- web/components/filter-select-users.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/filter-select-users.tsx b/web/components/filter-select-users.tsx index 8d2dbbae..7ce73cf8 100644 --- a/web/components/filter-select-users.tsx +++ b/web/components/filter-select-users.tsx @@ -35,7 +35,8 @@ export function FilterSelectUsers(props: { return ( !selectedUsers.map((user) => user.name).includes(user.name) && !ignoreUserIds.includes(user.id) && - user.name.toLowerCase().includes(query.toLowerCase()) + (user.name.toLowerCase().includes(query.toLowerCase()) || + user.username.toLowerCase().includes(query.toLowerCase())) ) }) ) From 3a6d28e2c2b94c26207c5abff126eae50410da4f Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 5 Jul 2022 17:18:37 -0600 Subject: [PATCH 050/519] Bold groups with recent chat activity (#621) * Bold groups with recent chat activity * Cleanup * Cleanup --- common/notification.ts | 3 ++ functions/src/create-notification.ts | 16 +++++- functions/src/index.ts | 3 +- ...nt.ts => on-create-comment-on-contract.ts} | 2 +- functions/src/on-create-comment-on-group.ts | 52 +++++++++++++++++++ functions/src/on-update-group.ts | 1 + web/components/nav/sidebar.tsx | 48 ++++++++++++++--- web/hooks/use-notifications.ts | 10 ++-- web/pages/notifications.tsx | 4 +- 9 files changed, 123 insertions(+), 16 deletions(-) rename functions/src/{on-create-comment.ts => on-create-comment-on-contract.ts} (98%) create mode 100644 functions/src/on-create-comment-on-group.ts diff --git a/common/notification.ts b/common/notification.ts index e90624a4..16444c48 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -22,6 +22,8 @@ export type Notification = { sourceSlug?: string sourceTitle?: string + + isSeenOnHref?: string } export type notification_source_types = | 'contract' @@ -58,3 +60,4 @@ export type notification_reason_types = | 'you_referred_user' | 'user_joined_to_bet_on_your_market' | 'unique_bettors_on_your_contract' + | 'on_group_you_are_member_of' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index b63958f0..45db1c4e 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -17,7 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types } + [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string } } export const createNotification = async ( @@ -72,6 +72,7 @@ export const createNotification = async ( sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, + isSeenOnHref: userToReasonTexts[userId].isSeeOnHref, } await notificationRef.set(removeUndefinedProps(notification)) }) @@ -276,6 +277,17 @@ export const createNotification = async ( } } + const notifyOtherGroupMembersOfComment = async ( + userToReasonTexts: user_to_reason_texts, + userId: string + ) => { + if (shouldGetNotification(userId, userToReasonTexts)) + userToReasonTexts[userId] = { + reason: 'on_group_you_are_member_of', + isSeeOnHref: sourceSlug, + } + } + const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -286,6 +298,8 @@ export const createNotification = async ( await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) } else if (sourceType === 'user' && relatedUserId) { await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) + } else if (sourceType === 'comment' && !sourceContract && relatedUserId) { + await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. diff --git a/functions/src/index.ts b/functions/src/index.ts index e4a30761..d9b7a255 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -10,7 +10,7 @@ export * from './stripe' export * from './create-user' export * from './create-answer' export * from './on-create-bet' -export * from './on-create-comment' +export * from './on-create-comment-on-contract' export * from './on-view' export * from './unsubscribe' export * from './update-metrics' @@ -28,6 +28,7 @@ export * from './on-create-liquidity-provision' export * from './on-update-group' export * from './on-create-group' export * from './on-update-user' +export * from './on-create-comment-on-group' // v2 export * from './health' diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment-on-contract.ts similarity index 98% rename from functions/src/on-create-comment.ts rename to functions/src/on-create-comment-on-contract.ts index 8d52fd46..f7839b44 100644 --- a/functions/src/on-create-comment.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -11,7 +11,7 @@ import { createNotification } from './create-notification' const firestore = admin.firestore() -export const onCreateComment = functions +export const onCreateCommentOnContract = functions .runWith({ secrets: ['MAILGUN_KEY'] }) .firestore.document('contracts/{contractId}/comments/{commentId}') .onCreate(async (change, context) => { diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts new file mode 100644 index 00000000..7217e602 --- /dev/null +++ b/functions/src/on-create-comment-on-group.ts @@ -0,0 +1,52 @@ +import * as functions from 'firebase-functions' +import { Comment } from '../../common/comment' +import * as admin from 'firebase-admin' +import { Group } from '../../common/group' +import { User } from '../../common/user' +import { createNotification } from './create-notification' +const firestore = admin.firestore() + +export const onCreateCommentOnGroup = functions.firestore + .document('groups/{groupId}/comments/{commentId}') + .onCreate(async (change, context) => { + const { eventId } = context + const { groupId } = context.params as { + groupId: string + } + + const comment = change.data() as Comment + const creatorSnapshot = await firestore + .collection('users') + .doc(comment.userId) + .get() + if (!creatorSnapshot.exists) throw new Error('Could not find user') + + const groupSnapshot = await firestore + .collection('groups') + .doc(groupId) + .get() + if (!groupSnapshot.exists) throw new Error('Could not find group') + + const group = groupSnapshot.data() as Group + await firestore.collection('groups').doc(groupId).update({ + mostRecentActivityTime: comment.createdTime, + }) + + await Promise.all( + group.memberIds.map(async (memberId) => { + return await createNotification( + comment.id, + 'comment', + 'created', + creatorSnapshot.data() as User, + eventId, + comment.text, + undefined, + undefined, + memberId, + `/group/${group.slug}`, + `${group.name}` + ) + }) + ) + }) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index bc6f6ab4..feaa6443 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,6 +12,7 @@ export const onUpdateGroup = functions.firestore // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return + // TODO: create notification with isSeeOnHref set to the group's /group/questions url await firestore .collection('groups') diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index ba46bd80..b9449ea0 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' import { ProfileSummary } from './profile-menu' import NotificationsIcon from 'web/components/notifications-icon' -import React from 'react' +import React, { useEffect } from 'react' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' @@ -26,6 +26,8 @@ import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' import { Group } from 'common/group' import { Spacer } from '../layout/spacer' +import { usePreferredNotifications } from 'web/hooks/use-notifications' +import { setNotificationsAsSeen } from 'web/pages/notifications' function getNavigation() { return [ @@ -182,6 +184,7 @@ export default function Sidebar(props: { className?: string }) { const { className } = props const router = useRouter() const currentPage = router.pathname + const user = useUser() const navigationOptions = !user ? signedOutNavigation : getNavigation() const mobileNavigationOptions = !user @@ -217,7 +220,11 @@ export default function Sidebar(props: { className?: string }) { /> )} - <GroupsList currentPage={currentPage} memberItems={memberItems} /> + <GroupsList + currentPage={router.asPath} + memberItems={memberItems} + user={user} + /> </div> {/* Desktop navigation */} @@ -236,14 +243,36 @@ export default function Sidebar(props: { className?: string }) { <div className="h-[1px] bg-gray-300" /> </div> )} - <GroupsList currentPage={currentPage} memberItems={memberItems} /> + <GroupsList + currentPage={router.asPath} + memberItems={memberItems} + user={user} + /> </div> </nav> ) } -function GroupsList(props: { currentPage: string; memberItems: Item[] }) { - const { currentPage, memberItems } = props +function GroupsList(props: { + currentPage: string + memberItems: Item[] + user: User | null | undefined +}) { + const { currentPage, memberItems, user } = props + const preferredNotifications = usePreferredNotifications(user?.id, { + unseenOnly: true, + customHref: '/group/', + }) + + // Set notification as seen if our current page is equal to the isSeenOnHref property + useEffect(() => { + preferredNotifications.forEach((notification) => { + if (notification.isSeenOnHref === currentPage) { + setNotificationsAsSeen([notification]) + } + }) + }, [currentPage, preferredNotifications]) + return ( <> <SidebarItem @@ -256,9 +285,14 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) { <a key={item.href} href={item.href} - className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900" + className={clsx( + 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', + preferredNotifications.some( + (n) => !n.isSeen && n.isSeenOnHref === item.href + ) && 'font-bold' + )} > - <span className="truncate">  {item.name}</span> + <span className="truncate">{item.name}</span> </a> ))} </div> diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index 0a15754d..539573dd 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -83,11 +83,11 @@ export function groupNotifications(notifications: Notification[]) { return notificationGroups } -function usePreferredNotifications( +export function usePreferredNotifications( userId: string | undefined, - options: { unseenOnly: boolean } + options: { unseenOnly: boolean; customHref?: string } ) { - const { unseenOnly } = options + const { unseenOnly, customHref } = options const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null) const [notifications, setNotifications] = useState<Notification[]>([]) const [userAppropriateNotifications, setUserAppropriateNotifications] = @@ -112,9 +112,11 @@ function usePreferredNotifications( const notificationsToShow = getAppropriateNotifications( notifications, privateUser.notificationPreferences + ).filter((n) => + customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref ) setUserAppropriateNotifications(notificationsToShow) - }, [privateUser, notifications]) + }, [privateUser, notifications, customHref]) return userAppropriateNotifications } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 229e8c8d..569f8ef8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -166,7 +166,7 @@ export default function Notifications() { ) } -const setNotificationsAsSeen = (notifications: Notification[]) => { +export const setNotificationsAsSeen = (notifications: Notification[]) => { notifications.forEach((notification) => { if (!notification.isSeen) updateDoc( @@ -758,7 +758,7 @@ function NotificationItem(props: { <div className={clsx( 'bg-white px-2 pt-6 text-sm sm:px-4', - highlighted && 'bg-indigo-200' + highlighted && 'bg-indigo-200 hover:bg-indigo-100' )} > <a href={getSourceUrl()}> From cb25a7752d8552f7dd6ae01483508d435067f0ff Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 5 Jul 2022 16:26:58 -0700 Subject: [PATCH 051/519] Duplicate a question from '...' screen (#622) * Duplicate a question from '...' screen * Remove unused code --- .../contract/contract-info-dialog.tsx | 2 + web/components/copy-contract-button.tsx | 54 +++++++++++++ web/pages/create.tsx | 78 +++++++++++-------- 3 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 web/components/copy-contract-button.tsx diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 12fd8dd9..3e51902b 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -21,6 +21,7 @@ import { Title } from '../title' import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' import { TagsInput } from 'web/components/tags-input' +import { DuplicateContractButton } from '../copy-contract-button' export const contractDetailsButtonClassName = 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' @@ -71,6 +72,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { tweetText={getTweetText(contract, false)} /> <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> + <DuplicateContractButton contract={contract} /> </Row> <div /> diff --git a/web/components/copy-contract-button.tsx b/web/components/copy-contract-button.tsx new file mode 100644 index 00000000..ad378878 --- /dev/null +++ b/web/components/copy-contract-button.tsx @@ -0,0 +1,54 @@ +import { DuplicateIcon } from '@heroicons/react/outline' +import clsx from 'clsx' +import { Contract } from 'common/contract' +import { getMappedValue } from 'common/pseudo-numeric' +import { trackCallback } from 'web/lib/service/analytics' + +export function DuplicateContractButton(props: { + contract: Contract + className?: string +}) { + const { contract, className } = props + + return ( + <a + className={clsx('btn btn-xs flex-nowrap normal-case', className)} + style={{ + backgroundColor: 'white', + border: '2px solid #a78bfa', + // violet-400 + color: '#a78bfa', + }} + href={duplicateContractHref(contract)} + onClick={trackCallback('duplicate market')} + target="_blank" + > + <DuplicateIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> + <div>Duplicate</div> + </a> + ) +} + +// Pass along the Uri to create a new contract +function duplicateContractHref(contract: Contract) { + const params = { + q: contract.question, + closeTime: contract.closeTime || 0, + description: contract.description, + outcomeType: contract.outcomeType, + } as Record<string, any> + + if (contract.outcomeType === 'PSEUDO_NUMERIC') { + params.min = contract.min + params.max = contract.max + params.isLogScale = contract.isLogScale + params.initValue = getMappedValue(contract)(contract.initialProbability) + } + + return ( + `/create?` + + Object.entries(params) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&') + ) +} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 6a5f96ae..95b8e247 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -28,14 +28,32 @@ import { GroupSelector } from 'web/components/groups/group-selector' import { CATEGORIES } from 'common/categories' import { User } from 'common/user' -export default function Create() { - const [question, setQuestion] = useState('') - // get query params: - const router = useRouter() - const { groupId } = router.query as { groupId: string } - useTracking('view create page') - const creator = useUser() +type NewQuestionParams = { + groupId?: string + q: string + type: string + description: string + closeTime: string + outcomeType: string + // Params for PSEUDO_NUMERIC outcomeType + min?: string + max?: string + isLogScale?: string + initValue?: string +} +export default function Create() { + useTracking('view create page') + const router = useRouter() + const params = router.query as NewQuestionParams + // TODO: Not sure why Question is pulled out as its own component; + // Maybe merge into newContract and then we don't need useEffect here. + const [question, setQuestion] = useState('') + useEffect(() => { + setQuestion(params.q ?? '') + }, [params.q]) + + const creator = useUser() useEffect(() => { if (creator === null) router.push('/') }, [creator, router]) @@ -65,11 +83,7 @@ export default function Create() { </div> </form> <Spacer h={6} /> - <NewContract - question={question} - groupId={groupId} - creator={creator} - /> + <NewContract question={question} params={params} creator={creator} /> </div> </div> </Page> @@ -80,20 +94,21 @@ export default function Create() { export function NewContract(props: { creator: User question: string - groupId?: string + params?: NewQuestionParams }) { - const { creator, question, groupId } = props - const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY') + const { creator, question, params } = props + const { groupId, initValue } = params ?? {} + const [outcomeType, setOutcomeType] = useState<outcomeType>( + (params?.outcomeType as outcomeType) ?? 'BINARY' + ) const [initialProb] = useState(50) - const [minString, setMinString] = useState('') - const [maxString, setMaxString] = useState('') - const [isLogScale, setIsLogScale] = useState(false) - const [initialValueString, setInitialValueString] = useState('') + const [minString, setMinString] = useState(params?.min ?? '') + const [maxString, setMaxString] = useState(params?.max ?? '') + const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale) + const [initialValueString, setInitialValueString] = useState(initValue) - const [description, setDescription] = useState('') - // const [tagText, setTagText] = useState<string>(tag ?? '') - // const tags = parseWordsAsTags(tagText) + const [description, setDescription] = useState(params?.description ?? '') useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { @@ -105,18 +120,17 @@ export function NewContract(props: { }, [creator, groupId]) const [ante, _setAnte] = useState(FIXED_ANTE) - // useEffect(() => { - // if (ante === null && creator) { - // const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100 - // setAnte(initialAnte) - // } - // }, [ante, creator]) - - // const [anteError, setAnteError] = useState<string | undefined>() + // If params.closeTime is set, extract out the specified date and time // By default, close the market a week from today const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD') - const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday) - const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>('23:59') + const timeInMs = Number(params?.closeTime ?? 0) + const initDate = timeInMs + ? dayjs(timeInMs).format('YYYY-MM-DD') + : weekFromToday + const initTime = timeInMs ? dayjs(timeInMs).format('HH:mm') : '23:59' + const [closeDate, setCloseDate] = useState<undefined | string>(initDate) + const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>(initTime) + const [marketInfoText, setMarketInfoText] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [selectedGroup, setSelectedGroup] = useState<Group | undefined>( From b71944607b7cd4701ce1a901a08c93c2196dd6b8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 5 Jul 2022 16:48:59 -0700 Subject: [PATCH 052/519] Simplify Tweet text --- .../contract/contract-info-dialog.tsx | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 3e51902b..b5ecea15 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -7,11 +7,7 @@ import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' -import { - contractPath, - contractPool, - getBinaryProbPercent, -} from 'web/lib/firebase/contracts' +import { contractPath, contractPool } from 'web/lib/firebase/contracts' import { LiquidityPanel } from '../liquidity-panel' import { Col } from '../layout/col' import { Modal } from '../layout/modal' @@ -69,7 +65,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { <Row className="justify-start gap-4"> <TweetButton className="self-start" - tweetText={getTweetText(contract, false)} + tweetText={getTweetText(contract)} /> <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> <DuplicateContractButton contract={contract} /> @@ -157,23 +153,13 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { ) } -const getTweetText = (contract: Contract, isCreator: boolean) => { - const { question, creatorName, resolution, outcomeType } = contract - const isBinary = outcomeType === 'BINARY' +const getTweetText = (contract: Contract) => { + const { question, resolution } = contract - const tweetQuestion = isCreator - ? question - : `${question}\nAsked by ${creatorName}.` - const tweetDescription = resolution - ? `Resolved ${resolution}!` - : isBinary - ? `Currently ${getBinaryProbPercent( - contract - )} chance, place your bets here:` - : `Submit your own answer:` + const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' const timeParam = `${Date.now()}`.substring(7) const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` - return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` + return `${question}\n\n${url}${tweetDescription}` } From 6cd8b04bd01f1f9852eff8d056c7fa683a3e34e8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 5 Jul 2022 16:53:00 -0700 Subject: [PATCH 053/519] Nit: Fix spacing --- common/pseudo-numeric.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/pseudo-numeric.ts b/common/pseudo-numeric.ts index 9a322e35..c99e670f 100644 --- a/common/pseudo-numeric.ts +++ b/common/pseudo-numeric.ts @@ -17,7 +17,7 @@ export const getMappedValue = if (isLogScale) { const logValue = p * Math.log10(max - min) - return 10 ** logValue + min + return 10 ** logValue + min } return p * (max - min) + min From 029021b35117a572e16ef8b30eb025cc41daa663 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 5 Jul 2022 17:20:37 -0700 Subject: [PATCH 054/519] Remove Categories from /create --- web/pages/create.tsx | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 95b8e247..df83fb9f 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -25,7 +25,6 @@ import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' -import { CATEGORIES } from 'common/categories' import { User } from 'common/user' type NewQuestionParams = { @@ -137,7 +136,6 @@ export function NewContract(props: { undefined ) const [showGroupSelector, setShowGroupSelector] = useState(true) - const [category, setCategory] = useState<string>('') const closeTime = closeDate ? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf() @@ -210,7 +208,6 @@ export function NewContract(props: { initialValue, isLogScale: (min ?? 0) < 0 ? false : isLogScale, groupId: selectedGroup?.id, - tags: category ? [category] : undefined, }) ) track('create market', { @@ -352,28 +349,6 @@ export function NewContract(props: { </> )} - <div className="form-control max-w-[265px] items-start"> - <label className="label gap-2"> - <span className="mb-1">Category</span> - </label> - - <select - className={clsx( - 'select select-bordered w-full text-sm', - category === '' ? 'font-normal text-gray-500' : '' - )} - value={category} - onChange={(e) => setCategory(e.currentTarget.value ?? '')} - > - <option value={''}>None</option> - {Object.entries(CATEGORIES).map(([id, name]) => ( - <option key={id} value={id}> - {name} - </option> - ))} - </select> - </div> - <div className={'mt-2'}> <GroupSelector selectedGroup={selectedGroup} From a6143c1abb791507fee58d4bce7c5f6f5a966830 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 07:27:21 -0600 Subject: [PATCH 055/519] Always group income --- web/pages/notifications.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 569f8ef8..e20b6028 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -83,16 +83,16 @@ export default function Notifications() { {paginatedNotificationGroups.length === 0 && "You don't have any notifications. Try changing your settings to see more."} {paginatedNotificationGroups.map((notification) => - notification.notifications.length === 1 ? ( - <NotificationItem - notification={notification.notifications[0]} - key={notification.notifications[0].id} - /> - ) : notification.type === 'income' ? ( + notification.type === 'income' ? ( <IncomeNotificationGroupItem notificationGroup={notification} key={notification.groupedById + notification.timePeriod} /> + ) : notification.notifications.length === 1 ? ( + <NotificationItem + notification={notification.notifications[0]} + key={notification.notifications[0].id} + /> ) : ( <NotificationGroupItem notificationGroup={notification} From 83a02c4b20035592067b2bee97de23b4b7838163 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 07:45:47 -0600 Subject: [PATCH 056/519] Small notifications ux improvements --- web/pages/notifications.tsx | 98 +++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index e20b6028..185225e9 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -275,7 +275,9 @@ function IncomeNotificationGroupItem(props: { > <span> {'Daily Income Summary: '} - <span className={'text-primary'}>{formatMoney(totalIncome)}</span> + <span className={'text-primary'}> + {'+' + formatMoney(totalIncome)} + </span> </span> </div> <RelativeTimestamp time={notifications[0].createdTime} /> @@ -291,11 +293,44 @@ function IncomeNotificationGroupItem(props: { .slice(0, numSummaryLines) .map((notification) => { return ( - <NotificationItem - notification={notification} - justSummary={true} + <Row + className={ + 'items-center text-sm text-gray-500 sm:justify-start' + } key={notification.id} - /> + > + <div + className={ + 'line-clamp-1 flex-1 overflow-hidden sm:flex' + } + > + <div className={'flex pl-1 sm:pl-0'}> + <div + className={ + 'inline-flex overflow-hidden text-ellipsis pl-1' + } + > + <div className={'mr-1 text-black'}> + <NotificationTextLabel + contract={null} + defaultText={notification.sourceText ?? ''} + className={'line-clamp-1'} + notification={notification} + justSummary={true} + /> + </div> + <span className={'flex-shrink-0'}> + {getReasonForShowingNotification( + notification, + true + )} + {` on`} + <NotificationLink notification={notification} /> + </span> + </div> + </div> + </div> + </Row> ) })} <div className={'text-sm text-gray-500 hover:underline '}> @@ -640,6 +675,34 @@ function NotificationSettings() { ) } +function NotificationLink(props: { notification: Notification }) { + const { notification } = props + const { + sourceType, + sourceContractTitle, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + sourceTitle, + } = notification + return ( + <a + href={ + sourceContractCreatorUsername + ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` + : sourceType === 'group' && sourceSlug + ? `${groupPath(sourceSlug)}` + : '' + } + className={ + 'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2' + } + > + {sourceContractTitle || sourceTitle} + </a> + ) +} + function NotificationItem(props: { notification: Notification justSummary?: boolean @@ -656,11 +719,9 @@ function NotificationItem(props: { sourceUserUsername, createdTime, sourceText, - sourceContractTitle, sourceContractCreatorUsername, sourceContractSlug, sourceSlug, - sourceTitle, } = notification const [defaultNotificationText, setDefaultNotificationText] = @@ -790,20 +851,7 @@ function NotificationItem(props: { {sourceType && reason && ( <div className={'inline truncate'}> {getReasonForShowingNotification(notification, false)} - <a - href={ - sourceContractCreatorUsername - ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : sourceType === 'group' && sourceSlug - ? `${groupPath(sourceSlug)}` - : '' - } - className={ - 'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2' - } - > - {sourceContractTitle || sourceTitle} - </a> + <NotificationLink notification={notification} /> </div> )} </div> @@ -892,9 +940,7 @@ function NotificationTextLabel(props: { ) } else if (sourceType === 'bonus' && sourceText) { return ( - <span className="text-primary"> - {'+' + formatMoney(parseInt(sourceText))} - </span> + <span className="text-primary">{formatMoney(parseInt(sourceText))}</span> ) } // return default text @@ -931,7 +977,7 @@ function getReasonForShowingNotification( else reasonText = `commented on` break case 'contract': - if (reason === 'you_follow_user') reasonText = 'created a new question' + if (reason === 'you_follow_user') reasonText = 'asked' else if (sourceUpdateType === 'resolved') reasonText = `resolved` else if (sourceUpdateType === 'closed') reasonText = `Please resolve your question` @@ -968,7 +1014,7 @@ function getReasonForShowingNotification( ? `You had ${ parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT } unique bettors on` - : 'You earned Mana for unique bettors:' + : ' for unique bettors' else reasonText = 'You earned your daily manna' break default: From 434b8b9dbe2f29b4ecbdd61aab0e45d23fafa053 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 07:51:32 -0600 Subject: [PATCH 057/519] Just show first names to save space --- web/components/notifications-icon.tsx | 2 +- web/components/user-page.tsx | 5 +++-- web/pages/notifications.tsx | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index ac4d772f..8f45a054 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -19,7 +19,7 @@ export default function NotificationsIcon(props: { className?: string }) { useEffect(() => { if (!privateUser) return - if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 60 * 1000) + if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 65 * 1000) requestBonuses({}).catch((error) => { console.log("couldn't get bonuses:", error.message) }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 07f722d7..c33476aa 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -45,15 +45,16 @@ export function UserLink(props: { username: string showUsername?: boolean className?: string + justFirstName?: boolean }) { - const { name, username, showUsername, className } = props + const { name, username, showUsername, className, justFirstName } = props return ( <SiteLink href={`/${username}`} className={clsx('z-10 truncate', className)} > - {name} + {justFirstName ? name.split(' ')[0] : name} {showUsername && ` (@${username})`} </SiteLink> ) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 185225e9..2c6c2433 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -783,13 +783,12 @@ function NotificationItem(props: { <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> <div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}> <div className={'flex pl-1 sm:pl-0'}> - {sourceType != 'bonus' && ( - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - /> - )} + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-0 flex-shrink-0'} + justFirstName={true} + /> <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> <span className={'flex-shrink-0'}> {sourceType && @@ -845,6 +844,7 @@ function NotificationItem(props: { name={sourceUserName || ''} username={sourceUserUsername || ''} className={'mr-0 flex-shrink-0'} + justFirstName={true} /> )} <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> From 2d1e76eae8cbe22cd2c9af14f6249df696a902f2 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 6 Jul 2022 10:39:19 -0700 Subject: [PATCH 058/519] When duplicating, add the original link in description --- web/components/copy-contract-button.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/components/copy-contract-button.tsx b/web/components/copy-contract-button.tsx index ad378878..fcb3a347 100644 --- a/web/components/copy-contract-button.tsx +++ b/web/components/copy-contract-button.tsx @@ -1,7 +1,9 @@ import { DuplicateIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { Contract } from 'common/contract' +import { ENV_CONFIG } from 'common/envs/constants' import { getMappedValue } from 'common/pseudo-numeric' +import { contractPath } from 'web/lib/firebase/contracts' import { trackCallback } from 'web/lib/service/analytics' export function DuplicateContractButton(props: { @@ -34,7 +36,9 @@ function duplicateContractHref(contract: Contract) { const params = { q: contract.question, closeTime: contract.closeTime || 0, - description: contract.description, + description: + (contract.description ? `${contract.description}\n\n` : '') + + `(Copied from https://${ENV_CONFIG.domain}${contractPath(contract)})`, outcomeType: contract.outcomeType, } as Record<string, any> From de20ee9fb9a4eddfd1049beebe5b2f881ee5322b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 13:30:51 -0600 Subject: [PATCH 059/519] Show tip notifications (#623) * Show tip notifications * Optimizing notifications for mobile * Unused vars * Move income reason logic to income notif * Remove unnecessary icons * Unused vars --- common/antes.ts | 1 + common/notification.ts | 1 + functions/src/create-notification.ts | 18 +- functions/src/get-daily-bonuses.ts | 13 +- functions/src/index.ts | 1 + functions/src/on-create-txn.ts | 68 ++++ web/components/avatar.tsx | 2 +- web/components/feed/feed-comments.tsx | 2 +- web/hooks/use-notifications.ts | 14 +- web/pages/notifications.tsx | 498 +++++++++++++++----------- 10 files changed, 391 insertions(+), 227 deletions(-) create mode 100644 functions/src/on-create-txn.ts diff --git a/common/antes.ts b/common/antes.ts index d4cb2ff9..b3dd990b 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -15,6 +15,7 @@ import { ENV_CONFIG } from './envs/constants' export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100 export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id +export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id export function getCpmmInitialLiquidity( providerId: string, diff --git a/common/notification.ts b/common/notification.ts index 16444c48..da8a045a 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -61,3 +61,4 @@ export type notification_reason_types = | 'user_joined_to_bet_on_your_market' | 'unique_bettors_on_your_contract' | 'on_group_you_are_member_of' + | 'tip_received' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 45db1c4e..49bff5f7 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -66,9 +66,7 @@ export const createNotification = async ( sourceUserAvatarUrl: sourceUser.avatarUrl, sourceText, sourceContractCreatorUsername: sourceContract?.creatorUsername, - // TODO: move away from sourceContractTitle to sourceTitle sourceContractTitle: sourceContract?.question, - // TODO: move away from sourceContractSlug to sourceSlug sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, @@ -278,13 +276,22 @@ export const createNotification = async ( } const notifyOtherGroupMembersOfComment = async ( + userToReasons: user_to_reason_texts, + userId: string + ) => { + if (shouldGetNotification(userId, userToReasons)) + userToReasons[userId] = { + reason: 'on_group_you_are_member_of', + isSeeOnHref: sourceSlug, + } + } + const notifyTippedUserOfNewTip = async ( userToReasonTexts: user_to_reason_texts, userId: string ) => { if (shouldGetNotification(userId, userToReasonTexts)) userToReasonTexts[userId] = { - reason: 'on_group_you_are_member_of', - isSeeOnHref: sourceSlug, + reason: 'tip_received', } } @@ -304,6 +311,7 @@ export const createNotification = async ( // The following functions need sourceContract to be defined. if (!sourceContract) return userToReasonTexts + if ( sourceType === 'comment' || sourceType === 'answer' || @@ -338,6 +346,8 @@ export const createNotification = async ( userToReasonTexts, sourceContract.creatorId ) + } else if (sourceType === 'tip' && relatedUserId) { + await notifyTippedUserOfNewTip(userToReasonTexts, relatedUserId) } return userToReasonTexts } diff --git a/functions/src/get-daily-bonuses.ts b/functions/src/get-daily-bonuses.ts index c5c1a1b3..017c32fc 100644 --- a/functions/src/get-daily-bonuses.ts +++ b/functions/src/get-daily-bonuses.ts @@ -1,11 +1,14 @@ import { APIError, newEndpoint } from './api' -import { log } from './utils' +import { isProd, log } from './utils' import * as admin from 'firebase-admin' import { PrivateUser } from '../../common/lib/user' import { uniq } from 'lodash' import { Bet } from '../../common/lib/bet' const firestore = admin.firestore() -import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' +import { + DEV_HOUSE_LIQUIDITY_PROVIDER_ID, + HOUSE_LIQUIDITY_PROVIDER_ID, +} from '../../common/antes' import { runTxn, TxnData } from './transact' import { createNotification } from './create-notification' import { User } from '../../common/lib/user' @@ -38,9 +41,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => { } } ) - // TODO: switch to prod id - // const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account - const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account + const fromUserId = isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID const fromSnap = await firestore.doc(`users/${fromUserId}`).get() if (!fromSnap.exists) throw new APIError(400, 'From user not found.') const fromUser = fromSnap.data() as User diff --git a/functions/src/index.ts b/functions/src/index.ts index d9b7a255..8d1756f2 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -29,6 +29,7 @@ export * from './on-update-group' export * from './on-create-group' export * from './on-update-user' export * from './on-create-comment-on-group' +export * from './on-create-txn' // v2 export * from './health' diff --git a/functions/src/on-create-txn.ts b/functions/src/on-create-txn.ts new file mode 100644 index 00000000..d877ecac --- /dev/null +++ b/functions/src/on-create-txn.ts @@ -0,0 +1,68 @@ +import * as functions from 'firebase-functions' +import { Txn } from 'common/txn' +import { getContract, getUser, log } from './utils' +import { createNotification } from './create-notification' +import * as admin from 'firebase-admin' +import { Comment } from 'common/comment' + +const firestore = admin.firestore() + +export const onCreateTxn = functions.firestore + .document('txns/{txnId}') + .onCreate(async (change, context) => { + const txn = change.data() as Txn + const { eventId } = context + + if (txn.category === 'TIP') { + await handleTipTxn(txn, eventId) + } + }) + +async function handleTipTxn(txn: Txn, eventId: string) { + // get user sending and receiving tip + const [sender, receiver] = await Promise.all([ + getUser(txn.fromId), + getUser(txn.toId), + ]) + if (!sender || !receiver) { + log('Could not find corresponding users') + return + } + + if (!txn.data?.contractId || !txn.data?.commentId) { + log('No contractId or comment id in tip txn.data') + return + } + + const contract = await getContract(txn.data.contractId) + if (!contract) { + log('Could not find contract') + return + } + + const commentSnapshot = await firestore + .collection('contracts') + .doc(contract.id) + .collection('comments') + .doc(txn.data.commentId) + .get() + if (!commentSnapshot.exists) { + log('Could not find comment') + return + } + const comment = commentSnapshot.data() as Comment + + await createNotification( + txn.id, + 'tip', + 'created', + sender, + eventId, + txn.amount.toString(), + contract, + 'comment', + receiver.id, + txn.data?.commentId, + comment.text + ) +} diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index e6506c03..53257deb 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -53,7 +53,7 @@ export function EmptyAvatar(props: { size?: number; multi?: boolean }) { return ( <div - className={`flex h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`} + className={`flex flex-shrink-0 h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`} > <Icon className={`h-${insize} w-${insize} text-gray-500`} aria-hidden /> </div> diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index ed02128e..c327d8af 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -224,7 +224,7 @@ export function FeedComment(props: { return ( <Row className={clsx( - 'flex space-x-1.5 transition-all duration-1000 sm:space-x-3', + 'flex space-x-1.5 sm:space-x-3', highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : '' )} > diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index 539573dd..98b0f2fd 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -39,17 +39,19 @@ export function groupNotifications(notifications: Notification[]) { ) Object.keys(notificationGroupsByDay).forEach((day) => { const notificationsGroupedByDay = notificationGroupsByDay[day] - const bonusNotifications = notificationsGroupedByDay.filter( - (notification) => notification.sourceType === 'bonus' + const incomeNotifications = notificationsGroupedByDay.filter( + (notification) => + notification.sourceType === 'bonus' || notification.sourceType === 'tip' ) const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter( - (notification) => notification.sourceType !== 'bonus' + (notification) => + notification.sourceType !== 'bonus' && notification.sourceType !== 'tip' ) - if (bonusNotifications.length > 0) { + if (incomeNotifications.length > 0) { notificationGroups = notificationGroups.concat({ - notifications: bonusNotifications, + notifications: incomeNotifications, groupedById: 'income' + day, - isSeen: bonusNotifications[0].isSeen, + isSeen: incomeNotifications[0].isSeen, timePeriod: day, type: 'income', }) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2c6c2433..45ca234a 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,7 +1,7 @@ import { Tabs } from 'web/components/layout/tabs' import { useUser } from 'web/hooks/use-user' import React, { useEffect, useState } from 'react' -import { Notification } from 'common/notification' +import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' @@ -34,10 +34,10 @@ import toast from 'react-hot-toast' import { formatMoney } from 'common/util/format' import { groupPath } from 'web/lib/firebase/groups' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' -import { groupBy } from 'lodash' +import { groupBy, sum, uniq } from 'lodash' export const NOTIFICATIONS_PER_PAGE = 30 -export const HIGHLIGHT_DURATION = 30 * 1000 +const MULTIPLE_USERS_KEY = 'multipleUsers' export default function Notifications() { const user = useUser() @@ -187,16 +187,12 @@ function IncomeNotificationGroupItem(props: { const { notificationGroup, className } = props const { notifications } = notificationGroup const numSummaryLines = 3 - const [expanded, setExpanded] = useState(false) - const [highlighted, setHighlighted] = useState(false) + const [highlighted, setHighlighted] = useState( + notifications.some((n) => !n.isSeen) + ) + useEffect(() => { - if (notifications.some((n) => !n.isSeen)) { - setHighlighted(true) - setTimeout(() => { - setHighlighted(false) - }, HIGHLIGHT_DURATION) - } setNotificationsAsSeen(notifications) }, [notifications]) @@ -204,51 +200,62 @@ function IncomeNotificationGroupItem(props: { if (expanded) setHighlighted(false) }, [expanded]) - const totalIncome = notifications.reduce( - (acc, notification) => - acc + - (notification.sourceType && - notification.sourceText && - notification.sourceType === 'bonus' - ? parseInt(notification.sourceText) - : 0), - 0 + const totalIncome = sum( + notifications.map((notification) => + notification.sourceText ? parseInt(notification.sourceText) : 0 + ) ) - // loop through the contracts and combine the notification items into one - function combineNotificationsByAddingSourceTextsAndReturningTheRest( + // Loop through the contracts and combine the notification items into one + function combineNotificationsByAddingNumericSourceTexts( notifications: Notification[] ) { const newNotifications = [] - const groupedNotificationsByContractId = groupBy( + const groupedNotificationsBySourceType = groupBy( notifications, - (notification) => { - return notification.sourceContractId - } + (n) => n.sourceType ) - for (const contractId in groupedNotificationsByContractId) { - const notificationsForContractId = - groupedNotificationsByContractId[contractId] - let sum = 0 - notificationsForContractId.forEach( - (notification) => - notification.sourceText && - (sum = parseInt(notification.sourceText) + sum) + for (const sourceType in groupedNotificationsBySourceType) { + const groupedNotificationsByContractId = groupBy( + groupedNotificationsBySourceType[sourceType], + (notification) => { + return notification.sourceContractId + } ) + for (const contractId in groupedNotificationsByContractId) { + const notificationsForContractId = + groupedNotificationsByContractId[contractId] + if (notificationsForContractId.length === 1) { + newNotifications.push(notificationsForContractId[0]) + continue + } + let sum = 0 + notificationsForContractId.forEach( + (notification) => + notification.sourceText && + (sum = parseInt(notification.sourceText) + sum) + ) + const uniqueUsers = uniq( + notificationsForContractId.map((notification) => { + return notification.sourceUserUsername + }) + ) - const newNotification = - notificationsForContractId.length === 1 - ? notificationsForContractId[0] - : { - ...notificationsForContractId[0], - sourceText: sum.toString(), - } - newNotifications.push(newNotification) + const newNotification = { + ...notificationsForContractId[0], + sourceText: sum.toString(), + sourceUserUsername: + uniqueUsers.length > 1 + ? MULTIPLE_USERS_KEY + : notificationsForContractId[0].sourceType, + } + newNotifications.push(newNotification) + } } return newNotifications } const combinedNotifs = - combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications) + combineNotificationsByAddingNumericSourceTexts(notifications) return ( <div @@ -286,53 +293,23 @@ function IncomeNotificationGroupItem(props: { <div> <div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}> {' '} - <div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}> + <div + className={clsx( + 'mt-1 ml-1 gap-1 whitespace-pre-line', + !expanded ? 'line-clamp-4' : '' + )} + > {!expanded ? ( <> {combinedNotifs .slice(0, numSummaryLines) - .map((notification) => { - return ( - <Row - className={ - 'items-center text-sm text-gray-500 sm:justify-start' - } - key={notification.id} - > - <div - className={ - 'line-clamp-1 flex-1 overflow-hidden sm:flex' - } - > - <div className={'flex pl-1 sm:pl-0'}> - <div - className={ - 'inline-flex overflow-hidden text-ellipsis pl-1' - } - > - <div className={'mr-1 text-black'}> - <NotificationTextLabel - contract={null} - defaultText={notification.sourceText ?? ''} - className={'line-clamp-1'} - notification={notification} - justSummary={true} - /> - </div> - <span className={'flex-shrink-0'}> - {getReasonForShowingNotification( - notification, - true - )} - {` on`} - <NotificationLink notification={notification} /> - </span> - </div> - </div> - </div> - </Row> - ) - })} + .map((notification) => ( + <IncomeNotificationItem + notification={notification} + justSummary={true} + key={notification.id} + /> + ))} <div className={'text-sm text-gray-500 hover:underline '}> {combinedNotifs.length - numSummaryLines > 0 ? 'And ' + @@ -344,7 +321,7 @@ function IncomeNotificationGroupItem(props: { ) : ( <> {combinedNotifs.map((notification) => ( - <NotificationItem + <IncomeNotificationItem notification={notification} key={notification.id} justSummary={false} @@ -361,28 +338,130 @@ function IncomeNotificationGroupItem(props: { ) } +function IncomeNotificationItem(props: { + notification: Notification + justSummary?: boolean +}) { + const { notification, justSummary } = props + const { + sourceType, + sourceUserName, + reason, + sourceUserUsername, + createdTime, + } = notification + const [highlighted] = useState(!notification.isSeen) + + useEffect(() => { + setNotificationsAsSeen([notification]) + }, [notification]) + + function getReasonForShowingIncomeNotification(simple: boolean) { + const { sourceText } = notification + let reasonText = '' + if (sourceType === 'bonus' && sourceText) { + reasonText = !simple + ? `bonus for ${ + parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT + } unique bettors` + : ' bonus for unique bettors on' + } else if (sourceType === 'tip') { + reasonText = !simple ? `tipped you` : `in tips on` + } + return <span className={'flex-shrink-0'}>{reasonText}</span> + } + + if (justSummary) { + return ( + <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> + <div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}> + <div className={'flex pl-1 sm:pl-0'}> + <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> + <div className={'mr-1 text-black'}> + <NotificationTextLabel + contract={null} + defaultText={notification.sourceText ?? ''} + className={'line-clamp-1'} + notification={notification} + justSummary={true} + /> + </div> + <span className={'flex truncate'}> + {getReasonForShowingIncomeNotification(true)} + <NotificationLink notification={notification} /> + </span> + </div> + </div> + </div> + </Row> + ) + } + + return ( + <div + className={clsx( + 'bg-white px-2 pt-6 text-sm sm:px-4', + highlighted && 'bg-indigo-200 hover:bg-indigo-100' + )} + > + <a href={getSourceUrl(notification)}> + <Row className={'items-center text-gray-500 sm:justify-start'}> + <div className={'flex max-w-xl shrink '}> + {sourceType && reason && ( + <div className={'inline'}> + <span className={'mr-1'}> + <NotificationTextLabel + contract={null} + defaultText={notification.sourceText ?? ''} + notification={notification} + /> + </span> + + {sourceType != 'bonus' && + (sourceUserUsername === MULTIPLE_USERS_KEY ? ( + <span className={'mr-1 truncate'}>Multiple users</span> + ) : ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-1 flex-shrink-0'} + justFirstName={true} + /> + ))} + </div> + )} + {getReasonForShowingIncomeNotification(false)} + <span className={'ml-1 flex hidden sm:inline-block'}> + on + <NotificationLink notification={notification} /> + </span> + <RelativeTimestamp time={createdTime} /> + </div> + </Row> + <span className={'flex truncate text-gray-500 sm:hidden'}> + on + <NotificationLink notification={notification} /> + </span> + <div className={'mt-4 border-b border-gray-300'} /> + </a> + </div> + ) +} + function NotificationGroupItem(props: { notificationGroup: NotificationGroup className?: string }) { const { notificationGroup, className } = props const { notifications } = notificationGroup - const { - sourceContractTitle, - sourceContractSlug, - sourceContractCreatorUsername, - } = notifications[0] + const { sourceContractTitle } = notifications[0] const numSummaryLines = 3 const [expanded, setExpanded] = useState(false) - const [highlighted, setHighlighted] = useState(false) + const [highlighted, setHighlighted] = useState( + notifications.some((n) => !n.isSeen) + ) useEffect(() => { - if (notifications.some((n) => !n.isSeen)) { - setHighlighted(true) - setTimeout(() => { - setHighlighted(false) - }, HIGHLIGHT_DURATION) - } setNotificationsAsSeen(notifications) }, [notifications]) @@ -408,27 +487,18 @@ function NotificationGroupItem(props: { )} <Row className={'items-center text-gray-500 sm:justify-start'}> <EmptyAvatar multi /> - <div className={'flex-1 overflow-hidden pl-2 sm:flex'}> + <div className={'flex truncate pl-2'}> <div onClick={() => setExpanded(!expanded)} - className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} + className={' flex cursor-pointer truncate pl-1 sm:pl-0'} > {sourceContractTitle ? ( - <span> - {'Activity on '} - <a - href={ - sourceContractCreatorUsername - ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : '' - } - className={ - 'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2' - } - > - {sourceContractTitle} - </a> - </span> + <> + <span className={'flex-shrink-0'}>{'Activity on '}</span> + <span className={'truncate'}> + <NotificationLink notification={notifications[0]} /> + </span> + </> ) : ( 'Other activity' )} @@ -439,7 +509,13 @@ function NotificationGroupItem(props: { <div> <div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}> {' '} - <div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}> + <div + className={clsx( + 'mt-1 ml-1 gap-1 whitespace-pre-line', + !expanded ? 'line-clamp-4' : '' + )} + > + {' '} {!expanded ? ( <> {notifications.slice(0, numSummaryLines).map((notification) => { @@ -466,6 +542,7 @@ function NotificationGroupItem(props: { notification={notification} key={notification.id} justSummary={false} + hideTitle={true} /> ))} </> @@ -695,7 +772,7 @@ function NotificationLink(props: { notification: Notification }) { : '' } className={ - 'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2' + 'ml-1 inline max-w-xs truncate font-bold text-gray-500 hover:underline hover:decoration-indigo-400 hover:decoration-2 sm:max-w-sm' } > {sourceContractTitle || sourceTitle} @@ -703,11 +780,54 @@ function NotificationLink(props: { notification: Notification }) { ) } +function getSourceUrl(notification: Notification) { + const { + sourceType, + sourceId, + sourceUserUsername, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + } = notification + if (sourceType === 'follow') return `/${sourceUserUsername}` + if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + if ( + sourceContractCreatorUsername && + sourceContractSlug && + sourceType === 'user' + ) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}` + if (sourceType === 'tip') + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` + if (sourceContractCreatorUsername && sourceContractSlug) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( + sourceId ?? '', + sourceType + )}` +} + +function getSourceIdForLinkComponent( + sourceId: string, + sourceType?: notification_source_types +) { + switch (sourceType) { + case 'answer': + return `answer-${sourceId}` + case 'comment': + return sourceId + case 'contract': + return '' + default: + return sourceId + } +} + function NotificationItem(props: { notification: Notification justSummary?: boolean + hideTitle?: boolean }) { - const { notification, justSummary } = props + const { notification, justSummary, hideTitle } = props const { sourceType, sourceId, @@ -721,7 +841,6 @@ function NotificationItem(props: { sourceText, sourceContractCreatorUsername, sourceContractSlug, - sourceSlug, } = notification const [defaultNotificationText, setDefaultNotificationText] = @@ -736,48 +855,12 @@ function NotificationItem(props: { } }, [reasonText, sourceText]) - const [highlighted, setHighlighted] = useState(false) - useEffect(() => { - if (!notification.isSeen) { - setHighlighted(true) - setTimeout(() => { - setHighlighted(false) - }, HIGHLIGHT_DURATION) - } - }, [notification.isSeen]) + const [highlighted] = useState(!notification.isSeen) useEffect(() => { setNotificationsAsSeen([notification]) }, [notification]) - function getSourceUrl() { - if (sourceType === 'follow') return `/${sourceUserUsername}` - if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` - if ( - sourceContractCreatorUsername && - sourceContractSlug && - sourceType === 'user' - ) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}` - if (sourceContractCreatorUsername && sourceContractSlug) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( - sourceId ?? '' - )}` - } - - function getSourceIdForLinkComponent(sourceId: string) { - switch (sourceType) { - case 'answer': - return `answer-${sourceId}` - case 'comment': - return sourceId - case 'contract': - return '' - default: - return sourceId - } - } - if (justSummary) { return ( <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> @@ -793,10 +876,7 @@ function NotificationItem(props: { <span className={'flex-shrink-0'}> {sourceType && reason && - getReasonForShowingNotification(notification, true).replace( - ' on', - '' - )} + getReasonForShowingNotification(notification, true, true)} </span> <div className={'ml-1 text-black'}> <NotificationTextLabel @@ -821,25 +901,21 @@ function NotificationItem(props: { highlighted && 'bg-indigo-200 hover:bg-indigo-100' )} > - <a href={getSourceUrl()}> + <a href={getSourceUrl(notification)}> <Row className={'items-center text-gray-500 sm:justify-start'}> - {sourceType != 'bonus' ? ( - <Avatar - avatarUrl={sourceUserAvatarUrl} - size={'sm'} - className={'mr-2'} - username={sourceUserName} - /> - ) : ( - <TrendingUpIcon className={'text-primary h-7 w-7'} /> - )} + <Avatar + avatarUrl={sourceUserAvatarUrl} + size={'sm'} + className={'mr-2'} + username={sourceUserName} + /> <div className={'flex-1 overflow-hidden sm:flex'}> <div className={ 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' } > - {sourceType != 'bonus' && sourceUpdateType != 'closed' && ( + {sourceUpdateType != 'closed' && ( <UserLink name={sourceUserName || ''} username={sourceUserUsername || ''} @@ -847,26 +923,30 @@ function NotificationItem(props: { justFirstName={true} /> )} - <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> - {sourceType && reason && ( - <div className={'inline truncate'}> - {getReasonForShowingNotification(notification, false)} + {sourceType && reason && ( + <div className={'inline flex truncate'}> + <span className={'ml-1 flex-shrink-0'}> + {getReasonForShowingNotification(notification, false, true)} + </span> + {!hideTitle && ( <NotificationLink notification={notification} /> - </div> - )} - </div> + )} + </div> + )} + {sourceId && + sourceContractSlug && + sourceContractCreatorUsername ? ( + <CopyLinkDateTimeComponent + prefix={sourceContractCreatorUsername} + slug={sourceContractSlug} + createdTime={createdTime} + elementId={getSourceIdForLinkComponent(sourceId)} + className={'-mx-1 inline-flex sm:inline-block'} + /> + ) : ( + <RelativeTimestamp time={createdTime} /> + )} </div> - {sourceId && sourceContractSlug && sourceContractCreatorUsername ? ( - <CopyLinkDateTimeComponent - prefix={sourceContractCreatorUsername} - slug={sourceContractSlug} - createdTime={createdTime} - elementId={getSourceIdForLinkComponent(sourceId)} - className={'-mx-1 inline-flex sm:inline-block'} - /> - ) : ( - <RelativeTimestamp time={createdTime} /> - )} </div> </Row> <div className={'mt-1 ml-1 md:text-base'}> @@ -938,9 +1018,11 @@ function NotificationTextLabel(props: { return ( <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span> ) - } else if (sourceType === 'bonus' && sourceText) { + } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) { return ( - <span className="text-primary">{formatMoney(parseInt(sourceText))}</span> + <span className="text-primary"> + {'+' + formatMoney(parseInt(sourceText))} + </span> ) } // return default text @@ -953,19 +1035,19 @@ function NotificationTextLabel(props: { function getReasonForShowingNotification( notification: Notification, - simple?: boolean + simple?: boolean, + replaceOn?: boolean ) { - const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } = - notification + const { sourceType, sourceUpdateType, reason, sourceSlug } = notification let reasonText: string switch (sourceType) { case 'comment': if (reason === 'reply_to_users_answer') - reasonText = !simple ? 'replied to your answer on' : 'replied' + reasonText = !simple ? 'replied to you on' : 'replied' else if (reason === 'tagged_user') - reasonText = !simple ? 'tagged you in a comment on' : 'tagged you' + reasonText = !simple ? 'tagged you on' : 'tagged you' else if (reason === 'reply_to_users_comment') - reasonText = !simple ? 'replied to your comment on' : 'replied' + reasonText = !simple ? 'replied to you on' : 'replied' else if (reason === 'on_users_contract') reasonText = !simple ? `commented on your question` : 'commented' else if (reason === 'on_contract_with_users_comment') @@ -973,7 +1055,7 @@ function getReasonForShowingNotification( else if (reason === 'on_contract_with_users_answer') reasonText = `commented on` else if (reason === 'on_contract_with_users_shares_in') - reasonText = `commented` + reasonText = `commented on` else reasonText = `commented on` break case 'contract': @@ -1008,17 +1090,13 @@ function getReasonForShowingNotification( else if (sourceSlug) reasonText = 'joined because you shared' else reasonText = 'joined because of you' break - case 'bonus': - if (reason === 'unique_bettors_on_your_contract' && sourceText) - reasonText = !simple - ? `You had ${ - parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT - } unique bettors on` - : ' for unique bettors' - else reasonText = 'You earned your daily manna' - break default: reasonText = '' } - return reasonText + + return ( + <span className={'flex-shrink-0'}> + {replaceOn ? reasonText.replace(' on', '') : reasonText} + </span> + ) } From 54b4f97a84a61b9f43e8f658a69f3cbb97e422c9 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 13:45:31 -0600 Subject: [PATCH 060/519] Move timestamp to same line --- web/pages/notifications.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 45ca234a..54dbdd09 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -275,7 +275,7 @@ function IncomeNotificationGroupItem(props: { )} <Row className={'items-center text-gray-500 sm:justify-start'}> <TrendingUpIcon className={'text-primary h-7 w-7'} /> - <div className={'flex-1 overflow-hidden pl-2 sm:flex'}> + <div className={'flex truncate'}> <div onClick={() => setExpanded(!expanded)} className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} @@ -286,8 +286,8 @@ function IncomeNotificationGroupItem(props: { {'+' + formatMoney(totalIncome)} </span> </span> + <RelativeTimestamp time={notifications[0].createdTime} /> </div> - <RelativeTimestamp time={notifications[0].createdTime} /> </div> </Row> <div> From e969540c72dab78770df9941e3af12adc0941106 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 15:06:41 -0600 Subject: [PATCH 061/519] Slight notifications refactor --- web/pages/notifications.tsx | 726 ++++++++++++++++++------------------ 1 file changed, 363 insertions(+), 363 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 54dbdd09..382505e2 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -166,20 +166,6 @@ export default function Notifications() { ) } -export const setNotificationsAsSeen = (notifications: Notification[]) => { - notifications.forEach((notification) => { - if (!notification.isSeen) - updateDoc( - doc(db, `users/${notification.userId}/notifications/`, notification.id), - { - isSeen: true, - viewTime: new Date(), - } - ) - }) - return notifications -} - function IncomeNotificationGroupItem(props: { notificationGroup: NotificationGroup className?: string @@ -556,6 +542,369 @@ function NotificationGroupItem(props: { ) } +function NotificationItem(props: { + notification: Notification + justSummary?: boolean + hideTitle?: boolean +}) { + const { notification, justSummary, hideTitle } = props + const { + sourceType, + sourceId, + sourceUserName, + sourceUserAvatarUrl, + sourceUpdateType, + reasonText, + reason, + sourceUserUsername, + createdTime, + sourceText, + sourceContractCreatorUsername, + sourceContractSlug, + } = notification + + const [defaultNotificationText, setDefaultNotificationText] = + useState<string>('') + + useEffect(() => { + if (sourceText) { + setDefaultNotificationText(sourceText) + } else if (reasonText) { + // Handle arbitrary notifications with reason text here. + setDefaultNotificationText(reasonText) + } + }, [reasonText, sourceText]) + + const [highlighted] = useState(!notification.isSeen) + + useEffect(() => { + setNotificationsAsSeen([notification]) + }, [notification]) + + if (justSummary) { + return ( + <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> + <div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}> + <div className={'flex pl-1 sm:pl-0'}> + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-0 flex-shrink-0'} + justFirstName={true} + /> + <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> + <span className={'flex-shrink-0'}> + {sourceType && + reason && + getReasonForShowingNotification(notification, true, true)} + </span> + <div className={'ml-1 text-black'}> + <NotificationTextLabel + contract={null} + defaultText={defaultNotificationText} + className={'line-clamp-1'} + notification={notification} + justSummary={true} + /> + </div> + </div> + </div> + </div> + </Row> + ) + } + + return ( + <div + className={clsx( + 'bg-white px-2 pt-6 text-sm sm:px-4', + highlighted && 'bg-indigo-200 hover:bg-indigo-100' + )} + > + <a href={getSourceUrl(notification)}> + <Row className={'items-center text-gray-500 sm:justify-start'}> + <Avatar + avatarUrl={sourceUserAvatarUrl} + size={'sm'} + className={'mr-2'} + username={sourceUserName} + /> + <div className={'flex-1 overflow-hidden sm:flex'}> + <div + className={ + 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' + } + > + {sourceUpdateType != 'closed' && ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-0 flex-shrink-0'} + justFirstName={true} + /> + )} + {sourceType && reason && ( + <div className={'inline flex truncate'}> + <span className={'ml-1 flex-shrink-0'}> + {getReasonForShowingNotification(notification, false, true)} + </span> + {!hideTitle && ( + <NotificationLink notification={notification} /> + )} + </div> + )} + {sourceId && + sourceContractSlug && + sourceContractCreatorUsername ? ( + <CopyLinkDateTimeComponent + prefix={sourceContractCreatorUsername} + slug={sourceContractSlug} + createdTime={createdTime} + elementId={getSourceIdForLinkComponent(sourceId)} + className={'-mx-1 inline-flex sm:inline-block'} + /> + ) : ( + <RelativeTimestamp time={createdTime} /> + )} + </div> + </div> + </Row> + <div className={'mt-1 ml-1 md:text-base'}> + <NotificationTextLabel + contract={null} + defaultText={defaultNotificationText} + notification={notification} + /> + </div> + + <div className={'mt-6 border-b border-gray-300'} /> + </a> + </div> + ) +} + +export const setNotificationsAsSeen = (notifications: Notification[]) => { + notifications.forEach((notification) => { + if (!notification.isSeen) + updateDoc( + doc(db, `users/${notification.userId}/notifications/`, notification.id), + { + isSeen: true, + viewTime: new Date(), + } + ) + }) + return notifications +} + +function NotificationLink(props: { notification: Notification }) { + const { notification } = props + const { + sourceType, + sourceContractTitle, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + sourceTitle, + } = notification + return ( + <a + href={ + sourceContractCreatorUsername + ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` + : sourceType === 'group' && sourceSlug + ? `${groupPath(sourceSlug)}` + : '' + } + className={ + 'ml-1 inline max-w-xs truncate font-bold text-gray-500 hover:underline hover:decoration-indigo-400 hover:decoration-2 sm:max-w-sm' + } + > + {sourceContractTitle || sourceTitle} + </a> + ) +} + +function getSourceUrl(notification: Notification) { + const { + sourceType, + sourceId, + sourceUserUsername, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + } = notification + if (sourceType === 'follow') return `/${sourceUserUsername}` + if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + if ( + sourceContractCreatorUsername && + sourceContractSlug && + sourceType === 'user' + ) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}` + if (sourceType === 'tip') + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` + if (sourceContractCreatorUsername && sourceContractSlug) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( + sourceId ?? '', + sourceType + )}` +} + +function getSourceIdForLinkComponent( + sourceId: string, + sourceType?: notification_source_types +) { + switch (sourceType) { + case 'answer': + return `answer-${sourceId}` + case 'comment': + return sourceId + case 'contract': + return '' + default: + return sourceId + } +} + +function NotificationTextLabel(props: { + defaultText: string + contract?: Contract | null + notification: Notification + className?: string + justSummary?: boolean +}) { + const { contract, className, defaultText, notification, justSummary } = props + const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } = + notification + if (sourceType === 'contract') { + if (justSummary) + return <span>{contract?.question || sourceContractTitle}</span> + if (!sourceText) return <div /> + // Resolved contracts + if (sourceType === 'contract' && sourceUpdateType === 'resolved') { + { + if (sourceText === 'YES' || sourceText == 'NO') { + return <BinaryOutcomeLabel outcome={sourceText as any} /> + } + if (sourceText.includes('%')) + return ( + <ProbPercentLabel prob={parseFloat(sourceText.replace('%', ''))} /> + ) + if (sourceText === 'CANCEL') return <CancelLabel /> + if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> + } + } + // Close date will be a number - it looks better without it + if (sourceUpdateType === 'closed') { + return <div /> + } + // Updated contracts + // Description will be in default text + if (parseInt(sourceText) > 0) { + return ( + <span> + Updated close time: {new Date(parseInt(sourceText)).toLocaleString()} + </span> + ) + } + } else if (sourceType === 'user' && sourceText) { + return ( + <span> + As a thank you, we sent you{' '} + <span className="text-primary"> + {formatMoney(parseInt(sourceText))} + </span> + ! + </span> + ) + } else if (sourceType === 'liquidity' && sourceText) { + return ( + <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span> + ) + } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) { + return ( + <span className="text-primary"> + {'+' + formatMoney(parseInt(sourceText))} + </span> + ) + } + // return default text + return ( + <div className={className ? className : 'line-clamp-4 whitespace-pre-line'}> + <Linkify text={defaultText} /> + </div> + ) +} + +function getReasonForShowingNotification( + notification: Notification, + simple?: boolean, + replaceOn?: boolean +) { + const { sourceType, sourceUpdateType, reason, sourceSlug } = notification + let reasonText: string + switch (sourceType) { + case 'comment': + if (reason === 'reply_to_users_answer') + reasonText = !simple ? 'replied to you on' : 'replied' + else if (reason === 'tagged_user') + reasonText = !simple ? 'tagged you on' : 'tagged you' + else if (reason === 'reply_to_users_comment') + reasonText = !simple ? 'replied to you on' : 'replied' + else if (reason === 'on_users_contract') + reasonText = !simple ? `commented on your question` : 'commented' + else if (reason === 'on_contract_with_users_comment') + reasonText = `commented on` + else if (reason === 'on_contract_with_users_answer') + reasonText = `commented on` + else if (reason === 'on_contract_with_users_shares_in') + reasonText = `commented on` + else reasonText = `commented on` + break + case 'contract': + if (reason === 'you_follow_user') reasonText = 'asked' + else if (sourceUpdateType === 'resolved') reasonText = `resolved` + else if (sourceUpdateType === 'closed') + reasonText = `Please resolve your question` + else reasonText = `updated` + break + case 'answer': + if (reason === 'on_users_contract') reasonText = `answered your question ` + else if (reason === 'on_contract_with_users_comment') + reasonText = `answered` + else if (reason === 'on_contract_with_users_answer') + reasonText = `answered` + else if (reason === 'on_contract_with_users_shares_in') + reasonText = `answered` + else reasonText = `answered` + break + case 'follow': + reasonText = 'followed you' + break + case 'liquidity': + reasonText = 'added liquidity to your question' + break + case 'group': + reasonText = 'added you to the group' + break + case 'user': + if (sourceSlug && reason === 'user_joined_to_bet_on_your_market') + reasonText = 'joined to bet on your market' + else if (sourceSlug) reasonText = 'joined because you shared' + else reasonText = 'joined because of you' + break + default: + reasonText = '' + } + + return ( + <span className={'flex-shrink-0'}> + {replaceOn ? reasonText.replace(' on', '') : reasonText} + </span> + ) +} + // TODO: where should we put referral bonus notifications? function NotificationSettings() { const user = useUser() @@ -751,352 +1100,3 @@ function NotificationSettings() { </div> ) } - -function NotificationLink(props: { notification: Notification }) { - const { notification } = props - const { - sourceType, - sourceContractTitle, - sourceContractCreatorUsername, - sourceContractSlug, - sourceSlug, - sourceTitle, - } = notification - return ( - <a - href={ - sourceContractCreatorUsername - ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : sourceType === 'group' && sourceSlug - ? `${groupPath(sourceSlug)}` - : '' - } - className={ - 'ml-1 inline max-w-xs truncate font-bold text-gray-500 hover:underline hover:decoration-indigo-400 hover:decoration-2 sm:max-w-sm' - } - > - {sourceContractTitle || sourceTitle} - </a> - ) -} - -function getSourceUrl(notification: Notification) { - const { - sourceType, - sourceId, - sourceUserUsername, - sourceContractCreatorUsername, - sourceContractSlug, - sourceSlug, - } = notification - if (sourceType === 'follow') return `/${sourceUserUsername}` - if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` - if ( - sourceContractCreatorUsername && - sourceContractSlug && - sourceType === 'user' - ) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}` - if (sourceType === 'tip') - return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` - if (sourceContractCreatorUsername && sourceContractSlug) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( - sourceId ?? '', - sourceType - )}` -} - -function getSourceIdForLinkComponent( - sourceId: string, - sourceType?: notification_source_types -) { - switch (sourceType) { - case 'answer': - return `answer-${sourceId}` - case 'comment': - return sourceId - case 'contract': - return '' - default: - return sourceId - } -} - -function NotificationItem(props: { - notification: Notification - justSummary?: boolean - hideTitle?: boolean -}) { - const { notification, justSummary, hideTitle } = props - const { - sourceType, - sourceId, - sourceUserName, - sourceUserAvatarUrl, - sourceUpdateType, - reasonText, - reason, - sourceUserUsername, - createdTime, - sourceText, - sourceContractCreatorUsername, - sourceContractSlug, - } = notification - - const [defaultNotificationText, setDefaultNotificationText] = - useState<string>('') - - useEffect(() => { - if (sourceText) { - setDefaultNotificationText(sourceText) - } else if (reasonText) { - // Handle arbitrary notifications with reason text here. - setDefaultNotificationText(reasonText) - } - }, [reasonText, sourceText]) - - const [highlighted] = useState(!notification.isSeen) - - useEffect(() => { - setNotificationsAsSeen([notification]) - }, [notification]) - - if (justSummary) { - return ( - <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> - <div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}> - <div className={'flex pl-1 sm:pl-0'}> - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - justFirstName={true} - /> - <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> - <span className={'flex-shrink-0'}> - {sourceType && - reason && - getReasonForShowingNotification(notification, true, true)} - </span> - <div className={'ml-1 text-black'}> - <NotificationTextLabel - contract={null} - defaultText={defaultNotificationText} - className={'line-clamp-1'} - notification={notification} - justSummary={true} - /> - </div> - </div> - </div> - </div> - </Row> - ) - } - - return ( - <div - className={clsx( - 'bg-white px-2 pt-6 text-sm sm:px-4', - highlighted && 'bg-indigo-200 hover:bg-indigo-100' - )} - > - <a href={getSourceUrl(notification)}> - <Row className={'items-center text-gray-500 sm:justify-start'}> - <Avatar - avatarUrl={sourceUserAvatarUrl} - size={'sm'} - className={'mr-2'} - username={sourceUserName} - /> - <div className={'flex-1 overflow-hidden sm:flex'}> - <div - className={ - 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' - } - > - {sourceUpdateType != 'closed' && ( - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - justFirstName={true} - /> - )} - {sourceType && reason && ( - <div className={'inline flex truncate'}> - <span className={'ml-1 flex-shrink-0'}> - {getReasonForShowingNotification(notification, false, true)} - </span> - {!hideTitle && ( - <NotificationLink notification={notification} /> - )} - </div> - )} - {sourceId && - sourceContractSlug && - sourceContractCreatorUsername ? ( - <CopyLinkDateTimeComponent - prefix={sourceContractCreatorUsername} - slug={sourceContractSlug} - createdTime={createdTime} - elementId={getSourceIdForLinkComponent(sourceId)} - className={'-mx-1 inline-flex sm:inline-block'} - /> - ) : ( - <RelativeTimestamp time={createdTime} /> - )} - </div> - </div> - </Row> - <div className={'mt-1 ml-1 md:text-base'}> - <NotificationTextLabel - contract={null} - defaultText={defaultNotificationText} - notification={notification} - /> - </div> - - <div className={'mt-6 border-b border-gray-300'} /> - </a> - </div> - ) -} - -function NotificationTextLabel(props: { - defaultText: string - contract?: Contract | null - notification: Notification - className?: string - justSummary?: boolean -}) { - const { contract, className, defaultText, notification, justSummary } = props - const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } = - notification - if (sourceType === 'contract') { - if (justSummary) - return <span>{contract?.question || sourceContractTitle}</span> - if (!sourceText) return <div /> - // Resolved contracts - if (sourceType === 'contract' && sourceUpdateType === 'resolved') { - { - if (sourceText === 'YES' || sourceText == 'NO') { - return <BinaryOutcomeLabel outcome={sourceText as any} /> - } - if (sourceText.includes('%')) - return ( - <ProbPercentLabel prob={parseFloat(sourceText.replace('%', ''))} /> - ) - if (sourceText === 'CANCEL') return <CancelLabel /> - if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> - } - } - // Close date will be a number - it looks better without it - if (sourceUpdateType === 'closed') { - return <div /> - } - // Updated contracts - // Description will be in default text - if (parseInt(sourceText) > 0) { - return ( - <span> - Updated close time: {new Date(parseInt(sourceText)).toLocaleString()} - </span> - ) - } - } else if (sourceType === 'user' && sourceText) { - return ( - <span> - As a thank you, we sent you{' '} - <span className="text-primary"> - {formatMoney(parseInt(sourceText))} - </span> - ! - </span> - ) - } else if (sourceType === 'liquidity' && sourceText) { - return ( - <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span> - ) - } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) { - return ( - <span className="text-primary"> - {'+' + formatMoney(parseInt(sourceText))} - </span> - ) - } - // return default text - return ( - <div className={className ? className : 'line-clamp-4 whitespace-pre-line'}> - <Linkify text={defaultText} /> - </div> - ) -} - -function getReasonForShowingNotification( - notification: Notification, - simple?: boolean, - replaceOn?: boolean -) { - const { sourceType, sourceUpdateType, reason, sourceSlug } = notification - let reasonText: string - switch (sourceType) { - case 'comment': - if (reason === 'reply_to_users_answer') - reasonText = !simple ? 'replied to you on' : 'replied' - else if (reason === 'tagged_user') - reasonText = !simple ? 'tagged you on' : 'tagged you' - else if (reason === 'reply_to_users_comment') - reasonText = !simple ? 'replied to you on' : 'replied' - else if (reason === 'on_users_contract') - reasonText = !simple ? `commented on your question` : 'commented' - else if (reason === 'on_contract_with_users_comment') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_answer') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `commented on` - else reasonText = `commented on` - break - case 'contract': - if (reason === 'you_follow_user') reasonText = 'asked' - else if (sourceUpdateType === 'resolved') reasonText = `resolved` - else if (sourceUpdateType === 'closed') - reasonText = `Please resolve your question` - else reasonText = `updated` - break - case 'answer': - if (reason === 'on_users_contract') reasonText = `answered your question ` - else if (reason === 'on_contract_with_users_comment') - reasonText = `answered` - else if (reason === 'on_contract_with_users_answer') - reasonText = `answered` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `answered` - else reasonText = `answered` - break - case 'follow': - reasonText = 'followed you' - break - case 'liquidity': - reasonText = 'added liquidity to your question' - break - case 'group': - reasonText = 'added you to the group' - break - case 'user': - if (sourceSlug && reason === 'user_joined_to_bet_on_your_market') - reasonText = 'joined to bet on your market' - else if (sourceSlug) reasonText = 'joined because you shared' - else reasonText = 'joined because of you' - break - default: - reasonText = '' - } - - return ( - <span className={'flex-shrink-0'}> - {replaceOn ? reasonText.replace(' on', '') : reasonText} - </span> - ) -} From 2591655269ecc300f1b77779a460b882d4f89d56 Mon Sep 17 00:00:00 2001 From: ahalekelly <ahalekelly@gmail.com> Date: Wed, 6 Jul 2022 14:41:13 -0700 Subject: [PATCH 062/519] Fix docs edit link (#624) * Fix docs edit link * Update github links --- docs/docusaurus.config.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 85129d87..0cf5a8f2 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -26,8 +26,7 @@ const config = { docs: { routeBasePath: '/', sidebarPath: require.resolve('./sidebars.js'), - // Please change this to your repo. - editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs', + editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs', remarkPlugins: [math], rehypePlugins: [katex], }, @@ -72,7 +71,7 @@ const config = { label: 'Docs', }, { - href: 'https://github.com/manifoldmarkets/docs', + href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs', label: 'GitHub', position: 'right', }, @@ -116,7 +115,7 @@ const config = { }, { label: 'GitHub', - href: 'https://github.com/manifoldmarkets/docs', + href: 'https://github.com/manifoldmarkets/manifold/', }, ], }, From a23c744c3e1eeda0ada57b779c607555114161aa Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 6 Jul 2022 17:24:53 -0600 Subject: [PATCH 063/519] Small groups UX changes --- web/components/create-question-button.tsx | 14 +- web/components/groups/group-chat.tsx | 3 +- web/lib/firebase/groups.ts | 2 +- web/pages/group/[...slugs]/index.tsx | 154 ++++++++++++++-------- web/pages/notifications.tsx | 4 +- 5 files changed, 113 insertions(+), 64 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index 564beb83..a9161ac6 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -3,6 +3,8 @@ import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' +export const createButtonStyle = + 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' export const CreateQuestionButton = (props: { user: User | null | undefined overrideText?: string @@ -12,20 +14,20 @@ export const CreateQuestionButton = (props: { const gradient = 'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700' - const buttonStyle = - 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0' - const { user, overrideText, className, query } = props return ( - <div className={clsx('aligncenter flex justify-center', className)}> + <div className={clsx('flex justify-center', className)}> {user ? ( <Link href={`/create${query ? query : ''}`} passHref> - <button className={clsx(gradient, buttonStyle)}> + <button className={clsx(gradient, createButtonStyle)}> {overrideText ? overrideText : 'Create a question'} </button> </Link> ) : ( - <button onClick={firebaseLogin} className={clsx(gradient, buttonStyle)}> + <button + onClick={firebaseLogin} + className={clsx(gradient, createButtonStyle)} + > Sign in </button> )} diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 114a9003..13028313 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -36,6 +36,7 @@ export function GroupChat(props: { const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [groupedMessages, setGroupedMessages] = useState<Comment[]>([]) const router = useRouter() + const isMember = user && group.memberIds.includes(user?.id) useMemo(() => { // Group messages with createdTime within 2 minutes of each other. @@ -120,7 +121,7 @@ export function GroupChat(props: { ))} {messages.length === 0 && ( <div className="p-2 text-gray-500"> - No messages yet. Why not{' '} + No messages yet. Why not{isMember ? ` ` : ' join and '} <button className={'cursor-pointer font-bold text-gray-700'} onClick={() => focusInput()} diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 04a5bd44..fbb11520 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -22,7 +22,7 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | 'chat' + subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 8a8bc4c1..b6fcfe38 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -21,7 +21,6 @@ import { User, writeReferralInfo, } from 'web/lib/firebase/users' -import { Spacer } from 'web/components/layout/spacer' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' @@ -36,7 +35,10 @@ import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { ContractsGrid } from 'web/components/contract/contracts-list' -import { CreateQuestionButton } from 'web/components/create-question-button' +import { + createButtonStyle, + CreateQuestionButton, +} from 'web/components/create-question-button' import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' @@ -45,11 +47,13 @@ import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' -import ShortToggle from 'web/components/widgets/short-toggle' import { ShareIconButton } from 'web/components/share-icon-button' import { REFERRAL_AMOUNT } from 'common/user' import { SiteLink } from 'web/components/site-link' import { ContractSearch } from 'web/components/contract-search' +import clsx from 'clsx' +import { FollowList } from 'web/components/follow-list' +import { SearchIcon } from '@heroicons/react/outline' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -104,7 +108,13 @@ async function toTopUsers(userScores: { [userId: string]: number }) { export async function getStaticPaths() { return { paths: [], fallback: 'blocking' } } -const groupSubpages = [undefined, 'chat', 'questions', 'about'] as const +const groupSubpages = [ + undefined, + 'chat', + 'questions', + 'rankings', + 'about', +] as const export default function GroupPage(props: { group: Group | null @@ -178,20 +188,18 @@ export default function GroupPage(props: { const rightSidebar = ( <Col className="mt-6 hidden xl:block"> <JoinOrCreateButton group={group} user={user} isMember={!!isMember} /> - <Spacer h={6} /> - {contracts && ( - <div className={'mt-2'}> - <div className={'my-2 text-gray-500'}>Recent Questions</div> - <ContractsGrid - contracts={contracts - .sort((a, b) => b.createdTime - a.createdTime) - .slice(0, 3)} - hasMore={false} - loadMore={() => {}} - overrideGridClassName={'grid w-full grid-cols-1 gap-4'} - /> - </div> - )} + </Col> + ) + const leaderboard = ( + <Col> + <GroupLeaderboards + traderScores={traderScores} + creatorScores={creatorScores} + topTraders={topTraders} + topCreators={topCreators} + members={members} + user={user} + /> </Col> ) @@ -203,16 +211,6 @@ export default function GroupPage(props: { isCreator={!!isCreator} user={user} /> - <Spacer h={8} /> - - <GroupLeaderboards - traderScores={traderScores} - creatorScores={creatorScores} - topTraders={topTraders} - topCreators={topCreators} - members={members} - user={user} - /> </Col> ) return ( @@ -243,7 +241,15 @@ export default function GroupPage(props: { </Col> <Tabs - defaultIndex={page === 'about' ? 2 : page === 'questions' ? 1 : 0} + defaultIndex={ + page === 'rankings' + ? 2 + : page === 'about' + ? 3 + : page === 'questions' + ? 1 + : 0 + } tabs={[ { title: 'Chat', @@ -287,11 +293,15 @@ export default function GroupPage(props: { ) : ( <LoadingIndicator /> )} - {isMember && <AddContractButton group={group} user={user} />} </div> ), href: groupPath(group.slug, 'questions'), }, + { + title: 'Leaderboards', + content: leaderboard, + href: groupPath(group.slug, 'rankings'), + }, { title: 'About', content: aboutTab, @@ -309,13 +319,16 @@ function JoinOrCreateButton(props: { isMember: boolean }) { const { group, user, isMember } = props - return isMember ? ( - <CreateQuestionButton - user={user} - overrideText={'Add a new question'} - className={'w-48 flex-shrink-0'} - query={`?groupId=${group.id}`} - /> + return user && isMember ? ( + <Row className={'justify-between sm:flex-col sm:justify-center'}> + <CreateQuestionButton + user={user} + overrideText={'Add a new question'} + className={'w-48 flex-shrink-0'} + query={`?groupId=${group.id}`} + /> + <AddContractButton group={group} user={user} /> + </Row> ) : group.anyoneCanJoin ? ( <JoinGroupButton group={group} user={user} /> ) : null @@ -389,11 +402,51 @@ function GroupOverview(props: { </ShareIconButton> </Row> )} + <Col className={'mt-2'}> + <GroupMemberSearch group={group} /> + </Col> </Col> </Col> ) } +function SearchBar(props: { setQuery: (query: string) => void }) { + const { setQuery } = props + const debouncedQuery = debounce(setQuery, 50) + return ( + <div className={'relative'}> + <SearchIcon className={'absolute left-5 top-3.5 h-5 w-5 text-gray-500'} /> + <input + type="text" + onChange={(e) => debouncedQuery(e.target.value)} + placeholder="Find a member" + className="input input-bordered mb-4 w-full pl-12" + /> + </div> + ) +} + +function GroupMemberSearch(props: { group: Group }) { + const [query, setQuery] = useState('') + const members = useMembers(props.group) + + // TODO use find-active-contracts to sort by? + const matches = sortBy(members, [(member) => member.name]).filter( + (m) => + checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) + ) + return ( + <div> + <SearchBar setQuery={setQuery} /> + <Col className={'gap-2'}> + {matches.length > 0 && ( + <FollowList userIds={matches.map((m) => m.id)} /> + )} + </Col> + </div> + ) +} + export function GroupMembersList(props: { group: Group }) { const { group } = props const members = useMembers(group) @@ -449,32 +502,24 @@ function GroupLeaderboards(props: { }) { const { traderScores, creatorScores, members, topTraders, topCreators } = props - const [includeOutsiders, setIncludeOutsiders] = useState(false) // Consider hiding M$0 + // If it's just one member (curator), show all bettors, otherwise just show members return ( <Col> - <Row className="items-center justify-end gap-4 text-gray-500"> - Include all users - <ShortToggle - enabled={includeOutsiders} - setEnabled={setIncludeOutsiders} - /> - </Row> - <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> - {!includeOutsiders ? ( + {members.length > 1 ? ( <> <SortedLeaderboard users={members} scoreFunction={(user) => traderScores[user.id] ?? 0} - title="🏅 Top bettors" + title="🏅 Bettor rankings" header="Profit" /> <SortedLeaderboard users={members} scoreFunction={(user) => creatorScores[user.id] ?? 0} - title="🏅 Top creators" + title="🏅 Creator rankings" header="Market volume" /> </> @@ -543,16 +588,17 @@ function AddContractButton(props: { group: Group; user: User }) { </div> </Col> </Modal> - <Row className={'items-center justify-center'}> + <div className={'flex w-48 justify-center'}> <button - className={ - 'btn btn-md btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case' - } + className={clsx( + createButtonStyle, + 'w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white' + )} onClick={() => setOpen(true)} > Add an old question </button> - </Row> + </div> </> ) } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 382505e2..08ef9bb8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -347,10 +347,10 @@ function IncomeNotificationItem(props: { let reasonText = '' if (sourceType === 'bonus' && sourceText) { reasonText = !simple - ? `bonus for ${ + ? `Bonus for ${ parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT } unique bettors` - : ' bonus for unique bettors on' + : 'bonus on' } else if (sourceType === 'tip') { reasonText = !simple ? `tipped you` : `in tips on` } From 93b29000152ed4434853252673f4459e36f69ad8 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 06:53:14 -0600 Subject: [PATCH 064/519] Groups UX on mobile --- web/components/create-question-button.tsx | 2 + web/components/groups/group-chat.tsx | 2 +- web/components/layout/tabs.tsx | 5 ++- web/pages/group/[...slugs]/index.tsx | 51 ++++++++++++++++++----- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index a9161ac6..b8b5dcf3 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -2,6 +2,8 @@ import Link from 'next/link' import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' +import { PlusIcon } from '@heroicons/react/outline' +import { Row } from 'web/components/layout/row' export const createButtonStyle = 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 13028313..1298065d 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -97,7 +97,7 @@ export function GroupChat(props: { } return ( - <Col className={'flex-1'}> + <Col className={'mt-2 flex-1'}> <Col className={ 'max-h-[65vh] w-full space-y-2 overflow-x-hidden overflow-y-scroll' diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index ac1c0fe3..f025951c 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -16,14 +16,15 @@ export function Tabs(props: { defaultIndex?: number labelClassName?: string onClick?: (tabTitle: string, index: number) => void + className?: string }) { - const { tabs, defaultIndex, labelClassName, onClick } = props + const { tabs, defaultIndex, labelClassName, onClick, className } = props const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case return ( <> - <div className="mb-4 border-b border-gray-200"> + <div className={clsx('mb-4 border-b border-gray-200', className)}> <nav className="-mb-px flex space-x-8" aria-label="Tabs"> {tabs.map((tab, i) => ( <Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index b6fcfe38..2fbd1c5e 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -53,7 +53,7 @@ import { SiteLink } from 'web/components/site-link' import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' -import { SearchIcon } from '@heroicons/react/outline' +import { PlusIcon, SearchIcon } from '@heroicons/react/outline' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -223,9 +223,17 @@ export default function GroupPage(props: { <Col className="px-3 lg:px-1"> <Row className={'items-center justify-between gap-4'}> - <div className={'mb-1'}> - <Title className={'line-clamp-2'} text={group.name} /> - <Linkify text={group.about} /> + <div className={'sm:mb-1'}> + <div + className={ + 'line-clamp-1 my-1 text-lg text-indigo-700 sm:my-3 sm:text-2xl' + } + > + {group.name} + </div> + <div className={'hidden sm:block'}> + <Linkify text={group.about} /> + </div> </div> <div className="hidden sm:block xl:hidden"> <JoinOrCreateButton @@ -241,6 +249,7 @@ export default function GroupPage(props: { </Col> <Tabs + className={'mb-0 sm:mb-2'} defaultIndex={ page === 'rankings' ? 2 @@ -320,11 +329,19 @@ function JoinOrCreateButton(props: { }) { const { group, user, isMember } = props return user && isMember ? ( - <Row className={'justify-between sm:flex-col sm:justify-center'}> + <Row + className={'-mt-2 justify-between sm:mt-0 sm:flex-col sm:justify-center'} + > <CreateQuestionButton user={user} overrideText={'Add a new question'} - className={'w-48 flex-shrink-0'} + className={'hidden w-48 flex-shrink-0 sm:block'} + query={`?groupId=${group.id}`} + /> + <CreateQuestionButton + user={user} + overrideText={'New question'} + className={'block w-40 flex-shrink-0 sm:hidden'} query={`?groupId=${group.id}`} /> <AddContractButton group={group} user={user} /> @@ -357,8 +374,8 @@ function GroupOverview(props: { } return ( - <Col> - <Col className="gap-2 rounded-b bg-white p-4"> + <> + <Col className="gap-2 rounded-b bg-white p-2"> <Row className={'flex-wrap justify-between'}> <div className={'inline-flex items-center'}> <div className="mr-1 text-gray-500">Created by</div> @@ -370,6 +387,9 @@ function GroupOverview(props: { </div> {isCreator && <EditGroupButton className={'ml-1'} group={group} />} </Row> + <div className={'block sm:hidden'}> + <Linkify text={group.about} /> + </div> <Row className={'items-center gap-1'}> <span className={'text-gray-500'}>Membership</span> {user && user.id === creator.id ? ( @@ -406,7 +426,7 @@ function GroupOverview(props: { <GroupMemberSearch group={group} /> </Col> </Col> - </Col> + </> ) } @@ -588,16 +608,25 @@ function AddContractButton(props: { group: Group; user: User }) { </div> </Col> </Modal> - <div className={'flex w-48 justify-center'}> + <div className={'flex justify-center'}> <button className={clsx( createButtonStyle, - 'w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white' + 'hidden w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:block' )} onClick={() => setOpen(true)} > Add an old question </button> + <button + className={clsx( + createButtonStyle, + 'block w-40 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:hidden' + )} + onClick={() => setOpen(true)} + > + Old question + </button> </div> </> ) From b8748fd49a0b4f2a7c69848fd76dc0576eb7c33e Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 06:54:00 -0600 Subject: [PATCH 065/519] Leaderboards => Rankings on groups --- web/pages/group/[...slugs]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 2fbd1c5e..9b155083 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -307,7 +307,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'questions'), }, { - title: 'Leaderboards', + title: 'Rankings', content: leaderboard, href: groupPath(group.slug, 'rankings'), }, From 7f8617832f6eaf7be1c345a178c8ed74d8fb671b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 07:05:12 -0600 Subject: [PATCH 066/519] Unused vars --- web/components/create-question-button.tsx | 2 -- web/pages/group/[...slugs]/index.tsx | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index b8b5dcf3..a9161ac6 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -2,8 +2,6 @@ import Link from 'next/link' import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' -import { PlusIcon } from '@heroicons/react/outline' -import { Row } from 'web/components/layout/row' export const createButtonStyle = 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 9b155083..b38750fc 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -2,7 +2,6 @@ import { take, sortBy, debounce } from 'lodash' import { Group } from 'common/group' import { Page } from 'web/components/page' -import { Title } from 'web/components/title' import { listAllBets } from 'web/lib/firebase/bets' import { Contract } from 'web/lib/firebase/contracts' import { @@ -53,7 +52,7 @@ import { SiteLink } from 'web/components/site-link' import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' -import { PlusIcon, SearchIcon } from '@heroicons/react/outline' +import { SearchIcon } from '@heroicons/react/outline' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -410,7 +409,7 @@ function GroupOverview(props: { </Row> {anyoneCanJoin && user && ( <Row className={'flex-wrap items-center gap-1'}> - <span className={'text-gray-500'}>Sharing</span> + <span className={'text-gray-500'}>Share</span> <ShareIconButton group={group} username={user.username} From a22b29ad6d495ed965b69a71c6709de6a905c0db Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 7 Jul 2022 12:36:34 -0400 Subject: [PATCH 067/519] create: remove automatic setting of log scale --- web/pages/create.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index df83fb9f..f26d5687 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -149,14 +149,6 @@ export function NewContract(props: { ? parseFloat(initialValueString) : undefined - const adjustIsLog = () => { - if (min === undefined || max === undefined) return - const lengthDiff = Math.log10(max - min) - if (lengthDiff > 2) { - setIsLogScale(true) - } - } - // get days from today until the end of this year: const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') @@ -279,7 +271,6 @@ export function NewContract(props: { placeholder="MIN" onClick={(e) => e.stopPropagation()} onChange={(e) => setMinString(e.target.value)} - onBlur={adjustIsLog} min={Number.MIN_SAFE_INTEGER} max={Number.MAX_SAFE_INTEGER} disabled={isSubmitting} @@ -291,7 +282,6 @@ export function NewContract(props: { placeholder="MAX" onClick={(e) => e.stopPropagation()} onChange={(e) => setMaxString(e.target.value)} - onBlur={adjustIsLog} min={Number.MIN_SAFE_INTEGER} max={Number.MAX_SAFE_INTEGER} disabled={isSubmitting} From cfbb78af48a26fa070e47aba121d46ddb03e4744 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 14:41:50 -0600 Subject: [PATCH 068/519] Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 --- web/components/nav/sidebar.tsx | 45 ++- web/components/notifications-icon.tsx | 56 +-- web/hooks/use-notifications.ts | 77 +++-- web/lib/firebase/notifications.ts | 27 +- web/pages/notifications.tsx | 475 ++++++++++++-------------- 5 files changed, 358 insertions(+), 322 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index b9449ea0..6ab095ef 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -12,7 +12,7 @@ import { import clsx from 'clsx' import Link from 'next/link' import { useRouter } from 'next/router' -import { useUser } from 'web/hooks/use-user' +import { usePrivateUser, useUser } from 'web/hooks/use-user' import { firebaseLogout, User } from 'web/lib/firebase/users' import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' @@ -26,8 +26,9 @@ import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' import { Group } from 'common/group' import { Spacer } from '../layout/spacer' -import { usePreferredNotifications } from 'web/hooks/use-notifications' +import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' +import { PrivateUser } from 'common/user' function getNavigation() { return [ @@ -186,6 +187,7 @@ export default function Sidebar(props: { className?: string }) { const currentPage = router.pathname const user = useUser() + const privateUser = usePrivateUser(user?.id) const navigationOptions = !user ? signedOutNavigation : getNavigation() const mobileNavigationOptions = !user ? signedOutMobileNavigation @@ -220,11 +222,13 @@ export default function Sidebar(props: { className?: string }) { /> )} - <GroupsList - currentPage={router.asPath} - memberItems={memberItems} - user={user} - /> + {privateUser && ( + <GroupsList + currentPage={router.asPath} + memberItems={memberItems} + privateUser={privateUser} + /> + )} </div> {/* Desktop navigation */} @@ -243,11 +247,13 @@ export default function Sidebar(props: { className?: string }) { <div className="h-[1px] bg-gray-300" /> </div> )} - <GroupsList - currentPage={router.asPath} - memberItems={memberItems} - user={user} - /> + {privateUser && ( + <GroupsList + currentPage={router.asPath} + memberItems={memberItems} + privateUser={privateUser} + /> + )} </div> </nav> ) @@ -256,13 +262,16 @@ export default function Sidebar(props: { className?: string }) { function GroupsList(props: { currentPage: string memberItems: Item[] - user: User | null | undefined + privateUser: PrivateUser }) { - const { currentPage, memberItems, user } = props - const preferredNotifications = usePreferredNotifications(user?.id, { - unseenOnly: true, - customHref: '/group/', - }) + const { currentPage, memberItems, privateUser } = props + const preferredNotifications = useUnseenPreferredNotifications( + privateUser, + { + customHref: '/group/', + }, + memberItems.length > 0 ? memberItems.length : undefined + ) // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index 8f45a054..2938fd17 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -4,45 +4,53 @@ import { Row } from 'web/components/layout/row' import { useEffect, useState } from 'react' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useRouter } from 'next/router' -import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications' +import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' import { requestBonuses } from 'web/lib/firebase/api-call' +import { PrivateUser } from 'common/user' export default function NotificationsIcon(props: { className?: string }) { const user = useUser() const privateUser = usePrivateUser(user?.id) - const notifications = usePreferredGroupedNotifications(privateUser?.id, { - unseenOnly: true, - }) - const [seen, setSeen] = useState(false) useEffect(() => { - if (!privateUser) return - - if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 65 * 1000) - requestBonuses({}).catch((error) => { - console.log("couldn't get bonuses:", error.message) - }) + if ( + privateUser && + privateUser.lastTimeCheckedBonuses && + Date.now() - privateUser.lastTimeCheckedBonuses > 1000 * 70 + ) + requestBonuses({}).catch(() => console.log('no bonuses for you (yet)')) }, [privateUser]) - const router = useRouter() - useEffect(() => { - if (router.pathname.endsWith('notifications')) return setSeen(true) - else setSeen(false) - }, [router.pathname]) - return ( <Row className={clsx('justify-center')}> <div className={'relative'}> - {!seen && notifications && notifications.length > 0 && ( - <div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2"> - {notifications.length > NOTIFICATIONS_PER_PAGE - ? `${NOTIFICATIONS_PER_PAGE}+` - : notifications.length} - </div> - )} + {privateUser && <UnseenNotificationsBubble privateUser={privateUser} />} <BellIcon className={clsx(props.className)} /> </div> </Row> ) } +function UnseenNotificationsBubble(props: { privateUser: PrivateUser }) { + const router = useRouter() + const { privateUser } = props + const [seen, setSeen] = useState(false) + + useEffect(() => { + if (router.pathname.endsWith('notifications')) return setSeen(true) + else setSeen(false) + }, [router.pathname]) + + const notifications = useUnseenPreferredNotificationGroups(privateUser) + if (!notifications || notifications.length === 0 || seen) { + return <div /> + } + + return ( + <div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2"> + {notifications.length > NOTIFICATIONS_PER_PAGE + ? `${NOTIFICATIONS_PER_PAGE}+` + : notifications.length} + </div> + ) +} diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index 98b0f2fd..f5502b85 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -1,9 +1,13 @@ import { useEffect, useState } from 'react' -import { listenForPrivateUser } from 'web/lib/firebase/users' import { notification_subscribe_types, PrivateUser } from 'common/user' import { Notification } from 'common/notification' -import { listenForNotifications } from 'web/lib/firebase/notifications' +import { + getNotificationsQuery, + listenForNotifications, +} from 'web/lib/firebase/notifications' import { groupBy, map } from 'lodash' +import { useFirestoreQuery } from '@react-query-firebase/firestore' +import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' export type NotificationGroup = { notifications: Notification[] @@ -13,15 +17,30 @@ export type NotificationGroup = { type: 'income' | 'normal' } -export function usePreferredGroupedNotifications( - userId: string | undefined, - options: { unseenOnly: boolean } -) { +// For some reason react-query subscriptions don't actually listen for notifications +// Use useUnseenPreferredNotificationGroups to listen for new notifications +export function usePreferredGroupedNotifications(privateUser: PrivateUser) { const [notificationGroups, setNotificationGroups] = useState< NotificationGroup[] | undefined >(undefined) + const [notifications, setNotifications] = useState<Notification[]>([]) + const key = `notifications-${privateUser.id}-all` + + const result = useFirestoreQuery([key], getNotificationsQuery(privateUser.id)) + useEffect(() => { + if (result.isLoading) return + if (!result.data) return setNotifications([]) + const notifications = result.data.docs.map( + (doc) => doc.data() as Notification + ) + + const notificationsToShow = getAppropriateNotifications( + notifications, + privateUser.notificationPreferences + ).filter((n) => !n.isSeenOnHref) + setNotifications(notificationsToShow) + }, [privateUser.notificationPreferences, result.data, result.isLoading]) - const notifications = usePreferredNotifications(userId, options) useEffect(() => { if (!notifications) return @@ -32,6 +51,20 @@ export function usePreferredGroupedNotifications( return notificationGroups } +export function useUnseenPreferredNotificationGroups(privateUser: PrivateUser) { + const notifications = useUnseenPreferredNotifications(privateUser, {}) + const [notificationGroups, setNotificationGroups] = useState< + NotificationGroup[] | undefined + >(undefined) + useEffect(() => { + if (!notifications) return + + const groupedNotifications = groupNotifications(notifications) + setNotificationGroups(groupedNotifications) + }, [notifications]) + return notificationGroups +} + export function groupNotifications(notifications: Notification[]) { let notificationGroups: NotificationGroup[] = [] const notificationGroupsByDay = groupBy(notifications, (notification) => @@ -85,32 +118,24 @@ export function groupNotifications(notifications: Notification[]) { return notificationGroups } -export function usePreferredNotifications( - userId: string | undefined, - options: { unseenOnly: boolean; customHref?: string } +export function useUnseenPreferredNotifications( + privateUser: PrivateUser, + options: { customHref?: string }, + limit: number = NOTIFICATIONS_PER_PAGE ) { - const { unseenOnly, customHref } = options - const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null) + const { customHref } = options const [notifications, setNotifications] = useState<Notification[]>([]) const [userAppropriateNotifications, setUserAppropriateNotifications] = useState<Notification[]>([]) useEffect(() => { - if (userId) listenForPrivateUser(userId, setPrivateUser) - }, [userId]) + return listenForNotifications(privateUser.id, setNotifications, { + unseenOnly: true, + limit, + }) + }, [limit, privateUser.id]) useEffect(() => { - if (privateUser) - return listenForNotifications( - privateUser.id, - setNotifications, - unseenOnly - ) - }, [privateUser, unseenOnly]) - - useEffect(() => { - if (!privateUser) return - const notificationsToShow = getAppropriateNotifications( notifications, privateUser.notificationPreferences @@ -118,7 +143,7 @@ export function usePreferredNotifications( customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref ) setUserAppropriateNotifications(notificationsToShow) - }, [privateUser, notifications, customHref]) + }, [notifications, customHref, privateUser.notificationPreferences]) return userAppropriateNotifications } diff --git a/web/lib/firebase/notifications.ts b/web/lib/firebase/notifications.ts index c0dca8be..d2db3665 100644 --- a/web/lib/firebase/notifications.ts +++ b/web/lib/firebase/notifications.ts @@ -1,21 +1,36 @@ -import { collection, query, where } from 'firebase/firestore' +import { collection, limit, orderBy, query, where } from 'firebase/firestore' import { Notification } from 'common/notification' import { db } from 'web/lib/firebase/init' import { listenForValues } from 'web/lib/firebase/utils' +import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' -function getNotificationsQuery(userId: string, unseenOnly?: boolean) { +export function getNotificationsQuery( + userId: string, + unseenOnlyOptions?: { unseenOnly: boolean; limit: number } +) { const notifsCollection = collection(db, `/users/${userId}/notifications`) - if (unseenOnly) return query(notifsCollection, where('isSeen', '==', false)) - return query(notifsCollection) + if (unseenOnlyOptions?.unseenOnly) + return query( + notifsCollection, + where('isSeen', '==', false), + orderBy('createdTime', 'desc'), + limit(unseenOnlyOptions.limit) + ) + return query( + notifsCollection, + orderBy('createdTime', 'desc'), + // Nobody's going through 10 pages of notifications, right? + limit(NOTIFICATIONS_PER_PAGE * 10) + ) } export function listenForNotifications( userId: string, setNotifications: (notifs: Notification[]) => void, - unseenOnly?: boolean + unseenOnlyOptions?: { unseenOnly: boolean; limit: number } ) { return listenForValues<Notification>( - getNotificationsQuery(userId, unseenOnly), + getNotificationsQuery(userId, unseenOnlyOptions), (notifs) => { notifs.sort((n1, n2) => n2.createdTime - n1.createdTime) setNotifications(notifs) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 08ef9bb8..3f9b4eed 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,5 +1,5 @@ import { Tabs } from 'web/components/layout/tabs' -import { useUser } from 'web/hooks/use-user' +import { usePrivateUser, useUser } from 'web/hooks/use-user' import React, { useEffect, useState } from 'react' import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' @@ -8,8 +8,6 @@ import { Page } from 'web/components/page' import { Title } from 'web/components/title' import { doc, updateDoc } from 'firebase/firestore' import { db } from 'web/lib/firebase/init' -import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' -import Custom404 from 'web/pages/404' import { UserLink } from 'web/components/user-page' import { notification_subscribe_types, PrivateUser } from 'common/user' import { Contract } from 'common/contract' @@ -35,137 +33,149 @@ import { formatMoney } from 'common/util/format' import { groupPath } from 'web/lib/firebase/groups' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { groupBy, sum, uniq } from 'lodash' +import Custom404 from 'web/pages/404' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' export default function Notifications() { const user = useUser() - const [page, setPage] = useState(1) - - const groupedNotifications = usePreferredGroupedNotifications(user?.id, { - unseenOnly: false, - }) - const [paginatedNotificationGroups, setPaginatedNotificationGroups] = - useState<NotificationGroup[]>([]) - useEffect(() => { - if (!groupedNotifications) return - const start = (page - 1) * NOTIFICATIONS_PER_PAGE - const end = start + NOTIFICATIONS_PER_PAGE - const maxNotificationsToShow = groupedNotifications.slice(start, end) - const remainingNotification = groupedNotifications.slice(end) - for (const notification of remainingNotification) { - if (notification.isSeen) break - else setNotificationsAsSeen(notification.notifications) - } - setPaginatedNotificationGroups(maxNotificationsToShow) - }, [groupedNotifications, page]) - - if (user === undefined) { - return <LoadingIndicator /> - } - if (user === null) { - return <Custom404 /> - } + const privateUser = usePrivateUser(user?.id) + if (!user) return <Custom404 /> return ( <Page> <div className={'p-2 sm:p-4'}> <Title text={'Notifications'} className={'hidden md:block'} /> - <Tabs - labelClassName={'pb-2 pt-1 '} - defaultIndex={0} - tabs={[ - { - title: 'Notifications', - content: groupedNotifications ? ( - <div className={''}> - {paginatedNotificationGroups.length === 0 && - "You don't have any notifications. Try changing your settings to see more."} - {paginatedNotificationGroups.map((notification) => - notification.type === 'income' ? ( - <IncomeNotificationGroupItem - notificationGroup={notification} - key={notification.groupedById + notification.timePeriod} - /> - ) : notification.notifications.length === 1 ? ( - <NotificationItem - notification={notification.notifications[0]} - key={notification.notifications[0].id} - /> - ) : ( - <NotificationGroupItem - notificationGroup={notification} - key={notification.groupedById + notification.timePeriod} - /> - ) - )} - {groupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( - <nav - className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" - aria-label="Pagination" - > - <div className="hidden sm:block"> - <p className="text-sm text-gray-700"> - Showing{' '} - <span className="font-medium"> - {page === 1 - ? page - : (page - 1) * NOTIFICATIONS_PER_PAGE} - </span>{' '} - to{' '} - <span className="font-medium"> - {page * NOTIFICATIONS_PER_PAGE} - </span>{' '} - of{' '} - <span className="font-medium"> - {groupedNotifications.length} - </span>{' '} - results - </p> - </div> - <div className="flex flex-1 justify-between sm:justify-end"> - <a - href="#" - className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => page > 1 && setPage(page - 1)} - > - Previous - </a> - <a - href="#" - className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => - page < - groupedNotifications?.length / - NOTIFICATIONS_PER_PAGE && setPage(page + 1) - } - > - Next - </a> - </div> - </nav> - )} - </div> - ) : ( - <LoadingIndicator /> - ), - }, - { - title: 'Settings', - content: ( - <div className={''}> - <NotificationSettings /> - </div> - ), - }, - ]} - /> + <div> + <Tabs + labelClassName={'pb-2 pt-1 '} + className={'mb-0 sm:mb-2'} + defaultIndex={0} + tabs={[ + { + title: 'Notifications', + content: privateUser ? ( + <NotificationsList privateUser={privateUser} /> + ) : ( + <LoadingIndicator /> + ), + }, + { + title: 'Settings', + content: ( + <div className={''}> + <NotificationSettings /> + </div> + ), + }, + ]} + /> + </div> </div> </Page> ) } +function NotificationsList(props: { privateUser: PrivateUser }) { + const { privateUser } = props + const [page, setPage] = useState(1) + const allGroupedNotifications = usePreferredGroupedNotifications(privateUser) + const [paginatedGroupedNotifications, setPaginatedGroupedNotifications] = + useState<NotificationGroup[] | undefined>(undefined) + + useEffect(() => { + if (!allGroupedNotifications) return + const start = (page - 1) * NOTIFICATIONS_PER_PAGE + const end = start + NOTIFICATIONS_PER_PAGE + const maxNotificationsToShow = allGroupedNotifications.slice(start, end) + const remainingNotification = allGroupedNotifications.slice(end) + for (const notification of remainingNotification) { + if (notification.isSeen) break + else setNotificationsAsSeen(notification.notifications) + } + setPaginatedGroupedNotifications(maxNotificationsToShow) + }, [allGroupedNotifications, page]) + + if (!paginatedGroupedNotifications || !allGroupedNotifications) + return <LoadingIndicator /> + + return ( + <div className={'min-h-[100vh]'}> + {paginatedGroupedNotifications.length === 0 && ( + <div className={'mt-2'}> + You don't have any notifications. Try changing your settings to see + more. + </div> + )} + + {paginatedGroupedNotifications.map((notification) => + notification.type === 'income' ? ( + <IncomeNotificationGroupItem + notificationGroup={notification} + key={notification.groupedById + notification.timePeriod} + /> + ) : notification.notifications.length === 1 ? ( + <NotificationItem + notification={notification.notifications[0]} + key={notification.notifications[0].id} + /> + ) : ( + <NotificationGroupItem + notificationGroup={notification} + key={notification.groupedById + notification.timePeriod} + /> + ) + )} + {paginatedGroupedNotifications.length > 0 && + allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( + <nav + className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" + aria-label="Pagination" + > + <div className="hidden sm:block"> + <p className="text-sm text-gray-700"> + Showing{' '} + <span className="font-medium"> + {page === 1 ? page : (page - 1) * NOTIFICATIONS_PER_PAGE} + </span>{' '} + to{' '} + <span className="font-medium"> + {page * NOTIFICATIONS_PER_PAGE} + </span>{' '} + of{' '} + <span className="font-medium"> + {allGroupedNotifications.length} + </span>{' '} + results + </p> + </div> + <div className="flex flex-1 justify-between sm:justify-end"> + <a + href="#" + className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page > 1 && setPage(page - 1)} + > + Previous + </a> + <a + href="#" + className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => + page < + allGroupedNotifications?.length / NOTIFICATIONS_PER_PAGE && + setPage(page + 1) + } + > + Next + </a> + </div> + </nav> + )} + </div> + ) +} + function IncomeNotificationGroupItem(props: { notificationGroup: NotificationGroup className?: string @@ -261,18 +271,20 @@ function IncomeNotificationGroupItem(props: { )} <Row className={'items-center text-gray-500 sm:justify-start'}> <TrendingUpIcon className={'text-primary h-7 w-7'} /> - <div className={'flex truncate'}> - <div - onClick={() => setExpanded(!expanded)} - className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} - > - <span> + <div + className={'flex w-full flex-row flex-wrap pl-1 sm:pl-0'} + onClick={() => setExpanded(!expanded)} + > + <div className={'flex w-full flex-row justify-between'}> + <div> {'Daily Income Summary: '} <span className={'text-primary'}> {'+' + formatMoney(totalIncome)} </span> - </span> - <RelativeTimestamp time={notifications[0].createdTime} /> + </div> + <div className={'inline-block'}> + <RelativeTimestamp time={notifications[0].createdTime} /> + </div> </div> </div> </Row> @@ -329,13 +341,7 @@ function IncomeNotificationItem(props: { justSummary?: boolean }) { const { notification, justSummary } = props - const { - sourceType, - sourceUserName, - reason, - sourceUserUsername, - createdTime, - } = notification + const { sourceType, sourceUserName, sourceUserUsername } = notification const [highlighted] = useState(!notification.isSeen) useEffect(() => { @@ -354,7 +360,7 @@ function IncomeNotificationItem(props: { } else if (sourceType === 'tip') { reasonText = !simple ? `tipped you` : `in tips on` } - return <span className={'flex-shrink-0'}>{reasonText}</span> + return reasonText } if (justSummary) { @@ -374,7 +380,7 @@ function IncomeNotificationItem(props: { </div> <span className={'flex truncate'}> {getReasonForShowingIncomeNotification(true)} - <NotificationLink notification={notification} /> + <NotificationLink notification={notification} noClick={true} /> </span> </div> </div> @@ -392,42 +398,33 @@ function IncomeNotificationItem(props: { > <a href={getSourceUrl(notification)}> <Row className={'items-center text-gray-500 sm:justify-start'}> - <div className={'flex max-w-xl shrink '}> - {sourceType && reason && ( - <div className={'inline'}> - <span className={'mr-1'}> - <NotificationTextLabel - contract={null} - defaultText={notification.sourceText ?? ''} - notification={notification} + <div className={'line-clamp-2 flex max-w-xl shrink '}> + <div className={'inline'}> + <span className={'mr-1'}> + <NotificationTextLabel + contract={null} + defaultText={notification.sourceText ?? ''} + notification={notification} + /> + </span> + </div> + <span> + {sourceType != 'bonus' && + (sourceUserUsername === MULTIPLE_USERS_KEY ? ( + <span className={'mr-1 truncate'}>Multiple users</span> + ) : ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-1 flex-shrink-0'} + justFirstName={true} /> - </span> - - {sourceType != 'bonus' && - (sourceUserUsername === MULTIPLE_USERS_KEY ? ( - <span className={'mr-1 truncate'}>Multiple users</span> - ) : ( - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-1 flex-shrink-0'} - justFirstName={true} - /> - ))} - </div> - )} - {getReasonForShowingIncomeNotification(false)} - <span className={'ml-1 flex hidden sm:inline-block'}> - on + ))} + {getReasonForShowingIncomeNotification(false)} {' on'} <NotificationLink notification={notification} /> </span> - <RelativeTimestamp time={createdTime} /> </div> </Row> - <span className={'flex truncate text-gray-500 sm:hidden'}> - on - <NotificationLink notification={notification} /> - </span> <div className={'mt-4 border-b border-gray-300'} /> </a> </div> @@ -473,23 +470,25 @@ function NotificationGroupItem(props: { )} <Row className={'items-center text-gray-500 sm:justify-start'}> <EmptyAvatar multi /> - <div className={'flex truncate pl-2'}> - <div - onClick={() => setExpanded(!expanded)} - className={' flex cursor-pointer truncate pl-1 sm:pl-0'} - > - {sourceContractTitle ? ( - <> - <span className={'flex-shrink-0'}>{'Activity on '}</span> - <span className={'truncate'}> - <NotificationLink notification={notifications[0]} /> - </span> - </> - ) : ( - 'Other activity' - )} - </div> - <RelativeTimestamp time={notifications[0].createdTime} /> + <div + className={'line-clamp-2 flex w-full flex-row flex-wrap pl-1 sm:pl-0'} + > + {sourceContractTitle ? ( + <div className={'flex w-full flex-row justify-between'}> + <div className={'ml-2'}> + Activity on + <NotificationLink notification={notifications[0]} /> + </div> + <div className={'hidden sm:inline-block'}> + <RelativeTimestamp time={notifications[0].createdTime} /> + </div> + </div> + ) : ( + <span> + Other activity + <RelativeTimestamp time={notifications[0].createdTime} /> + </span> + )} </div> </Row> <div> @@ -528,7 +527,7 @@ function NotificationGroupItem(props: { notification={notification} key={notification.id} justSummary={false} - hideTitle={true} + isChildOfGroup={true} /> ))} </> @@ -545,22 +544,18 @@ function NotificationGroupItem(props: { function NotificationItem(props: { notification: Notification justSummary?: boolean - hideTitle?: boolean + isChildOfGroup?: boolean }) { - const { notification, justSummary, hideTitle } = props + const { notification, justSummary, isChildOfGroup } = props const { sourceType, - sourceId, sourceUserName, sourceUserAvatarUrl, sourceUpdateType, reasonText, reason, sourceUserUsername, - createdTime, sourceText, - sourceContractCreatorUsername, - sourceContractSlug, } = notification const [defaultNotificationText, setDefaultNotificationText] = @@ -629,44 +624,38 @@ function NotificationItem(props: { className={'mr-2'} username={sourceUserName} /> - <div className={'flex-1 overflow-hidden sm:flex'}> + <div className={'flex w-full flex-row pl-1 sm:pl-0'}> <div className={ - 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' + 'line-clamp-2 sm:line-clamp-none flex w-full flex-row justify-between' } > - {sourceUpdateType != 'closed' && ( - <UserLink - name={sourceUserName || ''} - username={sourceUserUsername || ''} - className={'mr-0 flex-shrink-0'} - justFirstName={true} - /> - )} - {sourceType && reason && ( - <div className={'inline flex truncate'}> - <span className={'ml-1 flex-shrink-0'}> - {getReasonForShowingNotification(notification, false, true)} - </span> - {!hideTitle && ( - <NotificationLink notification={notification} /> - )} - </div> - )} - {sourceId && - sourceContractSlug && - sourceContractCreatorUsername ? ( - <CopyLinkDateTimeComponent - prefix={sourceContractCreatorUsername} - slug={sourceContractSlug} - createdTime={createdTime} - elementId={getSourceIdForLinkComponent(sourceId)} - className={'-mx-1 inline-flex sm:inline-block'} - /> - ) : ( - <RelativeTimestamp time={createdTime} /> - )} + <div> + {sourceUpdateType != 'closed' && ( + <UserLink + name={sourceUserName || ''} + username={sourceUserUsername || ''} + className={'mr-1 flex-shrink-0'} + justFirstName={true} + /> + )} + {getReasonForShowingNotification( + notification, + false, + isChildOfGroup + )} + {isChildOfGroup ? ( + <RelativeTimestamp time={notification.createdTime} /> + ) : ( + <NotificationLink notification={notification} /> + )} + </div> </div> + {!isChildOfGroup && ( + <div className={'hidden sm:inline-block'}> + <RelativeTimestamp time={notification.createdTime} /> + </div> + )} </div> </Row> <div className={'mt-1 ml-1 md:text-base'}> @@ -697,8 +686,11 @@ export const setNotificationsAsSeen = (notifications: Notification[]) => { return notifications } -function NotificationLink(props: { notification: Notification }) { - const { notification } = props +function NotificationLink(props: { + notification: Notification + noClick?: boolean +}) { + const { notification, noClick } = props const { sourceType, sourceContractTitle, @@ -707,8 +699,17 @@ function NotificationLink(props: { notification: Notification }) { sourceSlug, sourceTitle, } = notification + if (noClick) + return ( + <span className={'ml-1 font-bold '}> + {sourceContractTitle || sourceTitle} + </span> + ) return ( <a + className={ + 'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2 ' + } href={ sourceContractCreatorUsername ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` @@ -716,9 +717,6 @@ function NotificationLink(props: { notification: Notification }) { ? `${groupPath(sourceSlug)}` : '' } - className={ - 'ml-1 inline max-w-xs truncate font-bold text-gray-500 hover:underline hover:decoration-indigo-400 hover:decoration-2 sm:max-w-sm' - } > {sourceContractTitle || sourceTitle} </a> @@ -852,14 +850,6 @@ function getReasonForShowingNotification( reasonText = !simple ? 'tagged you on' : 'tagged you' else if (reason === 'reply_to_users_comment') reasonText = !simple ? 'replied to you on' : 'replied' - else if (reason === 'on_users_contract') - reasonText = !simple ? `commented on your question` : 'commented' - else if (reason === 'on_contract_with_users_comment') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_answer') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `commented on` else reasonText = `commented on` break case 'contract': @@ -871,12 +861,6 @@ function getReasonForShowingNotification( break case 'answer': if (reason === 'on_users_contract') reasonText = `answered your question ` - else if (reason === 'on_contract_with_users_comment') - reasonText = `answered` - else if (reason === 'on_contract_with_users_answer') - reasonText = `answered` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `answered` else reasonText = `answered` break case 'follow': @@ -897,12 +881,7 @@ function getReasonForShowingNotification( default: reasonText = '' } - - return ( - <span className={'flex-shrink-0'}> - {replaceOn ? reasonText.replace(' on', '') : reasonText} - </span> - ) + return replaceOn ? reasonText.replace(' on', '') : reasonText } // TODO: where should we put referral bonus notifications? From d6e808e1a39e20193c97f713b1710ed687f7a5a4 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 14:45:26 -0600 Subject: [PATCH 069/519] Remove category filters --- web/components/contract-search.tsx | 123 ++--------------------------- 1 file changed, 8 insertions(+), 115 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 2c7f5b62..220a95ab 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -20,13 +20,6 @@ import { Row } from './layout/row' import { useEffect, useMemo, useRef, useState } from 'react' import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' -import { useUser } from 'web/hooks/use-user' -import { useFollows } from 'web/hooks/use-follows' -import { EditCategoriesButton } from './feed/category-selector' -import { CATEGORIES, category } from 'common/categories' -import { Tabs } from './layout/tabs' -import { EditFollowingButton } from './following-button' -import { track } from '@amplitude/analytics-browser' import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' @@ -60,7 +53,6 @@ export function ContractSearch(props: { tag?: string excludeContractIds?: string[] } - showCategorySelector: boolean onContractClick?: (contract: Contract) => void showPlaceHolder?: boolean hideOrderSelector?: boolean @@ -70,7 +62,6 @@ export function ContractSearch(props: { const { querySortOptions, additionalFilter, - showCategorySelector, onContractClick, overrideGridClassName, hideOrderSelector, @@ -78,10 +69,6 @@ export function ContractSearch(props: { hideQuickBet, } = props - const user = useUser() - const followedCategories = user?.followedCategories - const follows = useFollows(user?.id) - const { initialSort } = useInitialQueryAndSort(querySortOptions) const sort = sortIndexes @@ -94,18 +81,11 @@ export function ContractSearch(props: { querySortOptions?.defaultFilter ?? 'open' ) - const [mode, setMode] = useState<'categories' | 'following'>('categories') - const { filters, numericFilters } = useMemo(() => { let filters = [ filter === 'open' ? 'isResolved:false' : '', filter === 'closed' ? 'isResolved:false' : '', filter === 'resolved' ? 'isResolved:true' : '', - showCategorySelector - ? mode === 'categories' - ? followedCategories?.map((cat) => `lowercaseTags:${cat}`) ?? '' - : follows?.map((creatorId) => `creatorId:${creatorId}`) ?? '' - : '', additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` : '', @@ -120,14 +100,7 @@ export function ContractSearch(props: { ].filter((f) => f) return { filters, numericFilters } - }, [ - filter, - showCategorySelector, - mode, - Object.values(additionalFilter ?? {}).join(','), - (followedCategories ?? []).join(','), - (follows ?? []).join(','), - ]) + }, [filter, Object.values(additionalFilter ?? {}).join(',')]) const indexName = `${indexPrefix}contracts-${sort}` @@ -182,28 +155,13 @@ export function ContractSearch(props: { <Spacer h={3} /> - {showCategorySelector && ( - <CategoryFollowSelector - mode={mode} - setMode={setMode} - followedCategories={followedCategories ?? []} - follows={follows ?? []} - /> - )} - - <Spacer h={4} /> - - {mode === 'following' && (follows ?? []).length === 0 ? ( - <>You're not following anyone yet.</> - ) : ( - <ContractSearchInner - querySortOptions={querySortOptions} - onContractClick={onContractClick} - overrideGridClassName={overrideGridClassName} - hideQuickBet={hideQuickBet} - excludeContractIds={additionalFilter?.excludeContractIds} - /> - )} + <ContractSearchInner + querySortOptions={querySortOptions} + onContractClick={onContractClick} + overrideGridClassName={overrideGridClassName} + hideQuickBet={hideQuickBet} + excludeContractIds={additionalFilter?.excludeContractIds} + /> </InstantSearch> ) } @@ -288,68 +246,3 @@ export function ContractSearchInner(props: { /> ) } - -function CategoryFollowSelector(props: { - mode: 'categories' | 'following' - setMode: (mode: 'categories' | 'following') => void - followedCategories: string[] - follows: string[] -}) { - const { mode, setMode, followedCategories, follows } = props - - const user = useUser() - - const categoriesTitle = `${ - followedCategories?.length ? followedCategories.length : 'All' - } Categories` - let categoriesDescription = `Showing all categories` - - const followingTitle = `${follows?.length ? follows.length : 'All'} Following` - - if (followedCategories.length) { - const categoriesLabel = followedCategories - .slice(0, 3) - .map((cat) => CATEGORIES[cat as category]) - .join(', ') - const andMoreLabel = - followedCategories.length > 3 - ? `, and ${followedCategories.length - 3} more` - : '' - categoriesDescription = `Showing ${categoriesLabel}${andMoreLabel}` - } - - return ( - <Tabs - defaultIndex={mode === 'categories' ? 0 : 1} - tabs={[ - { - title: categoriesTitle, - content: user && ( - <Row className="items-center gap-1 text-gray-500"> - <div>{categoriesDescription}</div> - <EditCategoriesButton className="self-start" user={user} /> - </Row> - ), - }, - ...(user - ? [ - { - title: followingTitle, - content: ( - <Row className="items-center gap-2 text-gray-500"> - <div>Showing markets by users you are following.</div> - <EditFollowingButton className="self-start" user={user} /> - </Row> - ), - }, - ] - : []), - ]} - onClick={(_, index) => { - const mode = index === 0 ? 'categories' : 'following' - setMode(mode) - track(`click ${mode} tab`) - }} - /> - ) -} From 3ff8b263122f0d8e030d1e8cfac10b28b217caae Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 14:55:28 -0600 Subject: [PATCH 070/519] Remove category selector references --- web/components/contract/contracts-list.tsx | 1 - web/pages/group/[...slugs]/index.tsx | 1 - web/pages/home.tsx | 1 - web/pages/markets.tsx | 2 +- web/pages/tag/[tag].tsx | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-list.tsx index e24090d9..20a85ef4 100644 --- a/web/components/contract/contracts-list.tsx +++ b/web/components/contract/contracts-list.tsx @@ -87,7 +87,6 @@ export function CreatorContractsList(props: { creator: User }) { additionalFilter={{ creatorId: creator.id, }} - showCategorySelector={false} /> ) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index b38750fc..db8d38be 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -598,7 +598,6 @@ function AddContractButton(props: { group: Group; user: User }) { <ContractSearch hideOrderSelector={true} onContractClick={addContractToCurrentGroup} - showCategorySelector={false} overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'} showPlaceHolder={true} hideQuickBet={true} diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 75eae351..12bd46a2 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -34,7 +34,6 @@ const Home = () => { shouldLoadFromStorage: true, defaultSort: getSavedSort() ?? '24-hour-vol', }} - showCategorySelector onContractClick={(c) => { // Show contract without navigating to contract page. setContract(c) diff --git a/web/pages/markets.tsx b/web/pages/markets.tsx index ab8e064d..a3e851fc 100644 --- a/web/pages/markets.tsx +++ b/web/pages/markets.tsx @@ -11,7 +11,7 @@ export default function Markets() { description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets." url="/markets" /> - <ContractSearch showCategorySelector /> + <ContractSearch /> </Page> ) } diff --git a/web/pages/tag/[tag].tsx b/web/pages/tag/[tag].tsx index c6b7d31d..476afecf 100644 --- a/web/pages/tag/[tag].tsx +++ b/web/pages/tag/[tag].tsx @@ -18,7 +18,6 @@ export default function TagPage() { shouldLoadFromStorage: true, }} additionalFilter={{ tag }} - showCategorySelector={false} /> </Page> ) From 3eee4a41030008f701e120792b22fc44fbd62a6a Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 15:06:29 -0600 Subject: [PATCH 071/519] Track notification clicks --- web/pages/notifications.tsx | 50 ++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 3f9b4eed..aa2cdc51 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -34,6 +34,7 @@ import { groupPath } from 'web/lib/firebase/groups' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { groupBy, sum, uniq } from 'lodash' import Custom404 from 'web/pages/404' +import { track } from '@amplitude/analytics-browser' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -380,7 +381,7 @@ function IncomeNotificationItem(props: { </div> <span className={'flex truncate'}> {getReasonForShowingIncomeNotification(true)} - <NotificationLink notification={notification} noClick={true} /> + <QuestionLink notification={notification} ignoreClick={true} /> </span> </div> </div> @@ -421,7 +422,7 @@ function IncomeNotificationItem(props: { /> ))} {getReasonForShowingIncomeNotification(false)} {' on'} - <NotificationLink notification={notification} /> + <QuestionLink notification={notification} /> </span> </div> </Row> @@ -477,7 +478,7 @@ function NotificationGroupItem(props: { <div className={'flex w-full flex-row justify-between'}> <div className={'ml-2'}> Activity on - <NotificationLink notification={notifications[0]} /> + <QuestionLink notification={notifications[0]} /> </div> <div className={'hidden sm:inline-block'}> <RelativeTimestamp time={notifications[0].createdTime} /> @@ -616,7 +617,22 @@ function NotificationItem(props: { highlighted && 'bg-indigo-200 hover:bg-indigo-100' )} > - <a href={getSourceUrl(notification)}> + <a + href={getSourceUrl(notification)} + onClick={() => + track('Notification Clicked', { + type: 'notification item', + sourceType, + sourceUserName, + sourceUserAvatarUrl, + sourceUpdateType, + reasonText, + reason, + sourceUserUsername, + sourceText, + }) + } + > <Row className={'items-center text-gray-500 sm:justify-start'}> <Avatar avatarUrl={sourceUserAvatarUrl} @@ -647,7 +663,7 @@ function NotificationItem(props: { {isChildOfGroup ? ( <RelativeTimestamp time={notification.createdTime} /> ) : ( - <NotificationLink notification={notification} /> + <QuestionLink notification={notification} /> )} </div> </div> @@ -686,11 +702,11 @@ export const setNotificationsAsSeen = (notifications: Notification[]) => { return notifications } -function NotificationLink(props: { +function QuestionLink(props: { notification: Notification - noClick?: boolean + ignoreClick?: boolean }) { - const { notification, noClick } = props + const { notification, ignoreClick } = props const { sourceType, sourceContractTitle, @@ -699,7 +715,8 @@ function NotificationLink(props: { sourceSlug, sourceTitle, } = notification - if (noClick) + + if (ignoreClick) return ( <span className={'ml-1 font-bold '}> {sourceContractTitle || sourceTitle} @@ -717,6 +734,17 @@ function NotificationLink(props: { ? `${groupPath(sourceSlug)}` : '' } + onClick={() => + track('Notification Clicked', { + type: 'question title', + sourceType, + sourceContractTitle, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + sourceTitle, + }) + } > {sourceContractTitle || sourceTitle} </a> @@ -969,6 +997,10 @@ function NotificationSettings() { newValue: notification_subscribe_types ) { if (!privateUser) return + track('In-App Notification Preferences Changed', { + newPreference: newValue, + oldPreference: privateUser.notificationPreferences, + }) toast.promise( updatePrivateUser(privateUser.id, { notificationPreferences: newValue, From e456b9a85562c8eabad9767213537bf5ec266504 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 15:24:13 -0600 Subject: [PATCH 072/519] Analyze tab usage --- web/components/contract/contract-tabs.tsx | 1 + web/components/layout/tabs.tsx | 16 +++++++++++++++- web/components/user-page.tsx | 1 + web/pages/group/[...slugs]/index.tsx | 1 + web/pages/groups.tsx | 1 + web/pages/leaderboards.tsx | 1 + web/pages/notifications.tsx | 1 + web/pages/stats.tsx | 1 + 8 files changed, 22 insertions(+), 1 deletion(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index e68e59b9..c7759fb8 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -87,6 +87,7 @@ export function ContractTabs(props: { return ( <Tabs + currentPageForAnalytics={'contract'} tabs={[ { title: 'Comments', content: commentActivity }, { title: 'Bets', content: betActivity }, diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index f025951c..8aec39b1 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import Link from 'next/link' import { ReactNode, useState } from 'react' import { Row } from './row' +import { track } from '@amplitude/analytics-browser' type Tab = { title: string @@ -17,8 +18,16 @@ export function Tabs(props: { labelClassName?: string onClick?: (tabTitle: string, index: number) => void className?: string + currentPageForAnalytics?: string }) { - const { tabs, defaultIndex, labelClassName, onClick, className } = props + const { + tabs, + defaultIndex, + labelClassName, + onClick, + className, + currentPageForAnalytics, + } = props const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case @@ -32,6 +41,11 @@ export function Tabs(props: { id={`tab-${i}`} key={tab.title} onClick={(e) => { + track('Clicked Tab', { + title: tab.title, + href: tab.href, + currentPage: currentPageForAnalytics, + }) if (!tab.href) { e.preventDefault() } diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index c33476aa..af03eb46 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -262,6 +262,7 @@ export function UserPage(props: { {usersContracts !== 'loading' && commentsByContract != 'loading' ? ( <Tabs + currentPageForAnalytics={'profile'} labelClassName={'pb-2 pt-1 '} defaultIndex={ defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0 diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index db8d38be..73d7819a 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -248,6 +248,7 @@ export default function GroupPage(props: { </Col> <Tabs + currentPageForAnalytics={groupPath(group.slug)} className={'mb-0 sm:mb-2'} defaultIndex={ page === 'rankings' diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 22fe7661..ae64cc76 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -106,6 +106,7 @@ export default function Groups(props: { </div> <Tabs + currentPageForAnalytics={'groups'} tabs={[ ...(user && memberGroupIds.length > 0 ? [ diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index f306493b..061f3a19 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -126,6 +126,7 @@ export default function Leaderboards(props: { <Page> <Title text={'Leaderboards'} className={'hidden md:block'} /> <Tabs + currentPageForAnalytics={'leaderboards'} defaultIndex={0} onClick={(title, index) => { const period = ['allTime', 'monthly', 'weekly', 'daily'][index] diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index aa2cdc51..bb495ecd 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -50,6 +50,7 @@ export default function Notifications() { <Title text={'Notifications'} className={'hidden md:block'} /> <div> <Tabs + currentPageForAnalytics={'notifications'} labelClassName={'pb-2 pt-1 '} className={'mb-0 sm:mb-2'} defaultIndex={0} diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index c81bc3ff..57c47843 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -25,6 +25,7 @@ export default function Analytics() { return ( <Page> <Tabs + currentPageForAnalytics={'stats'} tabs={[ { title: 'Activity', From 999c1cd8e3d929ff3c445e4dde3a0d32769f4809 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 15:52:28 -0600 Subject: [PATCH 073/519] Bold more on new group chats --- web/components/nav/nav-bar.tsx | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 9f0f8ddd..2b065f1c 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -9,7 +9,7 @@ import { import { Transition, Dialog } from '@headlessui/react' import { useState, Fragment } from 'react' import Sidebar, { Item } from './sidebar' -import { useUser } from 'web/hooks/use-user' +import { usePrivateUser, useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { Avatar } from '../avatar' import clsx from 'clsx' @@ -17,6 +17,8 @@ import { useRouter } from 'next/router' import NotificationsIcon from 'web/components/notifications-icon' import { useIsIframe } from 'web/hooks/use-is-iframe' import { trackCallback } from 'web/lib/service/analytics' +import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' +import { PrivateUser } from 'common/user' function getNavigation() { return [ @@ -42,6 +44,7 @@ export function BottomNavBar() { const currentPage = router.pathname const user = useUser() + const privateUser = usePrivateUser(user?.id) const isIframe = useIsIframe() if (isIframe) { @@ -81,8 +84,12 @@ export function BottomNavBar() { className="w-full select-none py-1 px-3 text-center hover:cursor-pointer hover:bg-indigo-200 hover:text-indigo-700" onClick={() => setSidebarOpen(true)} > - <MenuAlt3Icon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> - More + <MenuAlt3Icon className=" my-1 mx-auto h-6 w-6" aria-hidden="true" /> + {privateUser ? ( + <MoreMenuWithGroupNotifications privateUser={privateUser} /> + ) : ( + 'More' + )} </div> <MobileSidebar @@ -93,6 +100,22 @@ export function BottomNavBar() { ) } +function MoreMenuWithGroupNotifications(props: { privateUser: PrivateUser }) { + const { privateUser } = props + const preferredNotifications = useUnseenPreferredNotifications(privateUser, { + customHref: '/group/', + }) + return ( + <span + className={ + preferredNotifications.length > 0 ? 'font-bold' : 'font-normal' + } + > + More + </span> + ) +} + function NavBarItem(props: { item: Item; currentPage: string }) { const { item, currentPage } = props const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`) From c3bc25a4b92c7ff6b285d937eca678dc0ecb6e4f Mon Sep 17 00:00:00 2001 From: Ben Congdon <ben@congdon.dev> Date: Thu, 7 Jul 2022 15:36:02 -0700 Subject: [PATCH 074/519] Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs --- docs/docs/api.md | 58 ++++++++++++++++ firestore.indexes.json | 22 +++++++ web/lib/firebase/bets.ts | 44 +++++++++++++ web/pages/api/v0/_types.ts | 12 ++++ web/pages/api/v0/_validate.ts | 17 +++++ web/pages/api/v0/bets.ts | 66 +++++++++++++++++++ web/pages/api/v0/markets.ts | 48 +++++++------- .../api/v0/user/[username]/bets/index.ts | 25 +++++++ 8 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 web/pages/api/v0/_validate.ts create mode 100644 web/pages/api/v0/bets.ts create mode 100644 web/pages/api/v0/user/[username]/bets/index.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index a8ac18fe..1cea6027 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -567,6 +567,64 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ ]}' ``` +### `GET /v0/bets` + +Gets a list of bets, ordered by creation date descending. + +Parameters: + +- `username`: Optional. If set, the response will include only bets created by this user. +- `market`: Optional. The slug of a market. If set, the response will only include bets on this market. +- `limit`: Optional. How many bets to return. The maximum and the default is 1000. +- `before`: Optional. The ID of the bet before which the list will start. For + example, if you ask for the most recent 10 bets, and then perform a second + query for 10 more bets with `before=[the id of the 10th bet]`, you will + get bets 11 through 20. + +Requires no authorization. + +- Example request + ``` + https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-california-abolish-daylight-sa + ``` +- Response type: A `Bet[]`. + +- <details><summary>Example response</summary><p> + + ```json + [ + { + "probAfter": 0.44418877319153904, + "shares": -645.8346334931828, + "outcome": "YES", + "contractId": "tgB1XmvFXZNhjr3xMNLp", + "sale": { + "betId": "RcOtarI3d1DUUTjiE0rx", + "amount": 474.9999999999998 + }, + "createdTime": 1644602886293, + "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", + "probBefore": 0.7229189477449224, + "id": "x9eNmCaqQeXW8AgJ8Zmp", + "amount": -499.9999999999998 + }, + { + "probAfter": 0.9901970375647697, + "contractId": "zdeaYVAfHlo9jKzWh57J", + "outcome": "YES", + "amount": 1, + "id": "8PqxKYwXCcLYoXy2m2Nm", + "shares": 1.0049875638533763, + "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", + "probBefore": 0.9900000000000001, + "createdTime": 1644705818872 + } + ] + ``` + + </p> + </details> + ## Changelog - 2022-06-08: Add paging to markets endpoint diff --git a/firestore.indexes.json b/firestore.indexes.json index e0cee632..0a8b14bd 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -559,6 +559,28 @@ "queryScope": "COLLECTION_GROUP" } ] + }, + { + "collectionGroup": "bets", + "fieldPath": "id", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, + { + "arrayConfig": "CONTAINS", + "queryScope": "COLLECTION" + }, + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] } ] } diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts index c442ff73..6fc29d24 100644 --- a/web/lib/firebase/bets.ts +++ b/web/lib/firebase/bets.ts @@ -4,6 +4,13 @@ import { query, where, orderBy, + QueryConstraint, + limit, + startAfter, + doc, + getDocs, + getDoc, + DocumentSnapshot, } from 'firebase/firestore' import { uniq } from 'lodash' @@ -78,6 +85,43 @@ export async function getUserBets( .catch((reason) => reason) } +export async function getBets(options: { + userId?: string + contractId?: string + before?: string + limit: number +}) { + const { userId, contractId, before } = options + + const queryParts: QueryConstraint[] = [ + orderBy('createdTime', 'desc'), + limit(options.limit), + ] + if (userId) { + queryParts.push(where('userId', '==', userId)) + } + if (before) { + let beforeSnap: DocumentSnapshot + if (contractId) { + beforeSnap = await getDoc( + doc(db, 'contracts', contractId, 'bets', before) + ) + } else { + beforeSnap = ( + await getDocs( + query(collectionGroup(db, 'bets'), where('id', '==', before)) + ) + ).docs[0] + } + queryParts.push(startAfter(beforeSnap)) + } + + const querySource = contractId + ? collection(db, 'contracts', contractId, 'bets') + : collectionGroup(db, 'bets') + return await getValues<Bet>(query(querySource, ...queryParts)) +} + export async function getContractsOfUserBets(userId: string) { const bets: Bet[] = await getUserBets(userId, { includeRedemptions: false }) const contractIds = uniq(bets.map((bet) => bet.contractId)) diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index e0012c2b..7f52077d 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -55,6 +55,18 @@ export type ApiError = { error: string } +type ValidationErrorDetail = { + field: string | null + error: string +} +export class ValidationError { + details: ValidationErrorDetail[] + + constructor(details: ValidationErrorDetail[]) { + this.details = details + } +} + export function toLiteMarket(contract: Contract): LiteMarket { const { id, diff --git a/web/pages/api/v0/_validate.ts b/web/pages/api/v0/_validate.ts new file mode 100644 index 00000000..25f5af4e --- /dev/null +++ b/web/pages/api/v0/_validate.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' +import { ValidationError } from './_types' + +export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { + const result = schema.safeParse(val) + if (!result.success) { + const issues = result.error.issues.map((i) => { + return { + field: i.path.join('.') || null, + error: i.message, + } + }) + throw new ValidationError(issues) + } else { + return result.data as z.infer<T> + } +} diff --git a/web/pages/api/v0/bets.ts b/web/pages/api/v0/bets.ts new file mode 100644 index 00000000..c3de3e97 --- /dev/null +++ b/web/pages/api/v0/bets.ts @@ -0,0 +1,66 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { Bet, getBets } from 'web/lib/firebase/bets' +import { getContractFromSlug } from 'web/lib/firebase/contracts' +import { getUserByUsername } from 'web/lib/firebase/users' +import { ApiError, ValidationError } from './_types' +import { z } from 'zod' +import { validate } from './_validate' + +const queryParams = z + .object({ + username: z.string().optional(), + market: z.string().optional(), + limit: z + .number() + .default(1000) + .or(z.string().regex(/\d+/).transform(Number)) + .refine((n) => n >= 0 && n <= 1000, 'Limit must be between 0 and 1000'), + before: z.string().optional(), + }) + .strict() + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<Bet[] | ValidationError | ApiError> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + + let params: z.infer<typeof queryParams> + try { + params = validate(queryParams, req.query) + } catch (e) { + if (e instanceof ValidationError) { + return res.status(400).json(e) + } + console.error(`Unknown error during validation: ${e}`) + return res.status(500).json({ error: 'Unknown error during validation' }) + } + + const { username, market, limit, before } = params + + let userId: string | undefined + if (username) { + const user = await getUserByUsername(username) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + userId = user.id + } + + let contractId: string | undefined + if (market) { + const contract = await getContractFromSlug(market) + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + contractId = contract.id + } + + const bets = await getBets({ userId, contractId, limit, before }) + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(bets) +} diff --git a/web/pages/api/v0/markets.ts b/web/pages/api/v0/markets.ts index fec3cc30..56ecc594 100644 --- a/web/pages/api/v0/markets.ts +++ b/web/pages/api/v0/markets.ts @@ -2,38 +2,40 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { listAllContracts } from 'web/lib/firebase/contracts' import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' -import { toLiteMarket } from './_types' +import { toLiteMarket, ValidationError } from './_types' +import { z } from 'zod' +import { validate } from './_validate' + +const queryParams = z + .object({ + limit: z + .number() + .default(1000) + .or(z.string().regex(/\d+/).transform(Number)) + .refine((n) => n >= 0 && n <= 1000, 'Limit must be between 0 and 1000'), + before: z.string().optional(), + }) + .strict() export default async function handler( req: NextApiRequest, res: NextApiResponse ) { await applyCorsHeaders(req, res, CORS_UNRESTRICTED) - let before: string | undefined - let limit: number | undefined - if (req.query.before != null) { - if (typeof req.query.before !== 'string') { - res.status(400).json({ error: 'before must be null or a market ID.' }) - return + + let params: z.infer<typeof queryParams> + try { + params = validate(queryParams, req.query) + } catch (e) { + if (e instanceof ValidationError) { + return res.status(400).json(e) } - before = req.query.before - } - if (req.query.limit != null) { - if (typeof req.query.limit !== 'string') { - res - .status(400) - .json({ error: 'limit must be null or a number of markets to return.' }) - return - } - limit = parseInt(req.query.limit) - } else { - limit = 1000 - } - if (limit < 1 || limit > 1000) { - res.status(400).json({ error: 'limit must be between 1 and 1000.' }) - return + console.error(`Unknown error during validation: ${e}`) + return res.status(500).json({ error: 'Unknown error during validation' }) } + const { limit, before } = params + try { const contracts = await listAllContracts(limit, before) // Serve from Vercel cache, then update. see https://vercel.com/docs/concepts/functions/edge-caching diff --git a/web/pages/api/v0/user/[username]/bets/index.ts b/web/pages/api/v0/user/[username]/bets/index.ts new file mode 100644 index 00000000..464af52c --- /dev/null +++ b/web/pages/api/v0/user/[username]/bets/index.ts @@ -0,0 +1,25 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { Bet, getUserBets } from 'web/lib/firebase/bets' +import { getUserByUsername } from 'web/lib/firebase/users' +import { ApiError } from '../../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<Bet[] | ApiError> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { username } = req.query + + const user = await getUserByUsername(username as string) + + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + + const bets = await getUserBets(user.id, { includeRedemptions: false }) + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(bets) +} From 53ddb1243be61121485dac57cefde4fa13a334b2 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 7 Jul 2022 15:41:44 -0700 Subject: [PATCH 075/519] Clone missing indexes from firestore --- firestore.indexes.json | 108 ++++++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/firestore.indexes.json b/firestore.indexes.json index 0a8b14bd..12e88033 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -306,24 +306,6 @@ } ] }, - { - "collectionGroup": "txns", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "toId", - "order": "ASCENDING" - }, - { - "fieldPath": "toType", - "order": "ASCENDING" - }, - { - "fieldPath": "createdTime", - "order": "DESCENDING" - } - ] - }, { "collectionGroup": "manalinks", "queryScope": "COLLECTION", @@ -338,6 +320,34 @@ } ] }, + { + "collectionGroup": "notifications", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "isSeen", + "order": "ASCENDING" + }, + { + "fieldPath": "createdTime", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "portfolioHistory", + "queryScope": "COLLECTION_GROUP", + "fields": [ + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "timestamp", + "order": "ASCENDING" + } + ] + }, { "collectionGroup": "portfolioHistory", "queryScope": "COLLECTION", @@ -351,6 +361,24 @@ "order": "ASCENDING" } ] + }, + { + "collectionGroup": "txns", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "toId", + "order": "ASCENDING" + }, + { + "fieldPath": "toType", + "order": "ASCENDING" + }, + { + "fieldPath": "createdTime", + "order": "DESCENDING" + } + ] } ], "fieldOverrides": [ @@ -424,6 +452,28 @@ } ] }, + { + "collectionGroup": "bets", + "fieldPath": "id", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, + { + "arrayConfig": "CONTAINS", + "queryScope": "COLLECTION" + }, + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] + }, { "collectionGroup": "bets", "fieldPath": "userId", @@ -559,28 +609,6 @@ "queryScope": "COLLECTION_GROUP" } ] - }, - { - "collectionGroup": "bets", - "fieldPath": "id", - "indexes": [ - { - "order": "ASCENDING", - "queryScope": "COLLECTION" - }, - { - "order": "DESCENDING", - "queryScope": "COLLECTION" - }, - { - "arrayConfig": "CONTAINS", - "queryScope": "COLLECTION" - }, - { - "order": "ASCENDING", - "queryScope": "COLLECTION_GROUP" - } - ] } ] } From d6136a993763714d3f990543a964834ee6527173 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 17:17:10 -0600 Subject: [PATCH 076/519] Minor notif spacing adjustments --- web/pages/notifications.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index bb495ecd..0924fbdd 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -272,9 +272,11 @@ function IncomeNotificationGroupItem(props: { /> )} <Row className={'items-center text-gray-500 sm:justify-start'}> - <TrendingUpIcon className={'text-primary h-7 w-7'} /> + <TrendingUpIcon + className={'text-primary ml-1 h-7 w-7 flex-shrink-0 sm:ml-2'} + /> <div - className={'flex w-full flex-row flex-wrap pl-1 sm:pl-0'} + className={'ml-2 flex w-full flex-row flex-wrap truncate'} onClick={() => setExpanded(!expanded)} > <div className={'flex w-full flex-row justify-between'}> From b1b016f9e0732f87c68b084e8b0763f1c0e35c0f Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 7 Jul 2022 17:23:13 -0600 Subject: [PATCH 077/519] Enable tipping on group chats w/ notif (#629) --- common/txn.ts | 3 +- functions/src/create-notification.ts | 49 ++++++++++++++----- functions/src/on-create-txn.ts | 65 +++++++++++++++---------- functions/src/utils.ts | 5 ++ web/components/groups/group-chat.tsx | 39 ++++++++++----- web/components/tipper.tsx | 2 + web/hooks/use-tip-txns.ts | 16 ++++-- web/lib/firebase/txns.ts | 17 ++++++- web/pages/[username]/[contractSlug].tsx | 2 +- web/pages/group/[...slugs]/index.tsx | 9 +++- web/pages/notifications.tsx | 18 ++++--- 11 files changed, 160 insertions(+), 65 deletions(-) diff --git a/common/txn.ts b/common/txn.ts index 53b08501..701b67fe 100644 --- a/common/txn.ts +++ b/common/txn.ts @@ -36,8 +36,9 @@ type Tip = { toType: 'USER' category: 'TIP' data: { - contractId: string commentId: string + contractId?: string + groupId?: string } } diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 49bff5f7..519720fd 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -14,6 +14,8 @@ import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' +import { TipTxn } from '../../common/txn' +import { Group } from '../../common/group' const firestore = admin.firestore() type user_to_reason_texts = { @@ -285,15 +287,6 @@ export const createNotification = async ( isSeeOnHref: sourceSlug, } } - const notifyTippedUserOfNewTip = async ( - userToReasonTexts: user_to_reason_texts, - userId: string - ) => { - if (shouldGetNotification(userId, userToReasonTexts)) - userToReasonTexts[userId] = { - reason: 'tip_received', - } - } const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} @@ -346,8 +339,6 @@ export const createNotification = async ( userToReasonTexts, sourceContract.creatorId ) - } else if (sourceType === 'tip' && relatedUserId) { - await notifyTippedUserOfNewTip(userToReasonTexts, relatedUserId) } return userToReasonTexts } @@ -355,3 +346,39 @@ export const createNotification = async ( const userToReasonTexts = await getUsersToNotify() await createUsersNotifications(userToReasonTexts) } + +export const createTipNotification = async ( + fromUser: User, + toUser: User, + tip: TipTxn, + idempotencyKey: string, + commentId: string, + contract?: Contract, + group?: Group +) => { + const slug = group ? group.slug + `#${commentId}` : commentId + + const notificationRef = firestore + .collection(`/users/${toUser.id}/notifications`) + .doc(idempotencyKey) + const notification: Notification = { + id: idempotencyKey, + userId: toUser.id, + reason: 'tip_received', + createdTime: Date.now(), + isSeen: false, + sourceId: tip.id, + sourceType: 'tip', + sourceUpdateType: 'created', + sourceUserName: fromUser.name, + sourceUserUsername: fromUser.username, + sourceUserAvatarUrl: fromUser.avatarUrl, + sourceText: tip.amount.toString(), + sourceContractCreatorUsername: contract?.creatorUsername, + sourceContractTitle: contract?.question, + sourceContractSlug: contract?.slug, + sourceSlug: slug, + sourceTitle: group?.name, + } + return await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/on-create-txn.ts b/functions/src/on-create-txn.ts index d877ecac..b915cfa1 100644 --- a/functions/src/on-create-txn.ts +++ b/functions/src/on-create-txn.ts @@ -1,7 +1,7 @@ import * as functions from 'firebase-functions' -import { Txn } from 'common/txn' -import { getContract, getUser, log } from './utils' -import { createNotification } from './create-notification' +import { TipTxn, Txn } from 'common/txn' +import { getContract, getGroup, getUser, log } from './utils' +import { createTipNotification } from './create-notification' import * as admin from 'firebase-admin' import { Comment } from 'common/comment' @@ -18,7 +18,7 @@ export const onCreateTxn = functions.firestore } }) -async function handleTipTxn(txn: Txn, eventId: string) { +async function handleTipTxn(txn: TipTxn, eventId: string) { // get user sending and receiving tip const [sender, receiver] = await Promise.all([ getUser(txn.fromId), @@ -29,40 +29,53 @@ async function handleTipTxn(txn: Txn, eventId: string) { return } - if (!txn.data?.contractId || !txn.data?.commentId) { - log('No contractId or comment id in tip txn.data') + if (!txn.data?.commentId) { + log('No comment id in tip txn.data') return } + let contract = undefined + let group = undefined + let commentSnapshot = undefined - const contract = await getContract(txn.data.contractId) - if (!contract) { - log('Could not find contract') - return + if (txn.data.contractId) { + contract = await getContract(txn.data.contractId) + if (!contract) { + log('Could not find contract') + return + } + commentSnapshot = await firestore + .collection('contracts') + .doc(contract.id) + .collection('comments') + .doc(txn.data.commentId) + .get() + } else if (txn.data.groupId) { + group = await getGroup(txn.data.groupId) + if (!group) { + log('Could not find group') + return + } + commentSnapshot = await firestore + .collection('groups') + .doc(group.id) + .collection('comments') + .doc(txn.data.commentId) + .get() } - const commentSnapshot = await firestore - .collection('contracts') - .doc(contract.id) - .collection('comments') - .doc(txn.data.commentId) - .get() - if (!commentSnapshot.exists) { + if (!commentSnapshot || !commentSnapshot.exists) { log('Could not find comment') return } const comment = commentSnapshot.data() as Comment - await createNotification( - txn.id, - 'tip', - 'created', + await createTipNotification( sender, + receiver, + txn, eventId, - txn.amount.toString(), + comment.id, contract, - 'comment', - receiver.id, - txn.data?.commentId, - comment.text + group ) } diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 29f0db00..0414b01e 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -3,6 +3,7 @@ import * as admin from 'firebase-admin' import { chunk } from 'lodash' import { Contract } from '../../common/contract' import { PrivateUser, User } from '../../common/user' +import { Group } from '../../common/group' export const log = (...args: unknown[]) => { console.log(`[${new Date().toISOString()}]`, ...args) @@ -66,6 +67,10 @@ export const getContract = (contractId: string) => { return getDoc<Contract>('contracts', contractId) } +export const getGroup = (groupId: string) => { + return getDoc<Group>('groups', groupId) +} + export const getUser = (userId: string) => { return getDoc<User>('users', userId) } diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 1298065d..6e82b05c 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -18,13 +18,18 @@ import { UserLink } from 'web/components/user-page' import { groupPath } from 'web/lib/firebase/groups' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' +import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' +import { Tipper } from 'web/components/tipper' +import { sum } from 'lodash' +import { formatMoney } from 'common/util/format' export function GroupChat(props: { messages: Comment[] user: User | null | undefined group: Group + tips: CommentTipMap }) { - const { messages, user, group } = props + const { messages, user, group, tips } = props const [messageText, setMessageText] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [scrollToBottomRef, setScrollToBottomRef] = @@ -117,6 +122,7 @@ export function GroupChat(props: { ? setScrollToMessageRef : undefined } + tips={tips[message.id] ?? {}} /> ))} {messages.length === 0 && ( @@ -166,8 +172,9 @@ const GroupMessage = memo(function GroupMessage_(props: { onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void highlight?: boolean + tips: CommentTips }) { - const { comment, onReplyClick, group, setRef, highlight, user } = props + const { comment, onReplyClick, group, setRef, highlight, user, tips } = props const { text, userUsername, userName, userAvatarUrl, createdTime } = comment const isCreatorsComment = user && comment.userId === user.id return ( @@ -209,16 +216,24 @@ const GroupMessage = memo(function GroupMessage_(props: { shouldTruncate={false} /> </Row> - {!isCreatorsComment && onReplyClick && ( - <button - className={ - 'self-start py-1 text-xs font-bold text-gray-500 hover:underline' - } - onClick={() => onReplyClick(comment)} - > - Reply - </button> - )} + <Row> + {!isCreatorsComment && onReplyClick && ( + <button + className={ + 'self-start py-1 text-xs font-bold text-gray-500 hover:underline' + } + onClick={() => onReplyClick(comment)} + > + Reply + </button> + )} + {isCreatorsComment && sum(Object.values(tips)) > 0 && ( + <span className={'text-primary'}> + {formatMoney(sum(Object.values(tips)))} + </span> + )} + {!isCreatorsComment && <Tipper comment={comment} tips={tips} />} + </Row> </Col> ) }) diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 64bad4eb..6f7dfbcb 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -53,6 +53,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { data: { contractId: comment.contractId, commentId: comment.id, + groupId: comment.groupId, }, description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`, }) @@ -60,6 +61,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { track('send comment tip', { contractId: comment.contractId, commentId: comment.id, + groupId: comment.groupId, amount: change, fromId: user.id, toId: comment.userId, diff --git a/web/hooks/use-tip-txns.ts b/web/hooks/use-tip-txns.ts index 13ef3d34..50542402 100644 --- a/web/hooks/use-tip-txns.ts +++ b/web/hooks/use-tip-txns.ts @@ -1,17 +1,25 @@ import { TipTxn } from 'common/txn' import { groupBy, mapValues, sumBy } from 'lodash' import { useEffect, useMemo, useState } from 'react' -import { listenForTipTxns } from 'web/lib/firebase/txns' +import { + listenForTipTxns, + listenForTipTxnsOnGroup, +} from 'web/lib/firebase/txns' export type CommentTips = { [userId: string]: number } export type CommentTipMap = { [commentId: string]: CommentTips } -export function useTipTxns(contractId: string): CommentTipMap { +export function useTipTxns(on: { + contractId?: string + groupId?: string +}): CommentTipMap { const [txns, setTxns] = useState<TipTxn[]>([]) + const { contractId, groupId } = on useEffect(() => { - return listenForTipTxns(contractId, setTxns) - }, [contractId, setTxns]) + if (contractId) return listenForTipTxns(contractId, setTxns) + if (groupId) return listenForTipTxnsOnGroup(groupId, setTxns) + }, [contractId, groupId, setTxns]) return useMemo(() => { const byComment = groupBy(txns, 'data.commentId') diff --git a/web/lib/firebase/txns.ts b/web/lib/firebase/txns.ts index 17e9a09b..88ab1352 100644 --- a/web/lib/firebase/txns.ts +++ b/web/lib/firebase/txns.ts @@ -27,18 +27,31 @@ export function getAllCharityTxns() { return getValues<DonationTxn>(charitiesQuery) } -const getTipsQuery = (contractId: string) => +const getTipsOnContractQuery = (contractId: string) => query( txns, where('category', '==', 'TIP'), where('data.contractId', '==', contractId) ) +const getTipsOnGroupQuery = (groupId: string) => + query( + txns, + where('category', '==', 'TIP'), + where('data.groupId', '==', groupId) + ) + export function listenForTipTxns( contractId: string, setTxns: (txns: TipTxn[]) => void ) { - return listenForValues<TipTxn>(getTipsQuery(contractId), setTxns) + return listenForValues<TipTxn>(getTipsOnContractQuery(contractId), setTxns) +} +export function listenForTipTxnsOnGroup( + groupId: string, + setTxns: (txns: TipTxn[]) => void +) { + return listenForValues<TipTxn>(getTipsOnGroupQuery(groupId), setTxns) } // Find all manalink Txns that are from or to this user diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 2576c2e3..e33c116e 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -124,7 +124,7 @@ export function ContractPageContent( // Sort for now to see if bug is fixed. comments.sort((c1, c2) => c1.createdTime - c2.createdTime) - const tips = useTipTxns(contract.id) + const tips = useTipTxns({ contractId: contract.id }) const user = useUser() const { width, height } = useWindowSize() diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 73d7819a..dec25ab1 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -53,6 +53,7 @@ import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' +import { useTipTxns } from 'web/hooks/use-tip-txns' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -149,6 +150,7 @@ export default function GroupPage(props: { const group = useGroup(props.group?.id) ?? props.group const [contracts, setContracts] = useState<Contract[] | undefined>(undefined) const [query, setQuery] = useState('') + const tips = useTipTxns({ groupId: group?.id }) const messages = useCommentsOnGroup(group?.id) const debouncedQuery = debounce(setQuery, 50) @@ -263,7 +265,12 @@ export default function GroupPage(props: { { title: 'Chat', content: messages ? ( - <GroupChat messages={messages} user={user} group={group} /> + <GroupChat + messages={messages} + user={user} + group={group} + tips={tips} + /> ) : ( <LoadingIndicator /> ), diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 0924fbdd..3a8e4bc0 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -384,7 +384,10 @@ function IncomeNotificationItem(props: { </div> <span className={'flex truncate'}> {getReasonForShowingIncomeNotification(true)} - <QuestionLink notification={notification} ignoreClick={true} /> + <QuestionOrGroupLink + notification={notification} + ignoreClick={true} + /> </span> </div> </div> @@ -425,7 +428,7 @@ function IncomeNotificationItem(props: { /> ))} {getReasonForShowingIncomeNotification(false)} {' on'} - <QuestionLink notification={notification} /> + <QuestionOrGroupLink notification={notification} /> </span> </div> </Row> @@ -481,7 +484,7 @@ function NotificationGroupItem(props: { <div className={'flex w-full flex-row justify-between'}> <div className={'ml-2'}> Activity on - <QuestionLink notification={notifications[0]} /> + <QuestionOrGroupLink notification={notifications[0]} /> </div> <div className={'hidden sm:inline-block'}> <RelativeTimestamp time={notifications[0].createdTime} /> @@ -666,7 +669,7 @@ function NotificationItem(props: { {isChildOfGroup ? ( <RelativeTimestamp time={notification.createdTime} /> ) : ( - <QuestionLink notification={notification} /> + <QuestionOrGroupLink notification={notification} /> )} </div> </div> @@ -705,7 +708,7 @@ export const setNotificationsAsSeen = (notifications: Notification[]) => { return notifications } -function QuestionLink(props: { +function QuestionOrGroupLink(props: { notification: Notification ignoreClick?: boolean }) { @@ -733,7 +736,7 @@ function QuestionLink(props: { href={ sourceContractCreatorUsername ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : sourceType === 'group' && sourceSlug + : (sourceType === 'group' || sourceType === 'tip') && sourceSlug ? `${groupPath(sourceSlug)}` : '' } @@ -771,8 +774,9 @@ function getSourceUrl(notification: Notification) { sourceType === 'user' ) return `/${sourceContractCreatorUsername}/${sourceContractSlug}` - if (sourceType === 'tip') + if (sourceType === 'tip' && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` + if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}` if (sourceContractCreatorUsername && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( sourceId ?? '', From 50c5f8b6ebed75ddb20895a66a5434ef4cc9945a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 8 Jul 2022 12:34:16 -0400 Subject: [PATCH 078/519] reenable fees on share sales; rename getCpmmFees() --- common/calculate-cpmm.ts | 24 ++++++++++-------------- web/components/bet-panel.tsx | 4 ++-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index e7d56ba3..92d95251 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -1,7 +1,7 @@ import { sum, groupBy, mapValues, sumBy, partition } from 'lodash' import { CPMMContract } from './contract' -import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees' +import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { LiquidityProvision } from './liquidity-provision' import { addObjects } from './util/object' @@ -58,7 +58,7 @@ function calculateCpmmShares( : n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p)) } -export function getCpmmLiquidityFee( +export function getCpmmFees( contract: CPMMContract, bet: number, outcome: string @@ -83,7 +83,7 @@ export function calculateCpmmSharesAfterFee( outcome: string ) { const { pool, p } = contract - const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome) + const { remainingBet } = getCpmmFees(contract, bet, outcome) return calculateCpmmShares(pool, p, remainingBet, outcome) } @@ -94,9 +94,7 @@ export function calculateCpmmPurchase( outcome: string ) { const { pool, p } = contract - const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome) - // const remainingBet = bet - // const fees = noFees + const { remainingBet, fees } = getCpmmFees(contract, bet, outcome) const shares = calculateCpmmShares(pool, p, remainingBet, outcome) const { YES: y, NO: n } = pool @@ -176,19 +174,17 @@ export function calculateCpmmSale( throw new Error('Cannot sell non-positive shares') } - const saleValue = calculateCpmmShareValue( + const rawSaleValue = calculateCpmmShareValue( contract, shares, outcome as 'YES' | 'NO' ) - const fees = noFees - - // const { fees, remainingBet: saleValue } = getCpmmLiquidityFee( - // contract, - // rawSaleValue, - // outcome === 'YES' ? 'NO' : 'YES' - // ) + const { fees, remainingBet: saleValue } = getCpmmFees( + contract, + rawSaleValue, + outcome === 'YES' ? 'NO' : 'YES' + ) const { pool } = contract const { YES: y, NO: n } = pool diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index f76117b9..a43f6f12 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -37,7 +37,7 @@ import { useUserContractBets } from 'web/hooks/use-user-bets' import { calculateCpmmSale, getCpmmProbability, - getCpmmLiquidityFee, + getCpmmFees, } from 'common/calculate-cpmm' import { getFormattedMappedValue } from 'common/pseudo-numeric' import { SellRow } from './sell-row' @@ -302,7 +302,7 @@ function BuyPanel(props: { const cpmmFees = contract.mechanism === 'cpmm-1' && - getCpmmLiquidityFee(contract, betAmount ?? 0, betChoice ?? 'YES').totalFees + getCpmmFees(contract, betAmount ?? 0, betChoice ?? 'YES').totalFees const dpmTooltip = contract.mechanism === 'dpm-2' From 93b293ca0e2f984aa1dec93ec1dd6566c0113d5f Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 8 Jul 2022 12:50:46 -0400 Subject: [PATCH 079/519] remove quick betting for FR markets --- web/components/contract/quick-bet.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index adbcc456..76ee7536 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -131,6 +131,13 @@ export function QuickBet(props: { contract: Contract; user: User }) { }) } + if (outcomeType === 'FREE_RESPONSE') + return ( + <Col className="relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle"> + <QuickOutcomeView contract={contract} previewProb={previewProb} /> + </Col> + ) + return ( <Col className={clsx( From ed0544212d133bb6af7447f9739beac6a2a72899 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 8 Jul 2022 15:00:03 -0700 Subject: [PATCH 080/519] Migrate changeUserInfo function to v2 (#626) --- functions/src/change-user-info.ts | 61 ++++++++++--------------------- functions/src/index.ts | 3 +- web/lib/firebase/api-call.ts | 4 ++ web/lib/firebase/fn-call.ts | 10 ----- web/pages/profile.tsx | 19 ++++------ 5 files changed, 32 insertions(+), 65 deletions(-) diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts index 118d5c67..aa041856 100644 --- a/functions/src/change-user-info.ts +++ b/functions/src/change-user-info.ts @@ -1,5 +1,5 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { getUser } from './utils' import { Contract } from '../../common/contract' @@ -11,37 +11,23 @@ import { } from '../../common/util/clean-username' import { removeUndefinedProps } from '../../common/util/object' import { Answer } from '../../common/answer' +import { APIError, newEndpoint, validate } from './api' -export const changeUserInfo = functions - .runWith({ minInstances: 1 }) - .https.onCall( - async ( - data: { - username?: string - name?: string - avatarUrl?: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + username: z.string().optional(), + name: z.string().optional(), + avatarUrl: z.string().optional(), +}) - const user = await getUser(userId) - if (!user) return { status: 'error', message: 'User not found' } +export const changeuserinfo = newEndpoint({}, async (req, auth) => { + const { username, name, avatarUrl } = validate(bodySchema, req.body) - const { username, name, avatarUrl } = data + const user = await getUser(auth.uid) + if (!user) throw new APIError(400, 'User not found') - return await changeUser(user, { username, name, avatarUrl }) - .then(() => { - console.log('succesfully changed', user.username, 'to', data) - return { status: 'success' } - }) - .catch((e) => { - console.log('Error', e.message) - return { status: 'error', message: e.message } - }) - } - ) + await changeUser(user, { username, name, avatarUrl }) + return { message: 'Successfully changed user info.' } +}) export const changeUser = async ( user: User, @@ -55,14 +41,14 @@ export const changeUser = async ( if (update.username) { update.username = cleanUsername(update.username) if (!update.username) { - throw new Error('Invalid username') + throw new APIError(400, 'Invalid username') } const sameNameUser = await transaction.get( firestore.collection('users').where('username', '==', update.username) ) if (!sameNameUser.empty) { - throw new Error('Username already exists') + throw new APIError(400, 'Username already exists') } } @@ -104,17 +90,10 @@ export const changeUser = async ( ) const answerUpdate: Partial<Answer> = removeUndefinedProps(update) - await transaction.update(userRef, userUpdate) - - await Promise.all( - commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate)) - ) - - await Promise.all( - answerSnap.docs.map((d) => transaction.update(d.ref, answerUpdate)) - ) - - await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate)) + transaction.update(userRef, userUpdate) + commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate)) + answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate)) + contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate)) }) } diff --git a/functions/src/index.ts b/functions/src/index.ts index 8d1756f2..08639c7c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -// export * from './keep-awake' export * from './claim-manalink' export * from './transact' export * from './stripe' @@ -16,7 +15,6 @@ export * from './unsubscribe' export * from './update-metrics' export * from './update-stats' export * from './backup-db' -export * from './change-user-info' export * from './market-close-notifications' export * from './add-liquidity' export * from './on-create-answer' @@ -33,6 +31,7 @@ export * from './on-create-txn' // v2 export * from './health' +export * from './change-user-info' export * from './place-bet' export * from './sell-bet' export * from './sell-shares' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index db41e592..341e92b0 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -50,6 +50,10 @@ export function getFunctionUrl(name: string) { } } +export function changeUserInfo(params: any) { + return call(getFunctionUrl('changeuserinfo'), 'POST', params) +} + export function createMarket(params: any) { return call(getFunctionUrl('createmarket'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index ce78ac3a..b9b771b5 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -42,16 +42,6 @@ export const createUser: () => Promise<User | null> = () => { .catch(() => null) } -export const changeUserInfo = (data: { - username?: string - name?: string - avatarUrl?: string -}) => { - return cloudFunction('changeUserInfo')(data) - .then((r) => r.data as { status: string; message?: string }) - .catch((e) => ({ status: 'error', message: e.message })) -} - export const addLiquidity = (data: { amount: number; contractId: string }) => { return cloudFunction('addLiquidity')(data) .then((r) => r.data as { status: string }) diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index ac06eaf2..62177825 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -9,7 +9,7 @@ import { Title } from 'web/components/title' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' -import { changeUserInfo } from 'web/lib/firebase/fn-call' +import { changeUserInfo } from 'web/lib/firebase/api-call' import { uploadImage } from 'web/lib/firebase/storage' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' @@ -85,12 +85,9 @@ export default function ProfilePage() { if (newName) { setName(newName) - - await changeUserInfo({ name: newName }) - .catch(() => ({ status: 'error' })) - .then((r) => { - if (r.status === 'error') setName(user?.name || '') - }) + await changeUserInfo({ name: newName }).catch((_) => + setName(user?.name || '') + ) } else { setName(user?.name || '') } @@ -101,11 +98,9 @@ export default function ProfilePage() { if (newUsername) { setUsername(newUsername) - await changeUserInfo({ username: newUsername }) - .catch(() => ({ status: 'error' })) - .then((r) => { - if (r.status === 'error') setUsername(user?.username || '') - }) + await changeUserInfo({ username: newUsername }).catch((_) => + setUsername(user?.username || '') + ) } else { setUsername(user?.username || '') } From d9f42caa6a87acc643769475ff1e5a21b0cddc60 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 8 Jul 2022 15:08:17 -0700 Subject: [PATCH 081/519] Migrate addLiquidity and withdrawLiquidity functions to v2 (#627) --- functions/src/add-liquidity.ts | 149 +++++++++++----------- functions/src/index.ts | 2 +- functions/src/withdraw-liquidity.ts | 188 ++++++++++++---------------- web/components/liquidity-panel.tsx | 14 +-- web/lib/firebase/api-call.ts | 8 ++ web/lib/firebase/fn-call.ts | 11 -- 6 files changed, 167 insertions(+), 205 deletions(-) diff --git a/functions/src/add-liquidity.ts b/functions/src/add-liquidity.ts index eca0a056..3ef453c2 100644 --- a/functions/src/add-liquidity.ts +++ b/functions/src/add-liquidity.ts @@ -1,105 +1,96 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { removeUndefinedProps } from '../../common/util/object' import { redeemShares } from './redeem-shares' import { getNewLiquidityProvision } from '../../common/add-liquidity' +import { APIError, newEndpoint, validate } from './api' -export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall( - async ( - data: { - amount: number - contractId: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + contractId: z.string(), + amount: z.number().gt(0), +}) - const { amount, contractId } = data +export const addliquidity = newEndpoint({}, async (req, auth) => { + const { amount, contractId } = validate(bodySchema, req.body) - if (amount <= 0 || isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } + if (!isFinite(amount)) throw new APIError(400, 'Invalid amount') - // run as transaction to prevent race conditions - return await firestore - .runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) - return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User + // run as transaction to prevent race conditions + return await firestore + .runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${auth.uid}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) throw new APIError(400, 'User not found') + const user = userSnap.data() as User - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - if ( - contract.mechanism !== 'cpmm-1' || - (contract.outcomeType !== 'BINARY' && - contract.outcomeType !== 'PSEUDO_NUMERIC') - ) - return { status: 'error', message: 'Invalid contract' } + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Invalid contract') + const contract = contractSnap.data() as Contract + if ( + contract.mechanism !== 'cpmm-1' || + (contract.outcomeType !== 'BINARY' && + contract.outcomeType !== 'PSEUDO_NUMERIC') + ) + throw new APIError(400, 'Invalid contract') - const { closeTime } = contract - if (closeTime && Date.now() > closeTime) - return { status: 'error', message: 'Trading is closed' } + const { closeTime } = contract + if (closeTime && Date.now() > closeTime) + throw new APIError(400, 'Trading is closed') - if (user.balance < amount) - return { status: 'error', message: 'Insufficient balance' } + if (user.balance < amount) throw new APIError(400, 'Insufficient balance') - const newLiquidityProvisionDoc = firestore - .collection(`contracts/${contractId}/liquidity`) - .doc() + const newLiquidityProvisionDoc = firestore + .collection(`contracts/${contractId}/liquidity`) + .doc() - const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = - getNewLiquidityProvision( - user, - amount, - contract, - newLiquidityProvisionDoc.id - ) - - if (newP !== undefined && !isFinite(newP)) { - return { - status: 'error', - message: 'Liquidity injection rejected due to overflow error.', - } - } - - transaction.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - p: newP, - totalLiquidity: newTotalLiquidity, - }) + const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = + getNewLiquidityProvision( + user, + amount, + contract, + newLiquidityProvisionDoc.id ) - const newBalance = user.balance - amount - const newTotalDeposits = user.totalDeposits - amount - - if (!isFinite(newBalance)) { - throw new Error('Invalid user balance for ' + user.username) + if (newP !== undefined && !isFinite(newP)) { + return { + status: 'error', + message: 'Liquidity injection rejected due to overflow error.', } + } - transaction.update(userDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + p: newP, + totalLiquidity: newTotalLiquidity, }) + ) - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + const newBalance = user.balance - amount + const newTotalDeposits = user.totalDeposits - amount - return { status: 'success', newLiquidityProvision } + if (!isFinite(newBalance)) { + throw new APIError(500, 'Invalid user balance for ' + user.username) + } + + transaction.update(userDoc, { + balance: newBalance, + totalDeposits: newTotalDeposits, }) - .then(async (result) => { - await redeemShares(userId, contractId) - return result - }) - } -) + + transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + + return newLiquidityProvision + }) + .then(async (result) => { + await redeemShares(auth.uid, contractId) + return result + }) +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 08639c7c..2800bb7d 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -16,7 +16,6 @@ export * from './update-metrics' export * from './update-stats' export * from './backup-db' export * from './market-close-notifications' -export * from './add-liquidity' export * from './on-create-answer' export * from './on-update-contract' export * from './on-create-contract' @@ -36,6 +35,7 @@ export * from './place-bet' export * from './sell-bet' export * from './sell-shares' export * from './create-contract' +export * from './add-liquidity' export * from './withdraw-liquidity' export * from './create-group' export * from './resolve-market' diff --git a/functions/src/withdraw-liquidity.ts b/functions/src/withdraw-liquidity.ts index cc8c84cf..1bdb19de 100644 --- a/functions/src/withdraw-liquidity.ts +++ b/functions/src/withdraw-liquidity.ts @@ -1,5 +1,5 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { CPMMContract } from '../../common/contract' import { User } from '../../common/user' @@ -10,129 +10,107 @@ import { Bet } from '../../common/bet' import { getProbability } from '../../common/calculate' import { noFees } from '../../common/fees' -import { APIError } from './api' +import { APIError, newEndpoint, validate } from './api' import { redeemShares } from './redeem-shares' -export const withdrawLiquidity = functions - .runWith({ minInstances: 1 }) - .https.onCall( - async ( - data: { - contractId: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + contractId: z.string(), +}) - const { contractId } = data - if (!contractId) - return { status: 'error', message: 'Missing contract id' } +export const withdrawliquidity = newEndpoint({}, async (req, auth) => { + const { contractId } = validate(bodySchema, req.body) - return await firestore - .runTransaction(async (trans) => { - const lpDoc = firestore.doc(`users/${userId}`) - const lpSnap = await trans.get(lpDoc) - if (!lpSnap.exists) throw new APIError(400, 'User not found.') - const lp = lpSnap.data() as User + return await firestore + .runTransaction(async (trans) => { + const lpDoc = firestore.doc(`users/${auth.uid}`) + const lpSnap = await trans.get(lpDoc) + if (!lpSnap.exists) throw new APIError(400, 'User not found.') + const lp = lpSnap.data() as User - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await trans.get(contractDoc) - if (!contractSnap.exists) - throw new APIError(400, 'Contract not found.') - const contract = contractSnap.data() as CPMMContract + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await trans.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') + const contract = contractSnap.data() as CPMMContract - const liquidityCollection = firestore.collection( - `contracts/${contractId}/liquidity` - ) + const liquidityCollection = firestore.collection( + `contracts/${contractId}/liquidity` + ) - const liquiditiesSnap = await trans.get(liquidityCollection) + const liquiditiesSnap = await trans.get(liquidityCollection) - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision + ) - const userShares = getUserLiquidityShares( - userId, - contract, - liquidities - ) + const userShares = getUserLiquidityShares(auth.uid, contract, liquidities) - // zero all added amounts for now - // can add support for partial withdrawals in the future - liquiditiesSnap.docs - .filter( - (_, i) => - !liquidities[i].isAnte && liquidities[i].userId === userId - ) - .forEach((doc) => trans.update(doc.ref, { amount: 0 })) + // zero all added amounts for now + // can add support for partial withdrawals in the future + liquiditiesSnap.docs + .filter( + (_, i) => !liquidities[i].isAnte && liquidities[i].userId === auth.uid + ) + .forEach((doc) => trans.update(doc.ref, { amount: 0 })) - const payout = Math.min(...Object.values(userShares)) - if (payout <= 0) return {} + const payout = Math.min(...Object.values(userShares)) + if (payout <= 0) return {} - const newBalance = lp.balance + payout - const newTotalDeposits = lp.totalDeposits + payout - trans.update(lpDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, - } as Partial<User>) + const newBalance = lp.balance + payout + const newTotalDeposits = lp.totalDeposits + payout + trans.update(lpDoc, { + balance: newBalance, + totalDeposits: newTotalDeposits, + } as Partial<User>) - const newPool = subtractObjects(contract.pool, userShares) + const newPool = subtractObjects(contract.pool, userShares) - const minPoolShares = Math.min(...Object.values(newPool)) - const adjustedTotal = contract.totalLiquidity - payout + const minPoolShares = Math.min(...Object.values(newPool)) + const adjustedTotal = contract.totalLiquidity - payout - // total liquidity is a bogus number; use minPoolShares to prevent from going negative - const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) + // total liquidity is a bogus number; use minPoolShares to prevent from going negative + const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) - trans.update(contractDoc, { - pool: newPool, - totalLiquidity: newTotalLiquidity, - }) + trans.update(contractDoc, { + pool: newPool, + totalLiquidity: newTotalLiquidity, + }) - const prob = getProbability(contract) + const prob = getProbability(contract) - // surplus shares become user's bets - const bets = Object.entries(userShares) - .map(([outcome, shares]) => - shares - payout < 1 // don't create bet if less than 1 share - ? undefined - : ({ - userId: userId, - contractId: contract.id, - amount: - (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), - shares: shares - payout, - outcome, - probBefore: prob, - probAfter: prob, - createdTime: Date.now(), - isLiquidityProvision: true, - fees: noFees, - } as Omit<Bet, 'id'>) - ) - .filter((x) => x !== undefined) + // surplus shares become user's bets + const bets = Object.entries(userShares) + .map(([outcome, shares]) => + shares - payout < 1 // don't create bet if less than 1 share + ? undefined + : ({ + userId: auth.uid, + contractId: contract.id, + amount: + (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), + shares: shares - payout, + outcome, + probBefore: prob, + probAfter: prob, + createdTime: Date.now(), + isLiquidityProvision: true, + fees: noFees, + } as Omit<Bet, 'id'>) + ) + .filter((x) => x !== undefined) - for (const bet of bets) { - const doc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - trans.create(doc, { id: doc.id, ...bet }) - } + for (const bet of bets) { + const doc = firestore.collection(`contracts/${contract.id}/bets`).doc() + trans.create(doc, { id: doc.id, ...bet }) + } - return userShares - }) - .then(async (result) => { - // redeem surplus bet with pre-existing bets - await redeemShares(userId, contractId) - - console.log('userid', userId, 'withdraws', result) - return { status: 'success', userShares: result } - }) - .catch((e) => { - return { status: 'error', message: e.message } - }) - } - ) + return userShares + }) + .then(async (result) => { + // redeem surplus bet with pre-existing bets + await redeemShares(auth.uid, contractId) + console.log('userid', auth.uid, 'withdraws', result) + return result + }) +}) const firestore = admin.firestore() diff --git a/web/components/liquidity-panel.tsx b/web/components/liquidity-panel.tsx index 33efb335..d1e066be 100644 --- a/web/components/liquidity-panel.tsx +++ b/web/components/liquidity-panel.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import { CPMMContract } from 'common/contract' import { formatMoney } from 'common/util/format' import { useUser } from 'web/hooks/use-user' -import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/fn-call' +import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api-call' import { AmountInput } from './amount-input' import { Row } from './layout/row' import { useUserLiquidity } from 'web/hooks/use-liquidity' @@ -90,14 +90,10 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) { setIsSuccess(false) addLiquidity({ amount, contractId }) - .then((r) => { - if (r.status === 'success') { - setIsSuccess(true) - setError(undefined) - setIsLoading(false) - } else { - setError('Server error') - } + .then((_) => { + setIsSuccess(true) + setError(undefined) + setIsLoading(false) }) .catch((_) => setError('Server error')) diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 341e92b0..d169ea72 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -54,6 +54,14 @@ export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } +export function addLiquidity(params: any) { + return call(getFunctionUrl('addliquidity'), 'POST', params) +} + +export function withdrawLiquidity(params: any) { + return call(getFunctionUrl('withdrawliquidity'), 'POST', params) +} + export function createMarket(params: any) { return call(getFunctionUrl('createmarket'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index b9b771b5..3b16af70 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -9,11 +9,6 @@ import { safeLocalStorage } from '../util/local' export const cloudFunction = <RequestData, ResponseData>(name: string) => httpsCallable<RequestData, ResponseData>(functions, name) -export const withdrawLiquidity = cloudFunction< - { contractId: string }, - { status: 'error' | 'success'; userShares: { [outcome: string]: number } } ->('withdrawLiquidity') - export const transact = cloudFunction< Omit<Txn, 'id' | 'createdTime'>, { status: 'error' | 'success'; message?: string; txn?: Txn } @@ -42,12 +37,6 @@ export const createUser: () => Promise<User | null> = () => { .catch(() => null) } -export const addLiquidity = (data: { amount: number; contractId: string }) => { - return cloudFunction('addLiquidity')(data) - .then((r) => r.data as { status: string }) - .catch((e) => ({ status: 'error', message: e.message })) -} - export const claimManalink = cloudFunction< string, { status: 'error' | 'success'; message?: string } From fdde73710ee18400e22626b8fa99f3b55838ba18 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 8 Jul 2022 15:28:04 -0700 Subject: [PATCH 082/519] Migrate claimManalink function to v2 (#628) * Implement helpful `toString` on client `APIError` * Migrate claimManalink function to v2 --- functions/src/claim-manalink.ts | 166 ++++++++++++++++---------------- functions/src/index.ts | 2 +- web/lib/firebase/api-call.ts | 7 ++ web/lib/firebase/fn-call.ts | 5 - web/pages/link/[slug].tsx | 7 +- 5 files changed, 94 insertions(+), 93 deletions(-) diff --git a/functions/src/claim-manalink.ts b/functions/src/claim-manalink.ts index 4bcd8b16..3822bbf7 100644 --- a/functions/src/claim-manalink.ts +++ b/functions/src/claim-manalink.ts @@ -1,102 +1,104 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { User } from 'common/user' import { Manalink } from 'common/manalink' import { runTxn, TxnData } from './transact' +import { APIError, newEndpoint, validate } from './api' -export const claimManalink = functions - .runWith({ minInstances: 1 }) - .https.onCall(async (slug: string, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + slug: z.string(), +}) - // Run as transaction to prevent race conditions. - return await firestore.runTransaction(async (transaction) => { - // Look up the manalink - const manalinkDoc = firestore.doc(`manalinks/${slug}`) - const manalinkSnap = await transaction.get(manalinkDoc) - if (!manalinkSnap.exists) { - return { status: 'error', message: 'Manalink not found' } - } - const manalink = manalinkSnap.data() as Manalink +export const claimmanalink = newEndpoint({}, async (req, auth) => { + const { slug } = validate(bodySchema, req.body) - const { amount, fromId, claimedUserIds } = manalink + // Run as transaction to prevent race conditions. + return await firestore.runTransaction(async (transaction) => { + // Look up the manalink + const manalinkDoc = firestore.doc(`manalinks/${slug}`) + const manalinkSnap = await transaction.get(manalinkDoc) + if (!manalinkSnap.exists) { + throw new APIError(400, 'Manalink not found') + } + const manalink = manalinkSnap.data() as Manalink - if (amount <= 0 || isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } + const { amount, fromId, claimedUserIds } = manalink - const fromDoc = firestore.doc(`users/${fromId}`) - const fromSnap = await transaction.get(fromDoc) - if (!fromSnap.exists) { - return { status: 'error', message: `User ${fromId} not found` } - } - const fromUser = fromSnap.data() as User + if (amount <= 0 || isNaN(amount) || !isFinite(amount)) + throw new APIError(500, 'Invalid amount') - // Only permit one redemption per user per link - if (claimedUserIds.includes(userId)) { - return { - status: 'error', - message: `${fromUser.name} already redeemed manalink ${slug}`, - } - } + const fromDoc = firestore.doc(`users/${fromId}`) + const fromSnap = await transaction.get(fromDoc) + if (!fromSnap.exists) { + throw new APIError(500, `User ${fromId} not found`) + } + const fromUser = fromSnap.data() as User - // Disallow expired or maxed out links - if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { - return { - status: 'error', - message: `Manalink ${slug} expired on ${new Date( - manalink.expiresTime - ).toLocaleString()}`, - } - } - if ( - manalink.maxUses != null && - manalink.maxUses <= manalink.claims.length - ) { - return { - status: 'error', - message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`, - } - } + // Only permit one redemption per user per link + if (claimedUserIds.includes(auth.uid)) { + throw new APIError(400, `You already redeemed manalink ${slug}`) + } - if (fromUser.balance < amount) { - return { - status: 'error', - message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `, - } - } + // Disallow expired or maxed out links + if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { + throw new APIError( + 400, + `Manalink ${slug} expired on ${new Date( + manalink.expiresTime + ).toLocaleString()}` + ) + } + if ( + manalink.maxUses != null && + manalink.maxUses <= manalink.claims.length + ) { + throw new APIError( + 400, + `Manalink ${slug} has reached its max uses of ${manalink.maxUses}` + ) + } - // Actually execute the txn - const data: TxnData = { - fromId, - fromType: 'USER', - toId: userId, - toType: 'USER', - amount, - token: 'M$', - category: 'MANALINK', - description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`, - } - const result = await runTxn(transaction, data) - const txnId = result.txn?.id - if (!txnId) { - return { status: 'error', message: result.message } - } + if (fromUser.balance < amount) { + throw new APIError( + 400, + `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} ` + ) + } - // Update the manalink object with this info - const claim = { - toId: userId, - txnId, - claimedTime: Date.now(), - } - transaction.update(manalinkDoc, { - claimedUserIds: [...claimedUserIds, userId], - claims: [...manalink.claims, claim], - }) + // Actually execute the txn + const data: TxnData = { + fromId, + fromType: 'USER', + toId: auth.uid, + toType: 'USER', + amount, + token: 'M$', + category: 'MANALINK', + description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${auth.uid}`, + } + const result = await runTxn(transaction, data) + const txnId = result.txn?.id + if (!txnId) { + throw new APIError( + 500, + result.message ?? 'An error occurred posting the transaction.' + ) + } - return { status: 'success', message: 'Manalink claimed' } + // Update the manalink object with this info + const claim = { + toId: auth.uid, + txnId, + claimedTime: Date.now(), + } + transaction.update(manalinkDoc, { + claimedUserIds: [...claimedUserIds, auth.uid], + claims: [...manalink.claims, claim], }) + + return { message: 'Manalink claimed' } }) +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 2800bb7d..7c839396 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './claim-manalink' export * from './transact' export * from './stripe' export * from './create-user' @@ -34,6 +33,7 @@ export * from './change-user-info' export * from './place-bet' export * from './sell-bet' export * from './sell-shares' +export * from './claim-manalink' export * from './create-contract' export * from './add-liquidity' export * from './withdraw-liquidity' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index d169ea72..04c6b7ce 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -10,6 +10,9 @@ export class APIError extends Error { this.name = 'APIError' this.details = details } + toString() { + return this.name + } } export async function call(url: string, method: string, params: any) { @@ -82,6 +85,10 @@ export function sellBet(params: any) { return call(getFunctionUrl('sellbet'), 'POST', params) } +export function claimManalink(params: any) { + return call(getFunctionUrl('claimmanalink'), 'POST', params) +} + export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 3b16af70..6867b5bb 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -36,8 +36,3 @@ export const createUser: () => Promise<User | null> = () => { .then((r) => (r.data as any)?.user || null) .catch(() => null) } - -export const claimManalink = cloudFunction< - string, - { status: 'error' | 'success'; message?: string } ->('claimManalink') diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index eed68e1a..b36a9057 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' -import { claimManalink } from 'web/lib/firebase/fn-call' +import { claimManalink } from 'web/lib/firebase/api-call' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' @@ -42,10 +42,7 @@ export default function ClaimPage() { if (user == null) { await firebaseLogin() } - const result = await claimManalink(manalink.slug) - if (result.data.status == 'error') { - throw new Error(result.data.message) - } + await claimManalink({ slug: manalink.slug }) user && router.push(`/${user.username}?claimed-mana=yes`) } catch (e) { console.log(e) From c1ca1471a1705ca2501af746d6a9df4a44604360 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 9 Jul 2022 00:26:56 -0700 Subject: [PATCH 083/519] Migrate createAnswer function to v2 (#634) * Migrate createAnswer function to v2 * Remove unhelpful toString on APIError --- functions/src/create-answer.ts | 185 ++++++++---------- functions/src/index.ts | 2 +- .../answers/create-answer-panel.tsx | 25 +-- web/lib/firebase/api-call.ts | 6 +- web/lib/firebase/fn-call.ts | 10 - 5 files changed, 101 insertions(+), 127 deletions(-) diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index cf3867b0..2abaf44d 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -1,5 +1,5 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { Contract } from '../../common/contract' import { User } from '../../common/user' @@ -7,122 +7,103 @@ import { getNewMultiBetInfo } from '../../common/new-bet' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { getContract, getValues } from './utils' import { sendNewAnswerEmail } from './emails' +import { APIError, newEndpoint, validate } from './api' -export const createAnswer = functions - .runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) - .https.onCall( - async ( - data: { - contractId: string - amount: number - text: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + contractId: z.string().max(MAX_ANSWER_LENGTH), + amount: z.number().gt(0), + text: z.string(), +}) - const { contractId, amount, text } = data +const opts = { secrets: ['MAILGUN_KEY'] } - if (amount <= 0 || isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } +export const createanswer = newEndpoint(opts, async (req, auth) => { + const { contractId, amount, text } = validate(bodySchema, req.body) - if (!text || typeof text !== 'string' || text.length > MAX_ANSWER_LENGTH) - return { status: 'error', message: 'Invalid text' } + if (!isFinite(amount)) throw new APIError(400, 'Invalid amount') - // Run as transaction to prevent race conditions. - const result = await firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) - return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User + // Run as transaction to prevent race conditions. + const answer = await firestore.runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${auth.uid}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) throw new APIError(400, 'User not found') + const user = userSnap.data() as User - if (user.balance < amount) - return { status: 'error', message: 'Insufficient balance' } + if (user.balance < amount) throw new APIError(400, 'Insufficient balance') - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Invalid contract') + const contract = contractSnap.data() as Contract - if (contract.outcomeType !== 'FREE_RESPONSE') - return { - status: 'error', - message: 'Requires a free response contract', - } + if (contract.outcomeType !== 'FREE_RESPONSE') + throw new APIError(400, 'Requires a free response contract') - const { closeTime, volume } = contract - if (closeTime && Date.now() > closeTime) - return { status: 'error', message: 'Trading is closed' } + const { closeTime, volume } = contract + if (closeTime && Date.now() > closeTime) + throw new APIError(400, 'Trading is closed') - const [lastAnswer] = await getValues<Answer>( - firestore - .collection(`contracts/${contractId}/answers`) - .orderBy('number', 'desc') - .limit(1) - ) + const [lastAnswer] = await getValues<Answer>( + firestore + .collection(`contracts/${contractId}/answers`) + .orderBy('number', 'desc') + .limit(1) + ) - if (!lastAnswer) - return { status: 'error', message: 'Could not fetch last answer' } + if (!lastAnswer) throw new APIError(500, 'Could not fetch last answer') - const number = lastAnswer.number + 1 - const id = `${number}` + const number = lastAnswer.number + 1 + const id = `${number}` - const newAnswerDoc = firestore - .collection(`contracts/${contractId}/answers`) - .doc(id) + const newAnswerDoc = firestore + .collection(`contracts/${contractId}/answers`) + .doc(id) - const answerId = newAnswerDoc.id - const { username, name, avatarUrl } = user + const answerId = newAnswerDoc.id + const { username, name, avatarUrl } = user - const answer: Answer = { - id, - number, - contractId, - createdTime: Date.now(), - userId: user.id, - username, - name, - avatarUrl, - text, - } - transaction.create(newAnswerDoc, answer) - - const loanAmount = 0 - - const { newBet, newPool, newTotalShares, newTotalBets } = - getNewMultiBetInfo(answerId, amount, contract, loanAmount) - - const newBalance = user.balance - amount - const betDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() - transaction.create(betDoc, { - id: betDoc.id, - userId: user.id, - ...newBet, - }) - transaction.update(userDoc, { balance: newBalance }) - transaction.update(contractDoc, { - pool: newPool, - totalShares: newTotalShares, - totalBets: newTotalBets, - answers: [...(contract.answers ?? []), answer], - volume: volume + amount, - }) - - return { status: 'success', answerId, betId: betDoc.id, answer } - }) - - const { answer } = result - const contract = await getContract(contractId) - - if (answer && contract) await sendNewAnswerEmail(answer, contract) - - return result + const answer: Answer = { + id, + number, + contractId, + createdTime: Date.now(), + userId: user.id, + username, + name, + avatarUrl, + text, } - ) + transaction.create(newAnswerDoc, answer) + + const loanAmount = 0 + + const { newBet, newPool, newTotalShares, newTotalBets } = + getNewMultiBetInfo(answerId, amount, contract, loanAmount) + + const newBalance = user.balance - amount + const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc() + transaction.create(betDoc, { + id: betDoc.id, + userId: user.id, + ...newBet, + }) + transaction.update(userDoc, { balance: newBalance }) + transaction.update(contractDoc, { + pool: newPool, + totalShares: newTotalShares, + totalBets: newTotalBets, + answers: [...(contract.answers ?? []), answer], + volume: volume + amount, + }) + + return answer + }) + + const contract = await getContract(contractId) + + if (answer && contract) await sendNewAnswerEmail(answer, contract) + + return answer +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 7c839396..34fceaa7 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,7 +6,6 @@ admin.initializeApp() export * from './transact' export * from './stripe' export * from './create-user' -export * from './create-answer' export * from './on-create-bet' export * from './on-create-comment-on-contract' export * from './on-view' @@ -30,6 +29,7 @@ export * from './on-create-txn' // v2 export * from './health' export * from './change-user-info' +export * from './create-answer' export * from './place-bet' export * from './sell-bet' export * from './sell-shares' diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index ed9012c9..41745b09 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -6,7 +6,7 @@ import { findBestMatch } from 'string-similarity' import { FreeResponseContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' -import { createAnswer } from 'web/lib/firebase/fn-call' +import { APIError, createAnswer } from 'web/lib/firebase/api-call' import { Row } from '../layout/row' import { formatMoney, @@ -46,20 +46,23 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) { if (canSubmit) { setIsSubmitting(true) - const result = await createAnswer({ - contractId: contract.id, - text, - amount: betAmount, - }).then((r) => r.data) - - setIsSubmitting(false) - - if (result.status === 'success') { + try { + await createAnswer({ + contractId: contract.id, + text, + amount: betAmount, + }) setText('') setBetAmount(10) setAmountError(undefined) setPossibleDuplicateAnswer(undefined) - } else setAmountError(result.message) + } catch (e) { + if (e instanceof APIError) { + setAmountError(e.toString()) + } + } + + setIsSubmitting(false) } } diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 04c6b7ce..ef0cad1e 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -10,9 +10,6 @@ export class APIError extends Error { this.name = 'APIError' this.details = details } - toString() { - return this.name - } } export async function call(url: string, method: string, params: any) { @@ -53,6 +50,9 @@ export function getFunctionUrl(name: string) { } } +export function createAnswer(params: any) { + return call(getFunctionUrl('createanswer'), 'POST', params) +} export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 6867b5bb..27a5e8f3 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -14,16 +14,6 @@ export const transact = cloudFunction< { status: 'error' | 'success'; message?: string; txn?: Txn } >('transact') -export const createAnswer = cloudFunction< - { contractId: string; text: string; amount: number }, - { - status: 'error' | 'success' - message?: string - answerId?: string - betId?: string - } ->('createAnswer') - export const createUser: () => Promise<User | null> = () => { const local = safeLocalStorage() let deviceToken = local?.getItem('device-token') From e7e686d5799190682b2ea6af802937f43724a079 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 9 Jul 2022 13:53:50 -0400 Subject: [PATCH 084/519] return creator liquidity after resolution --- common/calculate-cpmm.ts | 27 +++++++++++++++++---------- common/payouts-fixed.ts | 4 ++-- functions/src/withdraw-liquidity.ts | 7 ++++++- web/hooks/use-liquidity.ts | 7 ++++++- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 92d95251..8a609970 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -1,4 +1,4 @@ -import { sum, groupBy, mapValues, sumBy, partition } from 'lodash' +import { sum, groupBy, mapValues, sumBy } from 'lodash' import { CPMMContract } from './contract' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' @@ -268,18 +268,20 @@ const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => { export function getCpmmLiquidityPoolWeights( contract: CPMMContract, - liquidities: LiquidityProvision[] + liquidities: LiquidityProvision[], + excludeAntes: boolean ) { - const [antes, nonAntes] = partition(liquidities, (l) => !!l.isAnte) - const calcLiqudity = calculateLiquidityDelta(contract.p) - const liquidityShares = nonAntes.map(calcLiqudity) + const liquidityShares = liquidities.map(calcLiqudity) + const shareSum = sum(liquidityShares) - const shareSum = sum(liquidityShares) + sum(antes.map(calcLiqudity)) + const includedLiquidities = excludeAntes + ? liquidityShares.filter((_, i) => !liquidities[i].isAnte) + : liquidityShares - const weights = liquidityShares.map((s, i) => ({ + const weights = includedLiquidities.map((s, i) => ({ weight: s / shareSum, - providerId: nonAntes[i].userId, + providerId: liquidities[i].userId, })) const userWeights = groupBy(weights, (w) => w.providerId) @@ -292,9 +294,14 @@ export function getCpmmLiquidityPoolWeights( export function getUserLiquidityShares( userId: string, contract: CPMMContract, - liquidities: LiquidityProvision[] + liquidities: LiquidityProvision[], + excludeAntes: boolean ) { - const weights = getCpmmLiquidityPoolWeights(contract, liquidities) + const weights = getCpmmLiquidityPoolWeights( + contract, + liquidities, + excludeAntes + ) const userWeight = weights[userId] ?? 0 return mapValues(contract.pool, (shares) => userWeight * shares) diff --git a/common/payouts-fixed.ts b/common/payouts-fixed.ts index 4e06042b..4b8de85a 100644 --- a/common/payouts-fixed.ts +++ b/common/payouts-fixed.ts @@ -72,7 +72,7 @@ export const getLiquidityPoolPayouts = ( const { pool } = contract const finalPool = pool[outcome] - const weights = getCpmmLiquidityPoolWeights(contract, liquidities) + const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) return Object.entries(weights).map(([providerId, weight]) => ({ userId: providerId, @@ -123,7 +123,7 @@ export const getLiquidityPoolProbPayouts = ( const { pool } = contract const finalPool = p * pool.YES + (1 - p) * pool.NO - const weights = getCpmmLiquidityPoolWeights(contract, liquidities) + const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) return Object.entries(weights).map(([providerId, weight]) => ({ userId: providerId, diff --git a/functions/src/withdraw-liquidity.ts b/functions/src/withdraw-liquidity.ts index 1bdb19de..53974f7d 100644 --- a/functions/src/withdraw-liquidity.ts +++ b/functions/src/withdraw-liquidity.ts @@ -42,7 +42,12 @@ export const withdrawliquidity = newEndpoint({}, async (req, auth) => { (doc) => doc.data() as LiquidityProvision ) - const userShares = getUserLiquidityShares(auth.uid, contract, liquidities) + const userShares = getUserLiquidityShares( + auth.uid, + contract, + liquidities, + true + ) // zero all added amounts for now // can add support for partial withdrawals in the future diff --git a/web/hooks/use-liquidity.ts b/web/hooks/use-liquidity.ts index 9c610f3b..9c7c2b6f 100644 --- a/web/hooks/use-liquidity.ts +++ b/web/hooks/use-liquidity.ts @@ -21,6 +21,11 @@ export const useLiquidity = (contractId: string) => { export const useUserLiquidity = (contract: CPMMContract, userId: string) => { const liquidities = useLiquidity(contract.id) - const userShares = getUserLiquidityShares(userId, contract, liquidities ?? []) + const userShares = getUserLiquidityShares( + userId, + contract, + liquidities ?? [], + true + ) return userShares } From 581a42f2885a53de275675667afacf7cf37f2320 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 9 Jul 2022 13:43:18 -0700 Subject: [PATCH 085/519] Migrate stripeWebhook and createCheckoutSession to v2 (#636) --- functions/src/stripe.ts | 22 ++++++++++++---------- web/lib/firebase/api-call.ts | 1 + web/lib/service/stripe.ts | 5 ++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index 67309aa8..450bbe35 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -1,4 +1,4 @@ -import * as functions from 'firebase-functions' +import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' import Stripe from 'stripe' @@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd() 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', } -export const createCheckoutSession = functions - .runWith({ minInstances: 1, secrets: ['STRIPE_APIKEY'] }) - .https.onRequest(async (req, res) => { +export const createcheckoutsession = onRequest( + { minInstances: 1, secrets: ['STRIPE_APIKEY'] }, + async (req, res) => { const userId = req.query.userId?.toString() const manticDollarQuantity = req.query.manticDollarQuantity?.toString() @@ -86,14 +86,15 @@ export const createCheckoutSession = functions }) res.redirect(303, session.url || '') - }) + } +) -export const stripeWebhook = functions - .runWith({ +export const stripewebhook = onRequest( + { minInstances: 1, secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'], - }) - .https.onRequest(async (req, res) => { + }, + async (req, res) => { const stripe = initStripe() let event @@ -115,7 +116,8 @@ export const stripeWebhook = functions } res.status(200).send('success') - }) + } +) const issueMoneys = async (session: StripeSession) => { const { id: sessionId } = session diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index ef0cad1e..d65a44f3 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -53,6 +53,7 @@ export function getFunctionUrl(name: string) { export function createAnswer(params: any) { return call(getFunctionUrl('createanswer'), 'POST', params) } + export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } diff --git a/web/lib/service/stripe.ts b/web/lib/service/stripe.ts index 395f7093..bedd68aa 100644 --- a/web/lib/service/stripe.ts +++ b/web/lib/service/stripe.ts @@ -1,12 +1,11 @@ -import { PROJECT_ID } from 'common/envs/constants' +import { getFunctionUrl } from 'web/lib/firebase/api-call' export const checkoutURL = ( userId: string, manticDollarQuantity: number, referer = '' ) => { - const endpoint = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/createCheckoutSession` - + const endpoint = getFunctionUrl('createcheckoutsession') return `${endpoint}?userId=${userId}&manticDollarQuantity=${manticDollarQuantity}&referer=${encodeURIComponent( referer )}` From 67a05c2f1be80e987c51f135861c9d2f24af4a14 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 9 Jul 2022 13:54:15 -0700 Subject: [PATCH 086/519] Migrate transact function to v2 (#635) --- functions/src/index.ts | 2 +- functions/src/transact.ts | 46 ++++++++++++++--------------- web/components/tipper.tsx | 2 +- web/lib/firebase/api-call.ts | 4 +++ web/lib/firebase/fn-call.ts | 6 ---- web/pages/charity/[charitySlug].tsx | 2 +- 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 34fceaa7..35f29954 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './transact' export * from './stripe' export * from './create-user' export * from './on-create-bet' @@ -28,6 +27,7 @@ export * from './on-create-txn' // v2 export * from './health' +export * from './transact' export * from './change-user-info' export * from './create-answer' export * from './place-bet' diff --git a/functions/src/transact.ts b/functions/src/transact.ts index cd091b83..113afc0b 100644 --- a/functions/src/transact.ts +++ b/functions/src/transact.ts @@ -1,40 +1,40 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { User } from '../../common/user' import { Txn } from '../../common/txn' import { removeUndefinedProps } from '../../common/util/object' +import { APIError, newEndpoint } from './api' export type TxnData = Omit<Txn, 'id' | 'createdTime'> -export const transact = functions - .runWith({ minInstances: 1 }) - .https.onCall(async (data: TxnData, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +// TODO: We totally fail to validate most of the input to this function, +// so anyone can spam our database with malformed transactions. - const { amount, fromType, fromId } = data +export const transact = newEndpoint({}, async (req, auth) => { + const data = req.body + const { amount, fromType, fromId } = data - if (fromType !== 'USER') - return { - status: 'error', - message: "From type is only implemented for type 'user'.", - } + if (fromType !== 'USER') + throw new APIError(400, "From type is only implemented for type 'user'.") - if (fromId !== userId) - return { - status: 'error', - message: 'Must be authenticated with userId equal to specified fromId.', - } + if (fromId !== auth.uid) + throw new APIError( + 403, + 'Must be authenticated with userId equal to specified fromId.' + ) - if (isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } + if (isNaN(amount) || !isFinite(amount)) + throw new APIError(400, 'Invalid amount') - // Run as transaction to prevent race conditions. - return await firestore.runTransaction(async (transaction) => { - await runTxn(transaction, data) - }) + // Run as transaction to prevent race conditions. + return await firestore.runTransaction(async (transaction) => { + const result = await runTxn(transaction, data) + if (result.status == 'error') { + throw new APIError(500, result.message ?? 'An unknown error occurred.') + } + return result }) +}) export async function runTxn( fbTransaction: admin.firestore.Transaction, diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 6f7dfbcb..e4b6580f 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -11,7 +11,7 @@ import { debounce, sum } from 'lodash' import { useEffect, useRef, useState } from 'react' import { CommentTips } from 'web/hooks/use-tip-txns' import { useUser } from 'web/hooks/use-user' -import { transact } from 'web/lib/firebase/fn-call' +import { transact } from 'web/lib/firebase/api-call' import { track } from 'web/lib/service/analytics' import { Row } from './layout/row' import { Tooltip } from './tooltip' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index d65a44f3..7882d9ba 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -54,6 +54,10 @@ export function createAnswer(params: any) { return call(getFunctionUrl('createanswer'), 'POST', params) } +export function transact(params: any) { + return call(getFunctionUrl('transact'), 'POST', params) +} + export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 27a5e8f3..2f299aea 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -1,5 +1,4 @@ import { httpsCallable } from 'firebase/functions' -import { Txn } from 'common/txn' import { User } from 'common/user' import { randomString } from 'common/util/random' import './init' @@ -9,11 +8,6 @@ import { safeLocalStorage } from '../util/local' export const cloudFunction = <RequestData, ResponseData>(name: string) => httpsCallable<RequestData, ResponseData>(functions, name) -export const transact = cloudFunction< - Omit<Txn, 'id' | 'createdTime'>, - { status: 'error' | 'success'; message?: string; txn?: Txn } ->('transact') - export const createUser: () => Promise<User | null> = () => { const local = safeLocalStorage() let deviceToken = local?.getItem('device-token') diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index 7c3ce51b..c3e0912a 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -10,7 +10,7 @@ import { Spacer } from 'web/components/layout/spacer' import { User } from 'common/user' import { useUser } from 'web/hooks/use-user' import { Linkify } from 'web/components/linkify' -import { transact } from 'web/lib/firebase/fn-call' +import { transact } from 'web/lib/firebase/api-call' import { charities, Charity } from 'common/charity' import { useRouter } from 'next/router' import Custom404 from '../404' From 43b1096313a41f6186ade3f5eb6bac9bc52a4504 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 9 Jul 2022 17:27:36 -0400 Subject: [PATCH 087/519] expand search bar when typing on mobile --- web/components/contract-search.tsx | 54 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 220a95ab..7c0460b4 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -22,6 +22,7 @@ import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' +import { useWindowSize } from 'web/hooks/use-window-size' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -104,6 +105,10 @@ export function ContractSearch(props: { const indexName = `${indexPrefix}contracts-${sort}` + const [isSearching, setIsSearching] = useState(false) + const { width } = useWindowSize() + const showOptions = !isSearching || (width ?? 0) >= 500 + if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { return ( <ContractSearchFirestore @@ -122,29 +127,38 @@ export function ContractSearch(props: { classNames={{ form: 'before:top-6', input: '!pl-10 !input !input-bordered shadow-none w-[100px]', - resetIcon: 'mt-2 hidden sm:flex', + resetIcon: 'mt-2 sm:flex', }} + onFocus={() => setIsSearching(true)} + onBlur={() => setIsSearching(false)} /> - <select - className="!select !select-bordered" - value={filter} - onChange={(e) => setFilter(e.target.value as filter)} - onBlur={trackCallback('select search filter')} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> - {!hideOrderSelector && ( - <SortBy - items={sortIndexes} - classNames={{ - select: '!select !select-bordered', - }} - onBlur={trackCallback('select search sort')} - /> + + {showOptions && ( + <> + <select + className="!select !select-bordered" + value={filter} + onChange={(e) => setFilter(e.target.value as filter)} + onBlur={trackCallback('select search filter')} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + + {!hideOrderSelector && ( + <SortBy + items={sortIndexes} + classNames={{ + select: '!select !select-bordered', + }} + onBlur={trackCallback('select search sort')} + /> + )} + </> )} + <Configure facetFilters={filters} numericFilters={numericFilters} From 480b3e7c54c4f28f844bdfb4dab6a79d4dfe3f16 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 9 Jul 2022 14:38:23 -0700 Subject: [PATCH 088/519] Make referral stuff not busted (#632) --- web/lib/firebase/users.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index e72fe141..7f007031 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -39,6 +39,9 @@ import { filterDefined } from 'common/util/array' import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +dayjs.extend(utc) + import { track } from '@amplitude/analytics-browser' export const users = coll<User>('users') From d063e209ddef09ecf9b523a3237d9c0d23980fb5 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 9 Jul 2022 22:04:50 -0400 Subject: [PATCH 089/519] Revert "expand search bar when typing on mobile" This reverts commit 43b1096313a41f6186ade3f5eb6bac9bc52a4504. --- web/components/contract-search.tsx | 54 +++++++++++------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 7c0460b4..220a95ab 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -22,7 +22,6 @@ import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' -import { useWindowSize } from 'web/hooks/use-window-size' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -105,10 +104,6 @@ export function ContractSearch(props: { const indexName = `${indexPrefix}contracts-${sort}` - const [isSearching, setIsSearching] = useState(false) - const { width } = useWindowSize() - const showOptions = !isSearching || (width ?? 0) >= 500 - if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { return ( <ContractSearchFirestore @@ -127,38 +122,29 @@ export function ContractSearch(props: { classNames={{ form: 'before:top-6', input: '!pl-10 !input !input-bordered shadow-none w-[100px]', - resetIcon: 'mt-2 sm:flex', + resetIcon: 'mt-2 hidden sm:flex', }} - onFocus={() => setIsSearching(true)} - onBlur={() => setIsSearching(false)} /> - - {showOptions && ( - <> - <select - className="!select !select-bordered" - value={filter} - onChange={(e) => setFilter(e.target.value as filter)} - onBlur={trackCallback('select search filter')} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> - - {!hideOrderSelector && ( - <SortBy - items={sortIndexes} - classNames={{ - select: '!select !select-bordered', - }} - onBlur={trackCallback('select search sort')} - /> - )} - </> + <select + className="!select !select-bordered" + value={filter} + onChange={(e) => setFilter(e.target.value as filter)} + onBlur={trackCallback('select search filter')} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + {!hideOrderSelector && ( + <SortBy + items={sortIndexes} + classNames={{ + select: '!select !select-bordered', + }} + onBlur={trackCallback('select search sort')} + /> )} - <Configure facetFilters={filters} numericFilters={numericFilters} From fc06b03af8cd50cee8f3cd672333343faac2e7ff Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 9 Jul 2022 22:39:26 -0400 Subject: [PATCH 090/519] fix getCpmmLiquidityPoolWeights --- common/calculate-cpmm.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 8a609970..66162132 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -1,4 +1,4 @@ -import { sum, groupBy, mapValues, sumBy } from 'lodash' +import { sum, groupBy, mapValues, sumBy, zip } from 'lodash' import { CPMMContract } from './contract' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' @@ -275,16 +275,16 @@ export function getCpmmLiquidityPoolWeights( const liquidityShares = liquidities.map(calcLiqudity) const shareSum = sum(liquidityShares) - const includedLiquidities = excludeAntes - ? liquidityShares.filter((_, i) => !liquidities[i].isAnte) - : liquidityShares - - const weights = includedLiquidities.map((s, i) => ({ - weight: s / shareSum, + const weights = liquidityShares.map((shares, i) => ({ + weight: shares / shareSum, providerId: liquidities[i].userId, })) - const userWeights = groupBy(weights, (w) => w.providerId) + const includedWeights = excludeAntes + ? weights.filter((_, i) => !liquidities[i].isAnte) + : weights + + const userWeights = groupBy(includedWeights, (w) => w.providerId) const totalUserWeights = mapValues(userWeights, (userWeight) => sumBy(userWeight, (w) => w.weight) ) From 80ae551ca9bdf125a4e384603488b17488a00df8 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 13:05:44 -0500 Subject: [PATCH 091/519] =?UTF-8?q?=F0=9F=A7=BE=20Limit=20orders!=20=20(#4?= =?UTF-8?q?95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com> --- common/bet.ts | 30 +- common/calculate-cpmm.ts | 215 +++++----- common/calculate.ts | 30 +- common/envs/constants.ts | 4 + common/new-bet.ts | 235 +++++++++-- common/notification.ts | 1 + common/sell-bet.ts | 30 +- common/util/algos.ts | 22 + common/util/math.ts | 14 + functions/src/api.ts | 3 +- functions/src/cancel-bet.ts | 35 ++ functions/src/create-notification.ts | 36 +- functions/src/index.ts | 1 + functions/src/on-create-bet.ts | 51 ++- functions/src/on-update-user.ts | 18 + functions/src/place-bet.ts | 104 ++++- functions/src/sell-shares.ts | 33 +- web/components/bet-panel.tsx | 390 +++++++++--------- web/components/bet-row.tsx | 21 +- web/components/bets-list.tsx | 35 +- web/components/bucket-input.tsx | 9 +- web/components/contract/contract-card.tsx | 5 +- web/components/contract/contract-overview.tsx | 9 +- web/components/contract/quick-bet.tsx | 51 ++- web/components/feed/contract-activity.tsx | 4 +- web/components/feed/feed-items.tsx | 7 +- web/components/limit-bets.tsx | 89 ++++ web/components/numeric-resolution-panel.tsx | 2 +- web/components/probability-input.tsx | 49 +++ web/components/sell-row.tsx | 17 +- web/components/use-save-binary-shares.ts | 56 +++ web/components/use-save-shares.ts | 59 --- web/hooks/use-bets.ts | 11 + web/hooks/use-focus.ts | 5 +- web/lib/firebase/api-call.ts | 4 + web/lib/firebase/bets.ts | 17 +- web/pages/[username]/[contractSlug].tsx | 7 +- web/pages/embed/[username]/[contractSlug].tsx | 7 +- web/pages/notifications.tsx | 15 +- 39 files changed, 1209 insertions(+), 522 deletions(-) create mode 100644 common/util/algos.ts create mode 100644 functions/src/cancel-bet.ts create mode 100644 web/components/limit-bets.tsx create mode 100644 web/components/probability-input.tsx create mode 100644 web/components/use-save-binary-shares.ts delete mode 100644 web/components/use-save-shares.ts diff --git a/common/bet.ts b/common/bet.ts index 993a2fac..d5072c0f 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -4,6 +4,7 @@ export type Bet = { id: string userId: string contractId: string + createdTime: number amount: number // bet size; negative if SELL bet loanAmount?: number @@ -25,9 +26,7 @@ export type Bet = { isAnte?: boolean isLiquidityProvision?: boolean isRedemption?: boolean - - createdTime: number -} +} & Partial<LimitProps> export type NumericBet = Bet & { value: number @@ -35,4 +34,29 @@ export type NumericBet = Bet & { allBetAmounts: { [outcome: string]: number } } +// Binary market limit order. +export type LimitBet = Bet & LimitProps + +type LimitProps = { + orderAmount: number // Amount of limit order. + limitProb: number // [0, 1]. Bet to this probability. + isFilled: boolean // Whether all of the bet amount has been filled. + isCancelled: boolean // Whether to prevent any further fills. + // A record of each transaction that partially (or fully) fills the orderAmount. + // I.e. A limit order could be filled by partially matching with several bets. + // Non-limit orders can also be filled by matching with multiple limit orders. + fills: fill[] +} + +export type fill = { + // The id the bet matched against, or null if the bet was matched by the pool. + matchedBetId: string | null + amount: number + shares: number + timestamp: number + // If the fill is a sale, it means the matching bet has shares of the same outcome. + // I.e. -fill.shares === matchedBet.shares + isSale?: boolean +} + export const MAX_LOAN_PER_CONTRACT = 20 diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 66162132..493b5fa9 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -1,10 +1,17 @@ -import { sum, groupBy, mapValues, sumBy, zip } from 'lodash' +import { sum, groupBy, mapValues, sumBy } from 'lodash' +import { LimitBet } from './bet' -import { CPMMContract } from './contract' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { LiquidityProvision } from './liquidity-provision' +import { computeFills } from './new-bet' +import { binarySearch } from './util/algos' import { addObjects } from './util/object' +export type CpmmState = { + pool: { [outcome: string]: number } + p: number +} + export function getCpmmProbability( pool: { [outcome: string]: number }, p: number @@ -14,11 +21,11 @@ export function getCpmmProbability( } export function getCpmmProbabilityAfterBetBeforeFees( - contract: CPMMContract, + state: CpmmState, outcome: string, bet: number ) { - const { pool, p } = contract + const { pool, p } = state const shares = calculateCpmmShares(pool, p, bet, outcome) const { YES: y, NO: n } = pool @@ -31,12 +38,12 @@ export function getCpmmProbabilityAfterBetBeforeFees( } export function getCpmmOutcomeProbabilityAfterBet( - contract: CPMMContract, + state: CpmmState, outcome: string, bet: number ) { - const { newPool } = calculateCpmmPurchase(contract, bet, outcome) - const p = getCpmmProbability(newPool, contract.p) + const { newPool } = calculateCpmmPurchase(state, bet, outcome) + const p = getCpmmProbability(newPool, state.p) return outcome === 'NO' ? 1 - p : p } @@ -58,12 +65,8 @@ function calculateCpmmShares( : n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p)) } -export function getCpmmFees( - contract: CPMMContract, - bet: number, - outcome: string -) { - const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet) +export function getCpmmFees(state: CpmmState, bet: number, outcome: string) { + const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet) const betP = outcome === 'YES' ? 1 - prob : prob const liquidityFee = LIQUIDITY_FEE * betP * bet @@ -78,23 +81,23 @@ export function getCpmmFees( } export function calculateCpmmSharesAfterFee( - contract: CPMMContract, + state: CpmmState, bet: number, outcome: string ) { - const { pool, p } = contract - const { remainingBet } = getCpmmFees(contract, bet, outcome) + const { pool, p } = state + const { remainingBet } = getCpmmFees(state, bet, outcome) return calculateCpmmShares(pool, p, remainingBet, outcome) } export function calculateCpmmPurchase( - contract: CPMMContract, + state: CpmmState, bet: number, outcome: string ) { - const { pool, p } = contract - const { remainingBet, fees } = getCpmmFees(contract, bet, outcome) + const { pool, p } = state + const { remainingBet, fees } = getCpmmFees(state, bet, outcome) const shares = calculateCpmmShares(pool, p, remainingBet, outcome) const { YES: y, NO: n } = pool @@ -113,117 +116,111 @@ export function calculateCpmmPurchase( return { shares, newPool, newP, fees } } -function computeK(y: number, n: number, p: number) { - return y ** p * n ** (1 - p) -} - -function sellSharesK( - y: number, - n: number, - p: number, - s: number, - outcome: 'YES' | 'NO', - b: number -) { - return outcome === 'YES' - ? computeK(y - b + s, n - b, p) - : computeK(y - b, n - b + s, p) -} - -function calculateCpmmShareValue( - contract: CPMMContract, - shares: number, +// Note: there might be a closed form solution for this. +// If so, feel free to switch out this implementation. +export function calculateCpmmAmountToProb( + state: CpmmState, + prob: number, outcome: 'YES' | 'NO' ) { - const { pool, p } = contract + if (outcome === 'NO') prob = 1 - prob - // Find bet amount that preserves k after selling shares. - const k = computeK(pool.YES, pool.NO, p) - const otherPool = outcome === 'YES' ? pool.NO : pool.YES + // First, find an upper bound that leads to a more extreme probability than prob. + let maxGuess = 10 + let newProb = 0 + do { + maxGuess *= 10 + newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, maxGuess) + } while (newProb < prob) - // Constrain the max sale value to the lessor of 1. shares and 2. the other pool. - // This is because 1. the max value per share is M$ 1, - // and 2. The other pool cannot go negative and the sale value is subtracted from it. - // (Without this, there are multiple solutions for the same k.) - let highAmount = Math.min(shares, otherPool) - let lowAmount = 0 - let mid = 0 - let kGuess = 0 - while (true) { - mid = lowAmount + (highAmount - lowAmount) / 2 + // Then, binary search for the amount that gets closest to prob. + const amount = binarySearch(0, maxGuess, (amount) => { + const newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, amount) + return newProb - prob + }) - // Break once we've reached max precision. - if (mid === lowAmount || mid === highAmount) break + return amount +} - kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid) - if (kGuess < k) { - highAmount = mid - } else { - lowAmount = mid - } - } - return mid +function calculateAmountToBuyShares( + state: CpmmState, + shares: number, + outcome: 'YES' | 'NO', + unfilledBets: LimitBet[] +) { + // Search for amount between bounds (0, shares). + // Min share price is M$0, and max is M$1 each. + return binarySearch(0, shares, (amount) => { + const { takers } = computeFills( + outcome, + amount, + state, + undefined, + unfilledBets + ) + + const totalShares = sumBy(takers, (taker) => taker.shares) + return totalShares - shares + }) } export function calculateCpmmSale( - contract: CPMMContract, + state: CpmmState, shares: number, - outcome: string + outcome: 'YES' | 'NO', + unfilledBets: LimitBet[] ) { if (Math.round(shares) < 0) { throw new Error('Cannot sell non-positive shares') } - const rawSaleValue = calculateCpmmShareValue( - contract, + const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES' + const buyAmount = calculateAmountToBuyShares( + state, shares, - outcome as 'YES' | 'NO' + oppositeOutcome, + unfilledBets ) - const { fees, remainingBet: saleValue } = getCpmmFees( - contract, - rawSaleValue, - outcome === 'YES' ? 'NO' : 'YES' + const { cpmmState, makers, takers, totalFees } = computeFills( + oppositeOutcome, + buyAmount, + state, + undefined, + unfilledBets ) - const { pool } = contract - const { YES: y, NO: n } = pool + // Transform buys of opposite outcome into sells. + const saleTakers = takers.map((taker) => ({ + ...taker, + // You bought opposite shares, which combine with existing shares, removing them. + shares: -taker.shares, + // Opposite shares combine with shares you are selling for M$ of shares. + // You paid taker.amount for the opposite shares. + // Take the negative because this is money you gain. + amount: -(taker.shares - taker.amount), + isSale: true, + })) - const { liquidityFee: fee } = fees + const saleValue = -sumBy(saleTakers, (taker) => taker.amount) - const [newY, newN] = - outcome === 'YES' - ? [y + shares - saleValue + fee, n - saleValue + fee] - : [y - saleValue + fee, n + shares - saleValue + fee] - - if (newY < 0 || newN < 0) { - console.log('calculateCpmmSale', { - newY, - newN, - y, - n, - shares, - saleValue, - fee, - outcome, - }) - throw new Error('Cannot sell more than in pool') + return { + saleValue, + cpmmState, + fees: totalFees, + makers, + takers: saleTakers, } - - const postBetPool = { YES: newY, NO: newN } - - const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee) - - return { saleValue, newPool, newP, fees } } export function getCpmmProbabilityAfterSale( - contract: CPMMContract, + state: CpmmState, shares: number, - outcome: 'YES' | 'NO' + outcome: 'YES' | 'NO', + unfilledBets: LimitBet[] ) { - const { newPool } = calculateCpmmSale(contract, shares, outcome) - return getCpmmProbability(newPool, contract.p) + const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets) + return getCpmmProbability(cpmmState.pool, cpmmState.p) } export function getCpmmLiquidity( @@ -267,11 +264,11 @@ const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => { } export function getCpmmLiquidityPoolWeights( - contract: CPMMContract, + state: CpmmState, liquidities: LiquidityProvision[], excludeAntes: boolean ) { - const calcLiqudity = calculateLiquidityDelta(contract.p) + const calcLiqudity = calculateLiquidityDelta(state.p) const liquidityShares = liquidities.map(calcLiqudity) const shareSum = sum(liquidityShares) @@ -293,16 +290,12 @@ export function getCpmmLiquidityPoolWeights( export function getUserLiquidityShares( userId: string, - contract: CPMMContract, + state: CpmmState, liquidities: LiquidityProvision[], excludeAntes: boolean ) { - const weights = getCpmmLiquidityPoolWeights( - contract, - liquidities, - excludeAntes - ) + const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes) const userWeight = weights[userId] ?? 0 - return mapValues(contract.pool, (shares) => userWeight * shares) + return mapValues(state.pool, (shares) => userWeight * shares) } diff --git a/common/calculate.ts b/common/calculate.ts index 482a0ccf..e1f3e239 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,5 +1,5 @@ import { maxBy } from 'lodash' -import { Bet } from './bet' +import { Bet, LimitBet } from './bet' import { calculateCpmmSale, getCpmmProbability, @@ -24,6 +24,7 @@ import { FreeResponseContract, PseudoNumericContract, } from './contract' +import { floatingEqual } from './util/math' export function getProbability( contract: BinaryContract | PseudoNumericContract @@ -73,11 +74,20 @@ export function calculateShares( : calculateDpmShares(contract.totalShares, bet, betChoice) } -export function calculateSaleAmount(contract: Contract, bet: Bet) { +export function calculateSaleAmount( + contract: Contract, + bet: Bet, + unfilledBets: LimitBet[] +) { return contract.mechanism === 'cpmm-1' && (contract.outcomeType === 'BINARY' || contract.outcomeType === 'PSEUDO_NUMERIC') - ? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue + ? calculateCpmmSale( + contract, + Math.abs(bet.shares), + bet.outcome as 'YES' | 'NO', + unfilledBets + ).saleValue : calculateDpmSaleAmount(contract, bet) } @@ -90,10 +100,16 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { export function getProbabilityAfterSale( contract: Contract, outcome: string, - shares: number + shares: number, + unfilledBets: LimitBet[] ) { return contract.mechanism === 'cpmm-1' - ? getCpmmProbabilityAfterSale(contract, shares, outcome as 'YES' | 'NO') + ? getCpmmProbabilityAfterSale( + contract, + shares, + outcome as 'YES' | 'NO', + unfilledBets + ) : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) } @@ -157,7 +173,9 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { const profit = payout + saleValue + redeemed - totalInvested const profitPercent = (profit / totalInvested) * 100 - const hasShares = Object.values(totalShares).some((shares) => shares > 0) + const hasShares = Object.values(totalShares).some( + (shares) => !floatingEqual(shares, 0) + ) return { invested: Math.max(0, currentInvested), diff --git a/common/envs/constants.ts b/common/envs/constants.ts index c03c44bc..7092d711 100644 --- a/common/envs/constants.ts +++ b/common/envs/constants.ts @@ -34,5 +34,9 @@ export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE' export const CORS_ORIGIN_MANIFOLD = new RegExp( '^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$' ) +// Vercel deployments, used for testing. +export const CORS_ORIGIN_VERCEL = new RegExp( + '^https?://[a-zA-Z0-9\\-]+' + escapeRegExp('mantic.vercel.app') + '$' +) // Any localhost server on any port export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/ diff --git a/common/new-bet.ts b/common/new-bet.ts index 57739af3..6c3e6856 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,6 +1,6 @@ -import { sumBy } from 'lodash' +import { sortBy, sumBy } from 'lodash' -import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' +import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' import { calculateDpmShares, getDpmProbability, @@ -8,7 +8,12 @@ import { getNumericBets, calculateNumericDpmShares, } from './calculate-dpm' -import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm' +import { + calculateCpmmAmountToProb, + calculateCpmmPurchase, + CpmmState, + getCpmmProbability, +} from './calculate-cpmm' import { CPMMBinaryContract, DPMBinaryContract, @@ -17,8 +22,13 @@ import { PseudoNumericContract, } from './contract' import { noFees } from './fees' -import { addObjects } from './util/object' +import { addObjects, removeUndefinedProps } from './util/object' import { NUMERIC_FIXED_VAR } from './numeric-constants' +import { + floatingEqual, + floatingGreaterEqual, + floatingLesserEqual, +} from './util/math' export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'> export type BetInfo = { @@ -30,38 +40,203 @@ export type BetInfo = { newP?: number } -export const getNewBinaryCpmmBetInfo = ( - outcome: 'YES' | 'NO', +const computeFill = ( amount: number, - contract: CPMMBinaryContract | PseudoNumericContract, - loanAmount: number + outcome: 'YES' | 'NO', + limitProb: number | undefined, + cpmmState: CpmmState, + matchedBet: LimitBet | undefined ) => { - const { shares, newPool, newP, fees } = calculateCpmmPurchase( - contract, - amount, - outcome - ) + const prob = getCpmmProbability(cpmmState.pool, cpmmState.p) - const { pool, p, totalLiquidity } = contract - const probBefore = getCpmmProbability(pool, p) - const probAfter = getCpmmProbability(newPool, newP) - - const newBet: CandidateBet = { - contractId: contract.id, - amount, - shares, - outcome, - fees, - loanAmount, - probBefore, - probAfter, - createdTime: Date.now(), + if ( + limitProb !== undefined && + (outcome === 'YES' + ? floatingGreaterEqual(prob, limitProb) && + (matchedBet?.limitProb ?? 1) > limitProb + : floatingLesserEqual(prob, limitProb) && + (matchedBet?.limitProb ?? 0) < limitProb) + ) { + // No fill. + return undefined } - const { liquidityFee } = fees - const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee + const timestamp = Date.now() - return { newBet, newPool, newP, newTotalLiquidity } + if ( + !matchedBet || + (outcome === 'YES' + ? prob < matchedBet.limitProb + : prob > matchedBet.limitProb) + ) { + // Fill from pool. + const limit = !matchedBet + ? limitProb + : outcome === 'YES' + ? Math.min(matchedBet.limitProb, limitProb ?? 1) + : Math.max(matchedBet.limitProb, limitProb ?? 0) + + const buyAmount = + limit === undefined + ? amount + : Math.min(amount, calculateCpmmAmountToProb(cpmmState, limit, outcome)) + + const { shares, newPool, newP, fees } = calculateCpmmPurchase( + cpmmState, + buyAmount, + outcome + ) + const newState = { pool: newPool, p: newP } + + return { + maker: { + matchedBetId: null, + shares, + amount: buyAmount, + state: newState, + fees, + timestamp, + }, + taker: { + matchedBetId: null, + shares, + amount: buyAmount, + timestamp, + }, + } + } + + // Fill from matchedBet. + const matchRemaining = matchedBet.orderAmount - matchedBet.amount + const shares = Math.min( + amount / + (outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb), + matchRemaining / + (outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb) + ) + + const maker = { + bet: matchedBet, + matchedBetId: 'taker', + amount: + shares * + (outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb), + shares, + timestamp, + } + const taker = { + matchedBetId: matchedBet.id, + amount: + shares * + (outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb), + shares, + timestamp, + } + return { maker, taker } +} + +export const computeFills = ( + outcome: 'YES' | 'NO', + betAmount: number, + state: CpmmState, + limitProb: number | undefined, + unfilledBets: LimitBet[] +) => { + const sortedBets = sortBy( + unfilledBets.filter((bet) => bet.outcome !== outcome), + (bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb), + (bet) => bet.createdTime + ) + + const takers: fill[] = [] + const makers: { + bet: LimitBet + amount: number + shares: number + timestamp: number + }[] = [] + + let amount = betAmount + let cpmmState = { pool: state.pool, p: state.p } + let totalFees = noFees + + let i = 0 + while (true) { + const matchedBet: LimitBet | undefined = sortedBets[i] + const fill = computeFill(amount, outcome, limitProb, cpmmState, matchedBet) + if (!fill) break + + const { taker, maker } = fill + + if (maker.matchedBetId === null) { + // Matched against pool. + cpmmState = maker.state + totalFees = addObjects(totalFees, maker.fees) + takers.push(taker) + } else { + // Matched against bet. + takers.push(taker) + makers.push(maker) + i++ + } + + amount -= taker.amount + + if (floatingEqual(amount, 0)) break + } + + return { takers, makers, totalFees, cpmmState } +} + +export const getBinaryCpmmBetInfo = ( + outcome: 'YES' | 'NO', + betAmount: number, + contract: CPMMBinaryContract | PseudoNumericContract, + limitProb: number | undefined, + unfilledBets: LimitBet[] +) => { + const { pool, p } = contract + const { takers, makers, cpmmState, totalFees } = computeFills( + outcome, + betAmount, + { pool, p }, + limitProb, + unfilledBets + ) + const probBefore = getCpmmProbability(contract.pool, contract.p) + const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) + + const takerAmount = sumBy(takers, 'amount') + const takerShares = sumBy(takers, 'shares') + const isFilled = floatingEqual(betAmount, takerAmount) + + const newBet: CandidateBet = removeUndefinedProps({ + orderAmount: betAmount, + amount: takerAmount, + shares: takerShares, + limitProb, + isFilled, + isCancelled: false, + fills: takers, + contractId: contract.id, + outcome, + probBefore, + probAfter, + loanAmount: 0, + createdTime: Date.now(), + fees: totalFees, + }) + + const { liquidityFee } = totalFees + const newTotalLiquidity = (contract.totalLiquidity ?? 0) + liquidityFee + + return { + newBet, + newPool: cpmmState.pool, + newP: cpmmState.p, + newTotalLiquidity, + makers, + } } export const getNewBinaryDpmBetInfo = ( diff --git a/common/notification.ts b/common/notification.ts index da8a045a..63a44a52 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -62,3 +62,4 @@ export type notification_reason_types = | 'unique_bettors_on_your_contract' | 'on_group_you_are_member_of' | 'tip_received' + | 'bet_fill' diff --git a/common/sell-bet.ts b/common/sell-bet.ts index 6d487ff2..e1fd9c5d 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -1,4 +1,4 @@ -import { Bet } from './bet' +import { Bet, LimitBet } from './bet' import { calculateDpmShareValue, deductDpmFees, @@ -7,6 +7,7 @@ import { import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' import { CPMMContract, DPMContract } from './contract' import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' +import { sumBy } from 'lodash' export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'> @@ -78,19 +79,24 @@ export const getCpmmSellBetInfo = ( shares: number, outcome: 'YES' | 'NO', contract: CPMMContract, - prevLoanAmount: number + prevLoanAmount: number, + unfilledBets: LimitBet[] ) => { const { pool, p } = contract - const { saleValue, newPool, newP, fees } = calculateCpmmSale( + const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale( contract, shares, - outcome + outcome, + unfilledBets ) const loanPaid = Math.min(prevLoanAmount, saleValue) const probBefore = getCpmmProbability(pool, p) - const probAfter = getCpmmProbability(newPool, p) + const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) + + const takerAmount = sumBy(takers, 'amount') + const takerShares = sumBy(takers, 'shares') console.log( 'SELL M$', @@ -104,20 +110,26 @@ export const getCpmmSellBetInfo = ( const newBet: CandidateBet<Bet> = { contractId: contract.id, - amount: -saleValue, - shares: -shares, + amount: takerAmount, + shares: takerShares, outcome, probBefore, probAfter, createdTime: Date.now(), loanAmount: -loanPaid, fees, + fills: takers, + isFilled: true, + isCancelled: false, + orderAmount: takerAmount, } return { newBet, - newPool, - newP, + newPool: cpmmState.pool, + newP: cpmmState.p, fees, + makers, + takers, } } diff --git a/common/util/algos.ts b/common/util/algos.ts new file mode 100644 index 00000000..dd450075 --- /dev/null +++ b/common/util/algos.ts @@ -0,0 +1,22 @@ +export function binarySearch( + min: number, + max: number, + comparator: (x: number) => number +) { + let mid = 0 + while (true) { + mid = min + (max - min) / 2 + + // Break once we've reached max precision. + if (mid === min || mid === max) break + + const comparison = comparator(mid) + if (comparison === 0) break + else if (comparison > 0) { + max = mid + } else { + min = mid + } + } + return mid +} diff --git a/common/util/math.ts b/common/util/math.ts index 66bcff1b..fb07afed 100644 --- a/common/util/math.ts +++ b/common/util/math.ts @@ -34,3 +34,17 @@ export function median(xs: number[]) { export function average(xs: number[]) { return sum(xs) / xs.length } + +const EPSILON = 0.00000001 + +export function floatingEqual(a: number, b: number, epsilon = EPSILON) { + return Math.abs(a - b) < epsilon +} + +export function floatingGreaterEqual(a: number, b: number, epsilon = EPSILON) { + return a + epsilon >= b +} + +export function floatingLesserEqual(a: number, b: number, epsilon = EPSILON) { + return a - epsilon <= b +} diff --git a/functions/src/api.ts b/functions/src/api.ts index 290ea3d8..6ebffc24 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -8,6 +8,7 @@ import { PrivateUser } from '../../common/user' import { CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST, + CORS_ORIGIN_VERCEL, } from '../../common/envs/constants' type Output = Record<string, unknown> @@ -118,7 +119,7 @@ const DEFAULT_OPTS = { concurrency: 100, memory: '2GiB', cpu: 1, - cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST], } export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { diff --git a/functions/src/cancel-bet.ts b/functions/src/cancel-bet.ts new file mode 100644 index 00000000..27e65ffb --- /dev/null +++ b/functions/src/cancel-bet.ts @@ -0,0 +1,35 @@ +import * as admin from 'firebase-admin' +import { z } from 'zod' +import { APIError, newEndpoint, validate } from './api' +import { LimitBet } from '../../common/bet' + +const bodySchema = z.object({ + betId: z.string(), +}) + +export const cancelbet = newEndpoint({}, async (req, auth) => { + const { betId } = validate(bodySchema, req.body) + + const result = await firestore.runTransaction(async (trans) => { + const snap = await trans.get( + firestore.collectionGroup('bets').where('id', '==', betId) + ) + const betDoc = snap.docs[0] + if (!betDoc?.exists) throw new APIError(400, 'Bet not found.') + + const bet = betDoc.data() as LimitBet + if (bet.userId !== auth.uid) + throw new APIError(400, 'Not authorized to cancel bet.') + if (bet.limitProb === undefined) + throw new APIError(400, 'Not a limit bet: Cannot cancel.') + if (bet.isCancelled) throw new APIError(400, 'Bet already cancelled.') + + trans.update(betDoc.ref, { isCancelled: true }) + + return { ...bet, isCancelled: true } + }) + + return result +}) + +const firestore = admin.firestore() diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 519720fd..0d3432a7 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -10,7 +10,7 @@ import { Contract } from '../../common/contract' import { getUserByUsername, getValues } from './utils' import { Comment } from '../../common/comment' import { uniq } from 'lodash' -import { Bet } from '../../common/bet' +import { Bet, LimitBet } from '../../common/bet' import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' @@ -382,3 +382,37 @@ export const createTipNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) } + +export const createBetFillNotification = async ( + fromUser: User, + toUser: User, + bet: Bet, + userBet: LimitBet, + contract: Contract, + idempotencyKey: string +) => { + const fill = userBet.fills.find((fill) => fill.matchedBetId === bet.id) + const fillAmount = fill?.amount ?? 0 + + const notificationRef = firestore + .collection(`/users/${toUser.id}/notifications`) + .doc(idempotencyKey) + const notification: Notification = { + id: idempotencyKey, + userId: toUser.id, + reason: 'bet_fill', + createdTime: Date.now(), + isSeen: false, + sourceId: userBet.id, + sourceType: 'bet', + sourceUpdateType: 'updated', + sourceUserName: fromUser.name, + sourceUserUsername: fromUser.username, + sourceUserAvatarUrl: fromUser.avatarUrl, + sourceText: fillAmount.toString(), + sourceContractCreatorUsername: contract.creatorUsername, + sourceContractTitle: contract.question, + sourceContractSlug: contract.slug, + } + return await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 35f29954..0d0de3ba 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -31,6 +31,7 @@ export * from './transact' export * from './change-user-info' export * from './create-answer' export * from './place-bet' +export * from './cancel-bet' export * from './sell-bet' export * from './sell-shares' export * from './claim-manalink' diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index 3e615e42..5789ed0b 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -1,7 +1,11 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { keyBy } from 'lodash' -import { Bet } from '../../common/bet' +import { Bet, LimitBet } from '../../common/bet' +import { getContract, getUser, getValues } from './utils' +import { createBetFillNotification } from './create-notification' +import { filterDefined } from '../../common/util/array' const firestore = admin.firestore() @@ -11,6 +15,8 @@ export const onCreateBet = functions.firestore const { contractId } = context.params as { contractId: string } + const { eventId } = context + const bet = change.data() as Bet const lastBetTime = bet.createdTime @@ -18,4 +24,47 @@ export const onCreateBet = functions.firestore .collection('contracts') .doc(contractId) .update({ lastBetTime, lastUpdatedTime: Date.now() }) + + await notifyFills(bet, contractId, eventId) }) + +const notifyFills = async (bet: Bet, contractId: string, eventId: string) => { + if (!bet.fills) return + + const user = await getUser(bet.userId) + if (!user) return + const contract = await getContract(contractId) + if (!contract) return + + const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null) + const matchedBets = ( + await Promise.all( + matchedFills.map((fill) => + getValues<LimitBet>( + firestore.collectionGroup('bets').where('id', '==', fill.matchedBetId) + ) + ) + ) + ).flat() + + const betUsers = await Promise.all( + matchedBets.map((bet) => getUser(bet.userId)) + ) + const betUsersById = keyBy(filterDefined(betUsers), 'id') + + await Promise.all( + matchedBets.map((matchedBet) => { + const matchedUser = betUsersById[matchedBet.userId] + if (!matchedUser) return + + return createBetFillNotification( + user, + matchedUser, + bet, + matchedBet, + contract, + eventId + ) + }) + ) +} diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts index b6ba6e0b..0ace3c53 100644 --- a/functions/src/on-update-user.ts +++ b/functions/src/on-update-user.ts @@ -5,6 +5,8 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' import { createNotification } from './create-notification' import { ReferralTxn } from '../../common/txn' import { Contract } from '../../common/contract' +import { LimitBet } from 'common/bet' +import { QuerySnapshot } from 'firebase-admin/firestore' const firestore = admin.firestore() export const onUpdateUser = functions.firestore @@ -17,6 +19,10 @@ export const onUpdateUser = functions.firestore if (prevUser.referredByUserId !== user.referredByUserId) { await handleUserUpdatedReferral(user, eventId) } + + if (user.balance <= 0) { + await cancelLimitOrders(user.id) + } }) async function handleUserUpdatedReferral(user: User, eventId: string) { @@ -109,3 +115,15 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { ) }) } + +async function cancelLimitOrders(userId: string) { + const snapshot = (await firestore + .collectionGroup('bets') + .where('userId', '==', userId) + .where('isFilled', '==', false) + .get()) as QuerySnapshot<LimitBet> + + await Promise.all( + snapshot.docs.map((doc) => doc.ref.update({ isCancelled: true })) + ) +} diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 43906f3c..52daf953 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,17 +1,25 @@ import * as admin from 'firebase-admin' import { z } from 'zod' +import { + DocumentReference, + FieldValue, + Query, + Transaction, +} from 'firebase-admin/firestore' +import { groupBy, mapValues, sumBy } from 'lodash' import { APIError, newEndpoint, validate } from './api' import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' import { User } from '../../common/user' import { BetInfo, - getNewBinaryCpmmBetInfo, - getNewBinaryDpmBetInfo, + getBinaryCpmmBetInfo, getNewMultiBetInfo, getNumericBetsInfo, } from '../../common/new-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' +import { LimitBet } from '../../common/bet' +import { floatingEqual } from '../../common/util/math' import { redeemShares } from './redeem-shares' import { log } from './utils' @@ -22,6 +30,7 @@ const bodySchema = z.object({ const binarySchema = z.object({ outcome: z.enum(['YES', 'NO']), + limitProb: z.number().gte(0.001).lte(0.999).optional(), }) const freeResponseSchema = z.object({ @@ -63,16 +72,30 @@ export const placebet = newEndpoint({}, async (req, auth) => { newTotalBets, newTotalLiquidity, newP, - } = await (async (): Promise<BetInfo> => { - if (outcomeType == 'BINARY' && mechanism == 'dpm-2') { - const { outcome } = validate(binarySchema, req.body) - return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount) - } else if ( - (outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') && + makers, + } = await (async (): Promise< + BetInfo & { + makers?: maker[] + } + > => { + if ( + (outcomeType == 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') && mechanism == 'cpmm-1' ) { - const { outcome } = validate(binarySchema, req.body) - return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount) + const { outcome, limitProb } = validate(binarySchema, req.body) + + const unfilledBetsSnap = await trans.get( + getUnfilledBetsQuery(contractDoc) + ) + const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data()) + + return getBinaryCpmmBetInfo( + outcome, + amount, + contract, + limitProb, + unfilledBets + ) } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') { const { outcome } = validate(freeResponseSchema, req.body) const answerDoc = contractDoc.collection('answers').doc(outcome) @@ -97,11 +120,15 @@ export const placebet = newEndpoint({}, async (req, auth) => { throw new APIError(400, 'Bet too large for current liquidity pool.') } - const newBalance = user.balance - amount - loanAmount const betDoc = contractDoc.collection('bets').doc() trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet }) log('Created new bet document.') - trans.update(userDoc, { balance: newBalance }) + + if (makers) { + updateMakers(makers, betDoc.id, contractDoc, trans) + } + + trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) }) log('Updated user balance.') trans.update( contractDoc, @@ -112,7 +139,7 @@ export const placebet = newEndpoint({}, async (req, auth) => { totalBets: newTotalBets, totalLiquidity: newTotalLiquidity, collectedFees: addObjects(newBet.fees, collectedFees), - volume: volume + amount, + volume: volume + newBet.amount, }) ) log('Updated contract properties.') @@ -127,3 +154,54 @@ export const placebet = newEndpoint({}, async (req, auth) => { }) const firestore = admin.firestore() + +export const getUnfilledBetsQuery = (contractDoc: DocumentReference) => { + return contractDoc + .collection('bets') + .where('isFilled', '==', false) + .where('isCancelled', '==', false) as Query<LimitBet> +} + +type maker = { + bet: LimitBet + amount: number + shares: number + timestamp: number +} +export const updateMakers = ( + makers: maker[], + takerBetId: string, + contractDoc: DocumentReference, + trans: Transaction +) => { + const makersByBet = groupBy(makers, (maker) => maker.bet.id) + for (const makers of Object.values(makersByBet)) { + const bet = makers[0].bet + const newFills = makers.map((maker) => { + const { amount, shares, timestamp } = maker + return { amount, shares, matchedBetId: takerBetId, timestamp } + }) + const fills = [...bet.fills, ...newFills] + const totalShares = sumBy(fills, 'shares') + const totalAmount = sumBy(fills, 'amount') + const isFilled = floatingEqual(totalAmount, bet.orderAmount) + + log('Updated a matched limit bet.') + trans.update(contractDoc.collection('bets').doc(bet.id), { + fills, + isFilled, + amount: totalAmount, + shares: totalShares, + }) + } + + // Deduct balance of makers. + const spentByUser = mapValues( + groupBy(makers, (maker) => maker.bet.userId), + (makers) => sumBy(makers, (maker) => maker.amount) + ) + for (const [userId, spent] of Object.entries(spentByUser)) { + const userDoc = firestore.collection('users').doc(userId) + trans.update(userDoc, { balance: FieldValue.increment(-spent) }) + } +} diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 62e43105..3407760b 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -9,6 +9,9 @@ import { getCpmmSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' import { getValues } from './utils' import { Bet } from '../../common/bet' +import { floatingLesserEqual } from '../../common/util/math' +import { getUnfilledBetsQuery, updateMakers } from './place-bet' +import { FieldValue } from 'firebase-admin/firestore' const bodySchema = z.object({ contractId: z.string(), @@ -46,14 +49,22 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) const maxShares = sumBy(outcomeBets, (bet) => bet.shares) - if (shares > maxShares) + if (!floatingLesserEqual(shares, maxShares)) throw new APIError(400, `You can only sell up to ${maxShares} shares.`) - const { newBet, newPool, newP, fees } = getCpmmSellBetInfo( - shares, + const soldShares = Math.min(shares, maxShares) + + const unfilledBetsSnap = await transaction.get( + getUnfilledBetsQuery(contractDoc) + ) + const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data()) + + const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo( + soldShares, outcome, contract, - prevLoanAmount + prevLoanAmount, + unfilledBets ) if ( @@ -65,11 +76,17 @@ export const sellshares = newEndpoint({}, async (req, auth) => { } const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc() - const newBalance = user.balance - newBet.amount + (newBet.loanAmount ?? 0) - const userId = user.id - transaction.update(userDoc, { balance: newBalance }) - transaction.create(newBetDoc, { id: newBetDoc.id, userId, ...newBet }) + updateMakers(makers, newBetDoc.id, contractDoc, transaction) + + transaction.update(userDoc, { + balance: FieldValue.increment(-newBet.amount), + }) + transaction.create(newBetDoc, { + id: newBetDoc.id, + userId: user.id, + ...newBet, + }) transaction.update( contractDoc, removeUndefinedProps({ diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index a43f6f12..271eeecc 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,13 +1,10 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' import { partition, sumBy } from 'lodash' +import { SwitchHorizontalIcon } from '@heroicons/react/solid' import { useUser } from 'web/hooks/use-user' -import { - BinaryContract, - CPMMBinaryContract, - PseudoNumericContract, -} from 'common/contract' +import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' @@ -18,20 +15,16 @@ import { formatPercent, formatWithCommas, } from 'common/util/format' +import { getBinaryCpmmBetInfo } from 'common/new-bet' import { Title } from './title' import { User } from 'web/lib/firebase/users' -import { Bet } from 'common/bet' +import { Bet, LimitBet } from 'common/bet' import { APIError, placeBet } from 'web/lib/firebase/api-call' import { sellShares } from 'web/lib/firebase/api-call' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' -import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' -import { - calculatePayoutAfterCorrectBet, - calculateShares, - getProbability, - getOutcomeProbabilityAfterBet, -} from 'common/calculate' +import { BinaryOutcomeLabel } from './outcome-label' +import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' import { @@ -39,178 +32,153 @@ import { getCpmmProbability, getCpmmFees, } from 'common/calculate-cpmm' -import { getFormattedMappedValue } from 'common/pseudo-numeric' +import { + getFormattedMappedValue, + getPseudoProbability, +} from 'common/pseudo-numeric' import { SellRow } from './sell-row' -import { useSaveShares } from './use-save-shares' +import { useSaveBinaryShares } from './use-save-binary-shares' import { SignUpPrompt } from './sign-up-prompt' import { isIOS } from 'web/lib/util/device' +import { ProbabilityInput } from './probability-input' import { track } from 'web/lib/service/analytics' +import { removeUndefinedProps } from 'common/util/object' +import { useUnfilledBets } from 'web/hooks/use-bets' +import { LimitBets } from './limit-bets' +import { BucketInput } from './bucket-input' export function BetPanel(props: { - contract: BinaryContract | PseudoNumericContract + contract: CPMMBinaryContract | PseudoNumericContract className?: string }) { const { contract, className } = props const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) - const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets) - const sharesOutcome = yesFloorShares - ? 'YES' - : noFloorShares - ? 'NO' - : undefined + const unfilledBets = useUnfilledBets(contract.id) ?? [] + const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id) + const { sharesOutcome } = useSaveBinaryShares(contract, userBets) + + const [isLimitOrder, setIsLimitOrder] = useState(false) return ( <Col className={className}> <SellRow contract={contract} user={user} - className={'rounded-t-md bg-gray-100 px-6 py-6'} + className={'rounded-t-md bg-gray-100 px-4 py-5'} /> <Col className={clsx( - 'rounded-b-md bg-white px-8 py-6', + 'relative rounded-b-md bg-white px-8 py-6', !sharesOutcome && 'rounded-t-md', className )} > - <div className="mb-6 text-2xl">Place your bet</div> - {/* <Title className={clsx('!mt-0 text-neutral')} text="Place a trade" /> */} + <Row className="align-center justify-between"> + <div className="mb-6 text-2xl"> + {isLimitOrder ? <>Limit bet</> : <>Place your bet</>} + </div> + <button + className="btn btn-ghost btn-sm text-sm normal-case" + onClick={() => setIsLimitOrder(!isLimitOrder)} + > + <SwitchHorizontalIcon className="inline h-6 w-6" /> + </button> + </Row> - <BuyPanel contract={contract} user={user} /> + <BuyPanel + contract={contract} + user={user} + isLimitOrder={isLimitOrder} + unfilledBets={unfilledBets} + /> <SignUpPrompt /> </Col> + {yourUnfilledBets.length > 0 && ( + <LimitBets + className="mt-4" + contract={contract} + bets={yourUnfilledBets} + /> + )} </Col> ) } -export function BetPanelSwitcher(props: { - contract: BinaryContract | PseudoNumericContract +export function SimpleBetPanel(props: { + contract: CPMMBinaryContract | PseudoNumericContract className?: string - title?: string // Set if BetPanel is on a feed modal selected?: 'YES' | 'NO' onBetSuccess?: () => void }) { - const { contract, className, title, selected, onBetSuccess } = props - - const { mechanism, outcomeType } = contract - const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const { contract, className, selected, onBetSuccess } = props const user = useUser() - const userBets = useUserContractBets(user?.id, contract.id) + const [isLimitOrder, setIsLimitOrder] = useState(false) - const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY') - - const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( - contract, - userBets - ) - - const floorShares = yesFloorShares || noFloorShares - const sharesOutcome = yesFloorShares - ? 'YES' - : noFloorShares - ? 'NO' - : undefined - - useEffect(() => { - // Switch back to BUY if the user has sold all their shares. - if (tradeType === 'SELL' && sharesOutcome === undefined) { - setTradeType('BUY') - } - }, [tradeType, sharesOutcome]) + const unfilledBets = useUnfilledBets(contract.id) ?? [] + const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id) return ( <Col className={className}> - {sharesOutcome && mechanism === 'cpmm-1' && ( - <Col className="rounded-t-md bg-gray-100 px-6 py-6"> - <Row className="items-center justify-between gap-2"> - <div> - You have {formatWithCommas(floorShares)}{' '} - {isPseudoNumeric ? ( - <PseudoNumericOutcomeLabel outcome={sharesOutcome} /> - ) : ( - <BinaryOutcomeLabel outcome={sharesOutcome} /> - )}{' '} - shares - </div> - - {tradeType === 'BUY' && ( - <button - className="btn btn-sm" - style={{ - backgroundColor: 'white', - border: '2px solid', - color: '#3D4451', - }} - onClick={() => - tradeType === 'BUY' - ? setTradeType('SELL') - : setTradeType('BUY') - } - > - {tradeType === 'BUY' ? 'Sell' : 'Bet'} - </button> - )} - </Row> - </Col> - )} - - <Col - className={clsx( - 'rounded-b-md bg-white px-8 py-6', - !sharesOutcome && 'rounded-t-md' - )} - > - <Title - className={clsx( - '!mt-0', - tradeType === 'BUY' && title ? '!text-xl' : '' - )} - text={tradeType === 'BUY' ? title ?? 'Place a trade' : 'Sell shares'} - /> - - {tradeType === 'SELL' && - mechanism == 'cpmm-1' && - user && - sharesOutcome && ( - <SellPanel - contract={contract} - shares={yesShares || noShares} - sharesOutcome={sharesOutcome} - user={user} - userBets={userBets ?? []} - onSellSuccess={onBetSuccess} - /> - )} - - {tradeType === 'BUY' && ( - <BuyPanel - contract={contract} - user={user} - selected={selected} - onBuySuccess={onBetSuccess} + <Col className={clsx('rounded-b-md rounded-t-md bg-white px-8 py-6')}> + <Row className="justify-between"> + <Title + className={clsx('!mt-0')} + text={isLimitOrder ? 'Limit bet' : 'Place a trade'} /> - )} + + <button + className="btn btn-ghost btn-sm text-sm normal-case" + onClick={() => setIsLimitOrder(!isLimitOrder)} + > + <SwitchHorizontalIcon className="inline h-6 w-6" /> + </button> + </Row> + + <BuyPanel + contract={contract} + user={user} + unfilledBets={unfilledBets} + selected={selected} + onBuySuccess={onBetSuccess} + isLimitOrder={isLimitOrder} + /> <SignUpPrompt /> </Col> + + {yourUnfilledBets.length > 0 && ( + <LimitBets + className="mt-4" + contract={contract} + bets={yourUnfilledBets} + /> + )} </Col> ) } function BuyPanel(props: { - contract: BinaryContract | PseudoNumericContract + contract: CPMMBinaryContract | PseudoNumericContract user: User | null | undefined + unfilledBets: Bet[] + isLimitOrder?: boolean selected?: 'YES' | 'NO' onBuySuccess?: () => void }) { - const { contract, user, selected, onBuySuccess } = props + const { contract, user, unfilledBets, isLimitOrder, selected, onBuySuccess } = + props + + const initialProb = getProbability(contract) const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected) const [betAmount, setBetAmount] = useState<number | undefined>(undefined) + const [limitProb, setLimitProb] = useState<number | undefined>( + Math.round(100 * initialProb) + ) const [error, setError] = useState<string | undefined>() const [isSubmitting, setIsSubmitting] = useState(false) const [wasSubmitted, setWasSubmitted] = useState(false) @@ -240,15 +208,22 @@ function BuyPanel(props: { async function submitBet() { if (!user || !betAmount) return + if (isLimitOrder && limitProb === undefined) return + + const limitProbScaled = + isLimitOrder && limitProb !== undefined ? limitProb / 100 : undefined setError(undefined) setIsSubmitting(true) - placeBet({ - amount: betAmount, - outcome: betChoice, - contractId: contract.id, - }) + placeBet( + removeUndefinedProps({ + amount: betAmount, + outcome: betChoice, + contractId: contract.id, + limitProb: limitProbScaled, + }) + ) .then((r) => { console.log('placed bet. Result:', r) setIsSubmitting(false) @@ -278,42 +253,31 @@ function BuyPanel(props: { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getProbability(contract) + const limitProbFrac = (limitProb ?? 0) / 100 - const outcomeProb = getOutcomeProbabilityAfterBet( + const { newPool, newP, newBet } = getBinaryCpmmBetInfo( + betChoice ?? 'YES', + betAmount ?? 0, contract, - betChoice || 'YES', - betAmount ?? 0 + isLimitOrder ? limitProbFrac : undefined, + unfilledBets as LimitBet[] ) - const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb - const shares = calculateShares(contract, betAmount ?? 0, betChoice || 'YES') - - const currentPayout = betAmount - ? calculatePayoutAfterCorrectBet(contract, { - outcome: betChoice, - amount: betAmount, - shares, - } as Bet) + const resultProb = getCpmmProbability(newPool, newP) + const remainingMatched = isLimitOrder + ? ((newBet.orderAmount ?? 0) - newBet.amount) / + (betChoice === 'YES' ? limitProbFrac : 1 - limitProbFrac) : 0 + const currentPayout = newBet.shares + remainingMatched const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturnPercent = formatPercent(currentReturn) - const cpmmFees = - contract.mechanism === 'cpmm-1' && - getCpmmFees(contract, betAmount ?? 0, betChoice ?? 'YES').totalFees - - const dpmTooltip = - contract.mechanism === 'dpm-2' - ? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas( - shares + - contract.totalShares[betChoice ?? 'YES'] - - (contract.phantomShares - ? contract.phantomShares[betChoice ?? 'YES'] - : 0) - )} ${betChoice ?? 'YES'} shares` - : undefined + const cpmmFees = getCpmmFees( + contract, + betAmount ?? 0, + betChoice ?? 'YES' + ).totalFees const format = getFormattedMappedValue(contract) @@ -336,29 +300,62 @@ function BuyPanel(props: { disabled={isSubmitting} inputRef={inputRef} /> - + {isLimitOrder && ( + <> + <Row className="my-3 items-center gap-2 text-left text-sm text-gray-500"> + Limit {isPseudoNumeric ? 'value' : 'probability'} + <InfoTooltip + text={`Bet ${betChoice === 'NO' ? 'down' : 'up'} to this ${ + isPseudoNumeric ? 'value' : 'probability' + } and wait to match other bets.`} + /> + </Row> + {isPseudoNumeric ? ( + <BucketInput + contract={contract} + onBucketChange={(value) => + setLimitProb( + value === undefined + ? undefined + : 100 * + getPseudoProbability( + value, + contract.min, + contract.max, + contract.isLogScale + ) + ) + } + isSubmitting={isSubmitting} + /> + ) : ( + <ProbabilityInput + inputClassName="w-full max-w-none" + prob={limitProb} + onChange={setLimitProb} + disabled={isSubmitting} + /> + )} + </> + )} <Col className="mt-3 w-full gap-3"> - <Row className="items-center justify-between text-sm"> - <div className="text-gray-500"> - {isPseudoNumeric ? 'Estimated value' : 'Probability'} - </div> - <div> - {format(initialProb)} - <span className="mx-2">→</span> - {format(resultProb)} - </div> - </Row> + {!isLimitOrder && ( + <Row className="items-center justify-between text-sm"> + <div className="text-gray-500"> + {isPseudoNumeric ? 'Estimated value' : 'Probability'} + </div> + <div> + {format(initialProb)} + <span className="mx-2">→</span> + {format(resultProb)} + </div> + </Row> + )} <Row className="items-center justify-between gap-2 text-sm"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <div> - {contract.mechanism === 'dpm-2' ? ( - <> - Estimated - <br /> payout if{' '} - <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> - </> - ) : isPseudoNumeric ? ( + {isPseudoNumeric ? ( 'Max payout' ) : ( <> @@ -366,14 +363,9 @@ function BuyPanel(props: { </> )} </div> - - {cpmmFees !== false && ( - <InfoTooltip - text={`Includes ${formatMoneyWithDecimals(cpmmFees)} in fees`} - /> - )} - - {dpmTooltip && <InfoTooltip text={dpmTooltip} />} + <InfoTooltip + text={`Includes ${formatMoneyWithDecimals(cpmmFees)} in fees`} + /> </Row> <div> <span className="mr-2 whitespace-nowrap"> @@ -424,19 +416,21 @@ export function SellPanel(props: { const [isSubmitting, setIsSubmitting] = useState(false) const [wasSubmitted, setWasSubmitted] = useState(false) + const unfilledBets = useUnfilledBets(contract.id) ?? [] + const betDisabled = isSubmitting || !amount || error + // Sell all shares if remaining shares would be < 1 + const sellQuantity = amount === Math.floor(shares) ? shares : amount + async function submitSell() { if (!user || !amount) return setError(undefined) setIsSubmitting(true) - // Sell all shares if remaining shares would be < 1 - const sellAmount = amount === Math.floor(shares) ? shares : amount - await sellShares({ - shares: sellAmount, + shares: sellQuantity, outcome: sharesOutcome, contractId: contract.id, }) @@ -461,18 +455,19 @@ export function SellPanel(props: { outcomeType: contract.outcomeType, slug: contract.slug, contractId: contract.id, - shares: sellAmount, + shares: sellQuantity, outcome: sharesOutcome, }) } const initialProb = getProbability(contract) - const { newPool } = calculateCpmmSale( + const { cpmmState, saleValue } = calculateCpmmSale( contract, - Math.min(amount ?? 0, shares), - sharesOutcome + sellQuantity ?? 0, + sharesOutcome, + unfilledBets ) - const resultProb = getCpmmProbability(newPool, contract.p) + const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p) const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale) const [yesBets, noBets] = partition( @@ -484,17 +479,8 @@ export function SellPanel(props: { sumBy(noBets, (bet) => bet.shares), ] - const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined const ownedShares = Math.round(yesShares) || Math.round(noShares) - const sharesSold = Math.min(amount ?? 0, ownedShares) - - const { saleValue } = calculateCpmmSale( - contract, - sharesSold, - sellOutcome as 'YES' | 'NO' - ) - const onAmountChange = (amount: number | undefined) => { setAmount(amount) diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index ae5e0b00..712d4a2c 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -1,18 +1,18 @@ import { useState } from 'react' import clsx from 'clsx' -import { BetPanelSwitcher } from './bet-panel' +import { SimpleBetPanel } from './bet-panel' import { YesNoSelector } from './yes-no-selector' -import { BinaryContract, PseudoNumericContract } from 'common/contract' +import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { Modal } from './layout/modal' import { SellButton } from './sell-button' import { useUser } from 'web/hooks/use-user' import { useUserContractBets } from 'web/hooks/use-user-bets' -import { useSaveShares } from './use-save-shares' +import { useSaveBinaryShares } from './use-save-binary-shares' // Inline version of a bet panel. Opens BetPanel in a new modal. export default function BetRow(props: { - contract: BinaryContract | PseudoNumericContract + contract: CPMMBinaryContract | PseudoNumericContract className?: string btnClassName?: string betPanelClassName?: string @@ -24,10 +24,8 @@ export default function BetRow(props: { ) const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) - const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( - contract, - userBets - ) + const { yesShares, noShares, hasYesShares, hasNoShares } = + useSaveBinaryShares(contract, userBets) return ( <> @@ -40,7 +38,7 @@ export default function BetRow(props: { setBetChoice(choice) }} replaceNoButton={ - yesFloorShares > 0 ? ( + hasYesShares ? ( <SellButton panelClassName={betPanelClassName} contract={contract} @@ -51,7 +49,7 @@ export default function BetRow(props: { ) : undefined } replaceYesButton={ - noFloorShares > 0 ? ( + hasNoShares ? ( <SellButton panelClassName={betPanelClassName} contract={contract} @@ -63,10 +61,9 @@ export default function BetRow(props: { } /> <Modal open={open} setOpen={setOpen}> - <BetPanelSwitcher + <SimpleBetPanel className={betPanelClassName} contract={contract} - title={contract.question} selected={betChoice} onBetSuccess={() => setOpen(false)} /> diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index b8fb7d31..72ac23db 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -44,6 +44,9 @@ import { NumericContract } from 'common/contract' import { formatNumericProbability } from 'common/pseudo-numeric' import { useUser } from 'web/hooks/use-user' import { SellSharesModal } from './sell-modal' +import { useUnfilledBets } from 'web/hooks/use-bets' +import { LimitBet } from 'common/bet' +import { floatingEqual } from 'common/util/math' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' @@ -390,6 +393,12 @@ export function BetsSummary(props: { const [showSellModal, setShowSellModal] = useState(false) const user = useUser() + const sharesOutcome = floatingEqual(totalShares.YES, 0) + ? floatingEqual(totalShares.NO, 0) + ? undefined + : 'NO' + : 'YES' + return ( <Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}> <Row className="flex-wrap gap-4 sm:gap-6"> @@ -469,6 +478,7 @@ export function BetsSummary(props: { !isClosed && !resolution && hasShares && + sharesOutcome && user && ( <> <button @@ -482,8 +492,8 @@ export function BetsSummary(props: { contract={contract} user={user} userBets={bets} - shares={totalShares.YES || totalShares.NO} - sharesOutcome={totalShares.YES ? 'YES' : 'NO'} + shares={totalShares[sharesOutcome]} + sharesOutcome={sharesOutcome} setOpen={setShowSellModal} /> )} @@ -505,7 +515,7 @@ export function ContractBetsTable(props: { const { contract, className, isYourBets } = props const bets = sortBy( - props.bets.filter((b) => !b.isAnte), + props.bets.filter((b) => !b.isAnte && b.amount !== 0), (bet) => bet.createdTime ).reverse() @@ -531,6 +541,8 @@ export function ContractBetsTable(props: { const isNumeric = outcomeType === 'NUMERIC' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const unfilledBets = useUnfilledBets(contract.id) ?? [] + return ( <div className={clsx('overflow-x-auto', className)}> {amountRedeemed > 0 && ( @@ -577,6 +589,7 @@ export function ContractBetsTable(props: { saleBet={salesDict[bet.id]} contract={contract} isYourBet={isYourBets} + unfilledBets={unfilledBets} /> ))} </tbody> @@ -590,8 +603,9 @@ function BetRow(props: { contract: Contract saleBet?: Bet isYourBet: boolean + unfilledBets: LimitBet[] }) { - const { bet, saleBet, contract, isYourBet } = props + const { bet, saleBet, contract, isYourBet, unfilledBets } = props const { amount, outcome, @@ -621,7 +635,7 @@ function BetRow(props: { formatMoney( isResolved ? resolvedPayout(contract, bet) - : calculateSaleAmount(contract, bet) + : calculateSaleAmount(contract, bet, unfilledBets) ) ) @@ -681,9 +695,16 @@ function SellButton(props: { contract: Contract; bet: Bet }) { outcome === 'NO' ? 'YES' : outcome ) - const outcomeProb = getProbabilityAfterSale(contract, outcome, shares) + const unfilledBets = useUnfilledBets(contract.id) ?? [] - const saleAmount = calculateSaleAmount(contract, bet) + const outcomeProb = getProbabilityAfterSale( + contract, + outcome, + shares, + unfilledBets + ) + + const saleAmount = calculateSaleAmount(contract, bet, unfilledBets) const profit = saleAmount - bet.amount return ( diff --git a/web/components/bucket-input.tsx b/web/components/bucket-input.tsx index 86456bff..195032dc 100644 --- a/web/components/bucket-input.tsx +++ b/web/components/bucket-input.tsx @@ -1,12 +1,12 @@ import { useState } from 'react' -import { NumericContract } from 'common/contract' +import { NumericContract, PseudoNumericContract } from 'common/contract' import { getMappedBucket } from 'common/calculate-dpm' import { NumberInput } from './number-input' export function BucketInput(props: { - contract: NumericContract + contract: NumericContract | PseudoNumericContract isSubmitting?: boolean onBucketChange: (value?: number, bucket?: string) => void }) { @@ -24,7 +24,10 @@ export function BucketInput(props: { return } - const bucket = getMappedBucket(value, contract) + const bucket = + contract.outcomeType === 'PSEUDO_NUMERIC' + ? '' + : getMappedBucket(value, contract) onBucketChange(value, bucket) } diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index c6cda43c..30c54363 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -52,10 +52,7 @@ export function ContractCard(props: { const showQuickBet = user && !marketClosed && - !( - outcomeType === 'FREE_RESPONSE' && getTopAnswer(contract) === undefined - ) && - outcomeType !== 'NUMERIC' && + (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') && !hideQuickBet return ( diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 897bef04..1fc8e077 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -16,7 +16,7 @@ import { import { Bet } from 'common/bet' import BetRow from '../bet-row' import { AnswersGraph } from '../answers/answers-graph' -import { Contract } from 'common/contract' +import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' import { ShareMarket } from '../share-market' @@ -70,6 +70,13 @@ export const ContractOverview = (props: { <Row className="items-center justify-between gap-4 xl:hidden"> <BinaryResolutionOrChance contract={contract} /> + {tradingAllowed(contract) && ( + <BetRow contract={contract as CPMMBinaryContract} /> + )} + </Row> + ) : isPseudoNumeric ? ( + <Row className="items-center justify-between gap-4 xl:hidden"> + <PseudoNumericResolutionOrExpectation contract={contract} /> {tradingAllowed(contract) && <BetRow contract={contract} />} </Row> ) : isPseudoNumeric ? ( diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 76ee7536..0ce1c3f5 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -7,7 +7,13 @@ import { } from 'common/calculate' import { getExpectedValue } from 'common/calculate-dpm' import { User } from 'common/user' -import { Contract, NumericContract, resolution } from 'common/contract' +import { + BinaryContract, + Contract, + NumericContract, + PseudoNumericContract, + resolution, +} from 'common/contract' import { formatLargeNumber, formatMoney, @@ -22,33 +28,30 @@ import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon' import { Col } from '../layout/col' import { OUTCOME_TO_COLOR } from '../outcome-label' -import { useSaveShares } from '../use-save-shares' +import { useSaveBinaryShares } from '../use-save-binary-shares' import { sellShares } from 'web/lib/firebase/api-call' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { track } from 'web/lib/service/analytics' import { formatNumericProbability } from 'common/pseudo-numeric' +import { useUnfilledBets } from 'web/hooks/use-bets' const BET_SIZE = 10 -export function QuickBet(props: { contract: Contract; user: User }) { +export function QuickBet(props: { + contract: BinaryContract | PseudoNumericContract + user: User +}) { const { contract, user } = props const { mechanism, outcomeType } = contract const isCpmm = mechanism === 'cpmm-1' const userBets = useUserContractBets(user.id, contract.id) - const topAnswer = - outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined + const unfilledBets = useUnfilledBets(contract.id) ?? [] - // TODO: yes/no from useSaveShares doesn't work on numeric contracts - const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( - contract, - userBets, - topAnswer?.number.toString() || undefined - ) - const hasUpShares = - yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC') - const hasDownShares = - noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC' + const { hasYesShares, hasNoShares, yesShares, noShares } = + useSaveBinaryShares(contract, userBets) + const hasUpShares = hasYesShares + const hasDownShares = hasNoShares && !hasUpShares const [upHover, setUpHover] = useState(false) const [downHover, setDownHover] = useState(false) @@ -85,13 +88,14 @@ export function QuickBet(props: { contract: Contract; user: User }) { const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob) sharesSold = Math.min(oppositeShares, maxSharesSold) - const { newPool, saleValue } = calculateCpmmSale( + const { cpmmState, saleValue } = calculateCpmmSale( contract, sharesSold, - sellOutcome + sellOutcome, + unfilledBets ) saleAmount = saleValue - previewProb = getCpmmProbability(newPool, contract.p) + previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p) } } @@ -131,13 +135,6 @@ export function QuickBet(props: { contract: Contract; user: User }) { }) } - if (outcomeType === 'FREE_RESPONSE') - return ( - <Col className="relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle"> - <QuickOutcomeView contract={contract} previewProb={previewProb} /> - </Col> - ) - return ( <Col className={clsx( @@ -158,7 +155,7 @@ export function QuickBet(props: { contract: Contract; user: User }) { {formatMoney(10)} </div> - {hasUpShares > 0 ? ( + {hasUpShares ? ( <TriangleFillIcon className={clsx( 'mx-auto h-5 w-5', @@ -193,7 +190,7 @@ export function QuickBet(props: { contract: Contract; user: User }) { onMouseLeave={() => setDownHover(false)} onClick={() => placeQuickBet('DOWN')} ></div> - {hasDownShares > 0 ? ( + {hasDownShares ? ( <TriangleDownFillIcon className={clsx( 'mx-auto h-5 w-5', diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index 8f728d39..c60afa70 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -31,7 +31,9 @@ export function ContractActivity(props: { const comments = updatedComments ?? props.comments const updatedBets = useBets(contract.id) - const bets = (updatedBets ?? props.bets).filter((bet) => !bet.isRedemption) + const bets = (updatedBets ?? props.bets).filter( + (bet) => !bet.isRedemption && bet.amount !== 0 + ) const items = getSpecificContractActivityItems( contract, bets, diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 312190e4..a9618f8c 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -34,7 +34,7 @@ import { TruncatedComment, } from 'web/components/feed/feed-comments' import { FeedBet } from 'web/components/feed/feed-bets' -import { NumericContract } from 'common/contract' +import { CPMMBinaryContract, NumericContract } from 'common/contract' import { FeedLiquidity } from './feed-liquidity' export function FeedItems(props: { @@ -68,7 +68,10 @@ export function FeedItems(props: { ))} </div> {outcomeType === 'BINARY' && tradingAllowed(contract) && ( - <BetRow contract={contract} className={clsx('mb-2', betRowClassName)} /> + <BetRow + contract={contract as CPMMBinaryContract} + className={clsx('mb-2', betRowClassName)} + /> )} </div> ) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx new file mode 100644 index 00000000..5a6a67c0 --- /dev/null +++ b/web/components/limit-bets.tsx @@ -0,0 +1,89 @@ +import clsx from 'clsx' +import { LimitBet } from 'common/bet' +import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' +import { getFormattedMappedValue } from 'common/pseudo-numeric' +import { formatMoney, formatPercent } from 'common/util/format' +import { sortBy } from 'lodash' +import { useState } from 'react' +import { cancelBet } from 'web/lib/firebase/api-call' +import { Col } from './layout/col' +import { LoadingIndicator } from './loading-indicator' +import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' + +export function LimitBets(props: { + contract: CPMMBinaryContract | PseudoNumericContract + bets: LimitBet[] + className?: string +}) { + const { contract, bets, className } = props + const recentBets = sortBy( + bets, + (bet) => -1 * bet.limitProb, + (bet) => -1 * bet.createdTime + ) + + return ( + <Col + className={clsx(className, 'gap-2 overflow-hidden rounded bg-white py-3')} + > + <div className="px-6 py-3 text-2xl">Your limit bets</div> + <div className="px-4"> + <table className="table-compact table w-full rounded text-gray-500"> + <tbody> + {recentBets.map((bet) => ( + <LimitBet key={bet.id} bet={bet} contract={contract} /> + ))} + </tbody> + </table> + </div> + </Col> + ) +} + +function LimitBet(props: { + contract: CPMMBinaryContract | PseudoNumericContract + bet: LimitBet +}) { + const { contract, bet } = props + const { orderAmount, amount, limitProb, outcome } = bet + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' + + const [isCancelling, setIsCancelling] = useState(false) + + const onCancel = () => { + cancelBet({ betId: bet.id }) + setIsCancelling(true) + } + + return ( + <tr> + <td> + <div className="pl-2"> + {isPseudoNumeric ? ( + <PseudoNumericOutcomeLabel outcome={outcome as 'YES' | 'NO'} /> + ) : ( + <BinaryOutcomeLabel outcome={outcome as 'YES' | 'NO'} /> + )} + </div> + </td> + <td>{formatMoney(orderAmount - amount)}</td> + <td> + {isPseudoNumeric + ? getFormattedMappedValue(contract)(limitProb) + : formatPercent(limitProb)} + </td> + <td> + {isCancelling ? ( + <LoadingIndicator /> + ) : ( + <button + className="btn btn-xs btn-outline my-auto normal-case" + onClick={onCancel} + > + Cancel + </button> + )} + </td> + </tr> + ) +} diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index cf111281..98a2aabc 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -96,7 +96,7 @@ export function NumericResolutionPanel(props: { {outcomeMode === 'NUMBER' && ( <BucketInput - contract={contract as any} + contract={contract} isSubmitting={isSubmitting} onBucketChange={(v, o) => (setValue(v), setOutcome(o))} /> diff --git a/web/components/probability-input.tsx b/web/components/probability-input.tsx new file mode 100644 index 00000000..15f73799 --- /dev/null +++ b/web/components/probability-input.tsx @@ -0,0 +1,49 @@ +import clsx from 'clsx' +import { Col } from './layout/col' +import { Spacer } from './layout/spacer' + +export function ProbabilityInput(props: { + prob: number | undefined + onChange: (newProb: number | undefined) => void + disabled?: boolean + className?: string + inputClassName?: string +}) { + const { prob, onChange, disabled, className, inputClassName } = props + + const onProbChange = (str: string) => { + let prob = parseInt(str.replace(/\D/g, '')) + const isInvalid = !str || isNaN(prob) + if (prob.toString().length > 2) { + if (prob === 100) prob = 99 + else if (prob < 1) prob = 1 + else prob = +prob.toString().slice(-2) + } + onChange(isInvalid ? undefined : prob) + } + + return ( + <Col className={className}> + <label className="input-group"> + <input + className={clsx( + 'input input-bordered max-w-[200px] text-lg', + inputClassName + )} + type="number" + max={99} + min={1} + pattern="[0-9]*" + inputMode="numeric" + placeholder="0" + maxLength={2} + value={prob ?? ''} + disabled={disabled} + onChange={(e) => onProbChange(e.target.value)} + /> + <span className="bg-gray-200 text-sm">%</span> + </label> + <Spacer h={4} /> + </Col> + ) +} diff --git a/web/components/sell-row.tsx b/web/components/sell-row.tsx index a8cb2851..4c12c35c 100644 --- a/web/components/sell-row.tsx +++ b/web/components/sell-row.tsx @@ -6,7 +6,7 @@ import { Row } from './layout/row' import { formatWithCommas } from 'common/util/format' import { OutcomeLabel } from './outcome-label' import { useUserContractBets } from 'web/hooks/use-user-bets' -import { useSaveShares } from './use-save-shares' +import { useSaveBinaryShares } from './use-save-binary-shares' import { SellSharesModal } from './sell-modal' export function SellRow(props: { @@ -20,16 +20,7 @@ export function SellRow(props: { const [showSellModal, setShowSellModal] = useState(false) const { mechanism } = contract - const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( - contract, - userBets - ) - const floorShares = yesFloorShares || noFloorShares - const sharesOutcome = yesFloorShares - ? 'YES' - : noFloorShares - ? 'NO' - : undefined + const { sharesOutcome, shares } = useSaveBinaryShares(contract, userBets) if (sharesOutcome && user && mechanism === 'cpmm-1') { return ( @@ -37,7 +28,7 @@ export function SellRow(props: { <Col className={className}> <Row className="items-center justify-between gap-2 "> <div> - You have {formatWithCommas(floorShares)}{' '} + You have {formatWithCommas(shares)}{' '} <OutcomeLabel outcome={sharesOutcome} contract={contract} @@ -64,7 +55,7 @@ export function SellRow(props: { contract={contract} user={user} userBets={userBets ?? []} - shares={yesShares || noShares} + shares={shares} sharesOutcome={sharesOutcome} setOpen={setShowSellModal} /> diff --git a/web/components/use-save-binary-shares.ts b/web/components/use-save-binary-shares.ts new file mode 100644 index 00000000..fefa8a55 --- /dev/null +++ b/web/components/use-save-binary-shares.ts @@ -0,0 +1,56 @@ +import { BinaryContract, PseudoNumericContract } from 'common/contract' +import { Bet } from 'common/bet' +import { useEffect, useState } from 'react' +import { partition, sumBy } from 'lodash' +import { safeLocalStorage } from 'web/lib/util/local' + +export const useSaveBinaryShares = ( + contract: BinaryContract | PseudoNumericContract, + userBets: Bet[] | undefined +) => { + const [savedShares, setSavedShares] = useState({ yesShares: 0, noShares: 0 }) + + const [yesBets, noBets] = partition( + userBets ?? [], + (bet) => bet.outcome === 'YES' + ) + const [yesShares, noShares] = userBets + ? [sumBy(yesBets, (bet) => bet.shares), sumBy(noBets, (bet) => bet.shares)] + : [savedShares.yesShares, savedShares.noShares] + + useEffect(() => { + const local = safeLocalStorage() + + // Read shares from local storage. + const savedShares = local?.getItem(`${contract.id}-shares`) + if (savedShares) { + setSavedShares(JSON.parse(savedShares)) + } + + if (userBets) { + // Save shares to local storage. + const sharesData = JSON.stringify({ yesShares, noShares }) + local?.setItem(`${contract.id}-shares`, sharesData) + } + }, [contract.id, userBets, noShares, yesShares]) + + const hasYesShares = yesShares >= 1 + const hasNoShares = noShares >= 1 + + const sharesOutcome = hasYesShares + ? ('YES' as const) + : hasNoShares + ? ('NO' as const) + : undefined + const shares = + sharesOutcome === 'YES' ? yesShares : sharesOutcome === 'NO' ? noShares : 0 + + return { + yesShares, + noShares, + shares, + sharesOutcome, + hasYesShares, + hasNoShares, + } +} diff --git a/web/components/use-save-shares.ts b/web/components/use-save-shares.ts deleted file mode 100644 index 494c1f29..00000000 --- a/web/components/use-save-shares.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Contract } from 'common/contract' -import { Bet } from 'common/bet' -import { useEffect, useState } from 'react' -import { partition, sumBy } from 'lodash' -import { safeLocalStorage } from 'web/lib/util/local' - -export const useSaveShares = ( - contract: Contract, - userBets: Bet[] | undefined, - freeResponseAnswerOutcome?: string -) => { - const [savedShares, setSavedShares] = useState< - | { - yesShares: number - noShares: number - yesFloorShares: number - noFloorShares: number - } - | undefined - >() - - // TODO: How do we handle numeric yes / no bets? - maybe bet amounts above vs below the highest peak - const [yesBets, noBets] = partition(userBets ?? [], (bet) => - freeResponseAnswerOutcome - ? bet.outcome === freeResponseAnswerOutcome - : bet.outcome === 'YES' - ) - const [yesShares, noShares] = [ - sumBy(yesBets, (bet) => bet.shares), - sumBy(noBets, (bet) => bet.shares), - ] - - const yesFloorShares = Math.round(yesShares) === 0 ? 0 : Math.floor(yesShares) - const noFloorShares = Math.round(noShares) === 0 ? 0 : Math.floor(noShares) - - useEffect(() => { - const local = safeLocalStorage() - // Save yes and no shares to local storage. - const savedShares = local?.getItem(`${contract.id}-shares`) - if (!userBets && savedShares) { - setSavedShares(JSON.parse(savedShares)) - } - - if (userBets) { - const updatedShares = { yesShares, noShares } - local?.setItem(`${contract.id}-shares`, JSON.stringify(updatedShares)) - } - }, [contract.id, userBets, noShares, yesShares]) - - if (userBets) return { yesShares, noShares, yesFloorShares, noFloorShares } - return ( - savedShares ?? { - yesShares: 0, - noShares: 0, - yesFloorShares: 0, - noFloorShares: 0, - } - ) -} diff --git a/web/hooks/use-bets.ts b/web/hooks/use-bets.ts index 5cab16a7..68b296cd 100644 --- a/web/hooks/use-bets.ts +++ b/web/hooks/use-bets.ts @@ -4,8 +4,10 @@ import { Bet, listenForBets, listenForRecentBets, + listenForUnfilledBets, withoutAnteBets, } from 'web/lib/firebase/bets' +import { LimitBet } from 'common/bet' export const useBets = (contractId: string) => { const [bets, setBets] = useState<Bet[] | undefined>() @@ -36,3 +38,12 @@ export const useRecentBets = () => { useEffect(() => listenForRecentBets(setRecentBets), []) return recentBets } + +export const useUnfilledBets = (contractId: string) => { + const [unfilledBets, setUnfilledBets] = useState<LimitBet[] | undefined>() + useEffect( + () => listenForUnfilledBets(contractId, setUnfilledBets), + [contractId] + ) + return unfilledBets +} diff --git a/web/hooks/use-focus.ts b/web/hooks/use-focus.ts index a71a0292..f41f46a7 100644 --- a/web/hooks/use-focus.ts +++ b/web/hooks/use-focus.ts @@ -1,11 +1,12 @@ import { useRef } from 'react' +import { useEvent } from './use-event' // Focus helper from https://stackoverflow.com/a/54159564/1222351 export function useFocus(): [React.RefObject<HTMLElement>, () => void] { const htmlElRef = useRef<HTMLElement>(null) - const setFocus = () => { + const setFocus = useEvent(() => { htmlElRef.current && htmlElRef.current.focus() - } + }) return [htmlElRef, setFocus] } diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 7882d9ba..94da9f09 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -82,6 +82,10 @@ export function placeBet(params: any) { return call(getFunctionUrl('placebet'), 'POST', params) } +export function cancelBet(params: { betId: string }) { + return call(getFunctionUrl('cancelbet'), 'POST', params) +} + export function sellShares(params: any) { return call(getFunctionUrl('sellshares'), 'POST', params) } diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts index 6fc29d24..ef0ab55d 100644 --- a/web/lib/firebase/bets.ts +++ b/web/lib/firebase/bets.ts @@ -15,7 +15,7 @@ import { import { uniq } from 'lodash' import { db } from './init' -import { Bet } from 'common/bet' +import { Bet, LimitBet } from 'common/bet' import { Contract } from 'common/contract' import { getValues, listenForValues } from './utils' import { getContractFromId } from './contracts' @@ -166,6 +166,21 @@ export function listenForUserContractBets( }) } +export function listenForUnfilledBets( + contractId: string, + setBets: (bets: LimitBet[]) => void +) { + const betsQuery = query( + collection(db, 'contracts', contractId, 'bets'), + where('isFilled', '==', false), + where('isCancelled', '==', false) + ) + return listenForValues<LimitBet>(betsQuery, (bets) => { + bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime) + setBets(bets) + }) +} + export function withoutAnteBets(contract: Contract, bets?: Bet[]) { const { createdTime } = contract diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index e33c116e..e8b290f3 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -39,6 +39,7 @@ import { FeedBet } from 'web/components/feed/feed-bets' import { useIsIframe } from 'web/hooks/use-is-iframe' import ContractEmbedPage from '../embed/[username]/[contractSlug]' import { useBets } from 'web/hooks/use-bets' +import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' @@ -127,6 +128,7 @@ export function ContractPageContent( const tips = useTipTxns({ contractId: contract.id }) const user = useUser() + const { width, height } = useWindowSize() const [showConfetti, setShowConfetti] = useState(false) @@ -169,7 +171,10 @@ export function ContractPageContent( (isNumeric ? ( <NumericBetPanel className="hidden xl:flex" contract={contract} /> ) : ( - <BetPanel className="hidden xl:flex" contract={contract} /> + <BetPanel + className="hidden xl:flex" + contract={contract as CPMMBinaryContract} + /> ))} {allowResolve && (isNumeric || isPseudoNumeric ? ( diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 93439be7..dc8cb51d 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -1,5 +1,5 @@ import { Bet } from 'common/bet' -import { Contract } from 'common/contract' +import { Contract, CPMMBinaryContract } from 'common/contract' import { DOMAIN } from 'common/envs/constants' import { AnswersGraph } from 'web/components/answers/answers-graph' import BetRow from 'web/components/bet-row' @@ -112,7 +112,10 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { {isBinary && ( <Row className="items-center gap-4"> - <BetRow contract={contract} betPanelClassName="scale-75" /> + <BetRow + contract={contract as CPMMBinaryContract} + betPanelClassName="scale-75" + /> <BinaryResolutionOrChance contract={contract} /> </Row> )} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 3a8e4bc0..39cc2017 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -795,6 +795,8 @@ function getSourceIdForLinkComponent( return sourceId case 'contract': return '' + case 'bet': + return '' default: return sourceId } @@ -861,8 +863,16 @@ function NotificationTextLabel(props: { {'+' + formatMoney(parseInt(sourceText))} </span> ) + } else if (sourceType === 'bet' && sourceText) { + return ( + <> + <span className="text-primary"> + {formatMoney(parseInt(sourceText))} + </span>{' '} + <span>of your limit bet was filled</span> + </> + ) } - // return default text return ( <div className={className ? className : 'line-clamp-4 whitespace-pre-line'}> <Linkify text={defaultText} /> @@ -913,6 +923,9 @@ function getReasonForShowingNotification( else if (sourceSlug) reasonText = 'joined because you shared' else reasonText = 'joined because of you' break + case 'bet': + reasonText = 'bet against you' + break default: reasonText = '' } From 4de22acb3ea6cb168d669a194986b7ace5459143 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 13:24:54 -0500 Subject: [PATCH 092/519] Tweak check for matching with pool --- common/new-bet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/new-bet.ts b/common/new-bet.ts index 6c3e6856..f484b9f7 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -66,8 +66,8 @@ const computeFill = ( if ( !matchedBet || (outcome === 'YES' - ? prob < matchedBet.limitProb - : prob > matchedBet.limitProb) + ? !floatingGreaterEqual(prob, matchedBet.limitProb) + : !floatingLesserEqual(prob, matchedBet.limitProb)) ) { // Fill from pool. const limit = !matchedBet From 900fc7550653b2fe07ffb7da2e29175f5767e8cb Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 13:45:32 -0500 Subject: [PATCH 093/519] Add sourceContractId to bet_fill notification --- functions/src/create-notification.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 0d3432a7..1fb6c3af 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -413,6 +413,7 @@ export const createBetFillNotification = async ( sourceContractCreatorUsername: contract.creatorUsername, sourceContractTitle: contract.question, sourceContractSlug: contract.slug, + sourceContractId: contract.id, } return await notificationRef.set(removeUndefinedProps(notification)) } From f2df32e71010868c7f3b2faf042570fe9d3d4f4d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 14:52:31 -0500 Subject: [PATCH 094/519] PseudoNumeric markets store resolveValue in resolved notification and render it --- functions/src/on-update-contract.ts | 3 +++ web/components/outcome-label.tsx | 7 ++++++- web/pages/notifications.tsx | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index f47c019c..4674bd82 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -24,6 +24,9 @@ export const onUpdateContract = functions.firestore if (resolutionText === 'MKT' && contract.resolutionProbability) resolutionText = `${contract.resolutionProbability}%` else if (resolutionText === 'MKT') resolutionText = 'PROB' + } else if (contract.outcomeType === 'PSEUDO_NUMERIC') { + if (resolutionText === 'MKT' && contract.resolutionValue) + resolutionText = `${contract.resolutionValue}` } await createNotification( diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index a94618e4..9ecda16f 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -9,7 +9,7 @@ import { FreeResponseContract, resolution, } from 'common/contract' -import { formatPercent } from 'common/util/format' +import { formatLargeNumber, formatPercent } from 'common/util/format' import { ClientRender } from './client-render' export function OutcomeLabel(props: { @@ -140,6 +140,11 @@ export function ProbPercentLabel(props: { prob: number }) { return <span className="text-blue-400">{formatPercent(prob)}</span> } +export function NumericValueLabel(props: { value: number }) { + const { value } = props + return <span className="text-blue-400">{formatLargeNumber(value)}</span> +} + export function AnswerNumberLabel(props: { number: string }) { return <span className="text-primary">#{props.number}</span> } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 39cc2017..aeeb9af0 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -21,7 +21,9 @@ import { BinaryOutcomeLabel, CancelLabel, MultiLabel, + NumericValueLabel, ProbPercentLabel, + PseudoNumericOutcomeLabel, } from 'web/components/outcome-label' import { NotificationGroup, @@ -828,6 +830,9 @@ function NotificationTextLabel(props: { ) if (sourceText === 'CANCEL') return <CancelLabel /> if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> + if (contract?.outcomeType === 'PSEUDO_NUMERIC') { + return <NumericValueLabel value={parseFloat(sourceText)} /> + } } } // Close date will be a number - it looks better without it From 83c5f9b323c4ef35e23833aa6bb63e1a3b643449 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 14:55:10 -0500 Subject: [PATCH 095/519] Fix unused var --- web/pages/notifications.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index aeeb9af0..dd43d64f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -23,7 +23,6 @@ import { MultiLabel, NumericValueLabel, ProbPercentLabel, - PseudoNumericOutcomeLabel, } from 'web/components/outcome-label' import { NotificationGroup, From eb9b14d6d54aec19e0ede56a49a72121a1383e68 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 10 Jul 2022 13:46:00 -0700 Subject: [PATCH 096/519] Migrate unsubscribe function to v2 (#637) * Migrate unsubscribe function to v2 * Move Stripe import because I forgot to do it before --- .../src/email-templates/market-close.html | 2 +- .../src/email-templates/market-resolved.html | 2 +- functions/src/emails.ts | 20 ++-- functions/src/index.ts | 4 +- functions/src/unsubscribe.ts | 109 +++++++++--------- 5 files changed, 69 insertions(+), 68 deletions(-) diff --git a/functions/src/email-templates/market-close.html b/functions/src/email-templates/market-close.html index 00e8b439..150987cc 100644 --- a/functions/src/email-templates/market-close.html +++ b/functions/src/email-templates/market-close.html @@ -613,7 +613,7 @@ >our Discord</a >! Or, <a - href="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolve" + href="https://unsubscribe-nggbo3neva-uc.a.run.app?id={{userId}}&type=market-resolve" style=" font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; diff --git a/functions/src/email-templates/market-resolved.html b/functions/src/email-templates/market-resolved.html index 42d4e2d8..f109d31e 100644 --- a/functions/src/email-templates/market-resolved.html +++ b/functions/src/email-templates/market-resolved.html @@ -635,7 +635,7 @@ >our Discord</a >! Or, <a - href="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolved" + href="https://unsubscribe-nggbo3neva-uc.a.run.app?id={{userId}}&type=market-resolved" style=" font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 40e8900c..e0e2da63 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,4 +1,4 @@ -import { DOMAIN, PROJECT_ID } from '../../common/envs/constants' +import { DOMAIN, ENV_CONFIG } from '../../common/envs/constants' import { Answer } from '../../common/answer' import { Bet } from '../../common/bet' import { getProbability } from '../../common/calculate' @@ -141,7 +141,8 @@ export const sendWelcomeEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const unsubscribeLink = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=${emailType}` + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -173,7 +174,8 @@ export const sendOneWeekBonusEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const unsubscribeLink = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=${emailType}` + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -205,7 +207,8 @@ export const sendThankYouEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const unsubscribeLink = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=${emailType}` + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -277,8 +280,9 @@ export const sendNewCommentEmail = async ( const { question, creatorUsername, slug } = contract const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}` - - const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment` + const emailType = 'market-comment' + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + const unsubscribeUrl = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator const { text } = comment @@ -359,7 +363,9 @@ export const sendNewAnswerEmail = async ( const { name, avatarUrl, text } = answer const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}` - const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer` + const emailType = 'market-answer' + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + const unsubscribeUrl = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` const subject = `New answer on ${question}` const from = `${name} <info@manifold.markets>` diff --git a/functions/src/index.ts b/functions/src/index.ts index 0d0de3ba..380e4f93 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,12 +3,10 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './stripe' export * from './create-user' export * from './on-create-bet' export * from './on-create-comment-on-contract' export * from './on-view' -export * from './unsubscribe' export * from './update-metrics' export * from './update-stats' export * from './backup-db' @@ -41,3 +39,5 @@ export * from './withdraw-liquidity' export * from './create-group' export * from './resolve-market' export * from './get-daily-bonuses' +export * from './unsubscribe' +export * from './stripe' diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index a41a7155..48dd29c0 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -1,71 +1,66 @@ -import * as functions from 'firebase-functions' +import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' import { getUser } from './utils' import { PrivateUser } from '../../common/user' -export const unsubscribe = functions - .runWith({ minInstances: 1 }) - .https.onRequest(async (req, res) => { - const id = req.query.id as string - let type = req.query.type as string - if (!id || !type) { - res.status(400).send('Empty id or type parameter.') - return - } +export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => { + const id = req.query.id as string + let type = req.query.type as string + if (!id || !type) { + res.status(400).send('Empty id or type parameter.') + return + } - if (type === 'market-resolved') type = 'market-resolve' + if (type === 'market-resolved') type = 'market-resolve' - if ( - ![ - 'market-resolve', - 'market-comment', - 'market-answer', - 'generic', - ].includes(type) - ) { - res.status(400).send('Invalid type parameter.') - return - } + if ( + !['market-resolve', 'market-comment', 'market-answer', 'generic'].includes( + type + ) + ) { + res.status(400).send('Invalid type parameter.') + return + } - const user = await getUser(id) + const user = await getUser(id) - if (!user) { - res.send('This user is not currently subscribed or does not exist.') - return - } + if (!user) { + res.send('This user is not currently subscribed or does not exist.') + return + } - const { name } = user + const { name } = user - const update: Partial<PrivateUser> = { - ...(type === 'market-resolve' && { - unsubscribedFromResolutionEmails: true, - }), - ...(type === 'market-comment' && { - unsubscribedFromCommentEmails: true, - }), - ...(type === 'market-answer' && { - unsubscribedFromAnswerEmails: true, - }), - ...(type === 'generic' && { - unsubscribedFromGenericEmails: true, - }), - } + const update: Partial<PrivateUser> = { + ...(type === 'market-resolve' && { + unsubscribedFromResolutionEmails: true, + }), + ...(type === 'market-comment' && { + unsubscribedFromCommentEmails: true, + }), + ...(type === 'market-answer' && { + unsubscribedFromAnswerEmails: true, + }), + ...(type === 'generic' && { + unsubscribedFromGenericEmails: true, + }), + } - await firestore.collection('private-users').doc(id).update(update) + await firestore.collection('private-users').doc(id).update(update) - if (type === 'market-resolve') - res.send( - `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` - ) - else if (type === 'market-comment') - res.send( - `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` - ) - else if (type === 'market-answer') - res.send( - `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` - ) - else res.send(`${name}, you have been unsubscribed.`) - }) + if (type === 'market-resolve') + res.send( + `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` + ) + else if (type === 'market-comment') + res.send( + `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` + ) + else if (type === 'market-answer') + res.send( + `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` + ) + else res.send(`${name}, you have been unsubscribed.`) +}) const firestore = admin.firestore() From 6462d4a2edfcafd6092aa7c16cc4e2416abf7980 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 10 Jul 2022 14:02:32 -0700 Subject: [PATCH 097/519] Migrate createUser function to v2 (#633) --- functions/src/create-user.ts | 119 +++++++++++++++++------------------ functions/src/index.ts | 2 +- web/lib/firebase/api-call.ts | 4 ++ web/lib/firebase/fn-call.ts | 22 ------- web/lib/firebase/users.ts | 29 ++++----- 5 files changed, 77 insertions(+), 99 deletions(-) delete mode 100644 web/lib/firebase/fn-call.ts diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 189976ed..e70371ca 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -1,6 +1,5 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' - +import { z } from 'zod' import { PrivateUser, STARTING_BALANCE, @@ -18,83 +17,79 @@ import { isWhitelisted } from '../../common/envs/constants' import { DEFAULT_CATEGORIES } from '../../common/categories' import { track } from './analytics' +import { APIError, newEndpoint, validate } from './api' -export const createUser = functions - .runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) - .https.onCall(async (data: { deviceToken?: string }, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + deviceToken: z.string().optional(), +}) - const preexistingUser = await getUser(userId) - if (preexistingUser) - return { - status: 'error', - message: 'User already created', - user: preexistingUser, - } +const opts = { secrets: ['MAILGUN_KEY'] } - const fbUser = await admin.auth().getUser(userId) +export const createuser = newEndpoint(opts, async (req, auth) => { + const { deviceToken } = validate(bodySchema, req.body) + const preexistingUser = await getUser(auth.uid) + if (preexistingUser) + throw new APIError(400, 'User already exists', { user: preexistingUser }) - const email = fbUser.email - if (!isWhitelisted(email)) { - return { status: 'error', message: `${email} is not whitelisted` } - } - const emailName = email?.replace(/@.*$/, '') + const fbUser = await admin.auth().getUser(auth.uid) - const rawName = fbUser.displayName || emailName || 'User' + randomString(4) - const name = cleanDisplayName(rawName) - let username = cleanUsername(name) + const email = fbUser.email + if (!isWhitelisted(email)) { + throw new APIError(400, `${email} is not whitelisted`) + } + const emailName = email?.replace(/@.*$/, '') - const sameNameUser = await getUserByUsername(username) - if (sameNameUser) { - username += randomString(4) - } + const rawName = fbUser.displayName || emailName || 'User' + randomString(4) + const name = cleanDisplayName(rawName) + let username = cleanUsername(name) - const avatarUrl = fbUser.photoURL + const sameNameUser = await getUserByUsername(username) + if (sameNameUser) { + username += randomString(4) + } - const { deviceToken } = data - const deviceUsedBefore = - !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) + const avatarUrl = fbUser.photoURL + const deviceUsedBefore = + !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) - const ipAddress = context.rawRequest.ip - const ipCount = ipAddress ? await numberUsersWithIp(ipAddress) : 0 + const ipCount = req.ip ? await numberUsersWithIp(req.ip) : 0 - const balance = - deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE + const balance = + deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE - const user: User = { - id: userId, - name, - username, - avatarUrl, - balance, - totalDeposits: balance, - createdTime: Date.now(), - profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, - creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, - followerCountCached: 0, - followedCategories: DEFAULT_CATEGORIES, - } + const user: User = { + id: auth.uid, + name, + username, + avatarUrl, + balance, + totalDeposits: balance, + createdTime: Date.now(), + profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, + creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, + followerCountCached: 0, + followedCategories: DEFAULT_CATEGORIES, + } - await firestore.collection('users').doc(userId).create(user) - console.log('created user', username, 'firebase id:', userId) + await firestore.collection('users').doc(auth.uid).create(user) + console.log('created user', username, 'firebase id:', auth.uid) - const privateUser: PrivateUser = { - id: userId, - username, - email, - initialIpAddress: ipAddress, - initialDeviceToken: deviceToken, - } + const privateUser: PrivateUser = { + id: auth.uid, + username, + email, + initialIpAddress: req.ip, + initialDeviceToken: deviceToken, + } - await firestore.collection('private-users').doc(userId).create(privateUser) + await firestore.collection('private-users').doc(auth.uid).create(privateUser) - await sendWelcomeEmail(user, privateUser) + await sendWelcomeEmail(user, privateUser) - await track(userId, 'create user', { username }, { ip: ipAddress }) + await track(auth.uid, 'create user', { username }, { ip: req.ip }) - return { status: 'success', user } - }) + return user +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 380e4f93..e5ae78ec 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './create-user' export * from './on-create-bet' export * from './on-create-comment-on-contract' export * from './on-view' @@ -27,6 +26,7 @@ export * from './on-create-txn' export * from './health' export * from './transact' export * from './change-user-info' +export * from './create-user' export * from './create-answer' export * from './place-bet' export * from './cancel-bet' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 94da9f09..fc1e78bd 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -58,6 +58,10 @@ export function transact(params: any) { return call(getFunctionUrl('transact'), 'POST', params) } +export function createUser(params: any) { + return call(getFunctionUrl('createuser'), 'POST', params) +} + export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts deleted file mode 100644 index 2f299aea..00000000 --- a/web/lib/firebase/fn-call.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { httpsCallable } from 'firebase/functions' -import { User } from 'common/user' -import { randomString } from 'common/util/random' -import './init' -import { functions } from './init' -import { safeLocalStorage } from '../util/local' - -export const cloudFunction = <RequestData, ResponseData>(name: string) => - httpsCallable<RequestData, ResponseData>(functions, name) - -export const createUser: () => Promise<User | null> = () => { - const local = safeLocalStorage() - let deviceToken = local?.getItem('device-token') - if (!deviceToken) { - deviceToken = randomString() - local?.setItem('device-token', deviceToken) - } - - return cloudFunction('createUser')({ deviceToken }) - .then((r) => (r.data as any)?.user || null) - .catch(() => null) -} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 7f007031..d2e1ee04 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -20,11 +20,10 @@ import { GoogleAuthProvider, signInWithPopup, } from 'firebase/auth' -import { throttle, zip } from 'lodash' - +import { zip } from 'lodash' import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' -import { createUser } from './fn-call' +import { createUser } from './api-call' import { coll, getValue, @@ -38,6 +37,7 @@ import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' +import { randomString } from 'common/util/random' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' dayjs.extend(utc) @@ -101,11 +101,6 @@ const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY' -// used to avoid weird race condition -let createUserPromise: Promise<User | null> | undefined = undefined - -const warmUpCreateUser = throttle(createUser, 5000 /* ms */) - export function writeReferralInfo( defaultReferrerUsername: string, contractId?: string, @@ -183,22 +178,29 @@ async function setCachedReferralInfoForUser(user: User | null) { local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } +// used to avoid weird race condition +let createUserPromise: Promise<User> | undefined = undefined + export function listenForLogin(onUser: (user: User | null) => void) { const local = safeLocalStorage() const cachedUser = local?.getItem(CACHED_USER_KEY) onUser(cachedUser && JSON.parse(cachedUser)) - if (!cachedUser) warmUpCreateUser() - return onAuthStateChanged(auth, async (fbUser) => { if (fbUser) { let user: User | null = await getUser(fbUser.uid) if (!user) { - if (!createUserPromise) { - createUserPromise = createUser() + if (createUserPromise == null) { + const local = safeLocalStorage() + let deviceToken = local?.getItem('device-token') + if (!deviceToken) { + deviceToken = randomString() + local?.setItem('device-token', deviceToken) + } + createUserPromise = createUser({ deviceToken }).then((r) => r as User) } - user = (await createUserPromise) || null + user = await createUserPromise } onUser(user) @@ -211,7 +213,6 @@ export function listenForLogin(onUser: (user: User | null) => void) { // User logged out; reset to null onUser(null) local?.removeItem(CACHED_USER_KEY) - createUserPromise = undefined } }) } From 4700ceb14c85c478cf94313a88a9f5b67e3816f7 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 10 Jul 2022 15:03:15 -0700 Subject: [PATCH 098/519] Refactor some backend-related stuff (#639) * web/lib/firebase/api-call -> common/api, web/lib/firebase/api * Reuse `APIError` type in server code * Reuse `getFunctionUrl` in server code --- common/api.ts | 22 ++++++++++++++ functions/src/api.ts | 16 ++-------- .../src/email-templates/market-close.html | 2 +- .../src/email-templates/market-resolved.html | 2 +- functions/src/emails.ts | 28 +++++++++++------- web/components/answers/answer-bet-panel.tsx | 2 +- .../answers/answer-resolve-panel.tsx | 2 +- .../answers/create-answer-panel.tsx | 2 +- web/components/bet-panel.tsx | 4 +-- web/components/bets-list.tsx | 2 +- web/components/contract/quick-bet.tsx | 4 +-- web/components/groups/create-group-button.tsx | 2 +- web/components/limit-bets.tsx | 2 +- web/components/liquidity-panel.tsx | 2 +- web/components/notifications-icon.tsx | 2 +- web/components/numeric-bet-panel.tsx | 6 ++-- web/components/numeric-resolution-panel.tsx | 2 +- web/components/resolution-panel.tsx | 2 +- web/components/tipper.tsx | 2 +- web/lib/api/proxy.ts | 2 +- web/lib/firebase/{api-call.ts => api.ts} | 29 ++----------------- web/lib/firebase/users.ts | 2 +- web/lib/service/stripe.ts | 2 +- web/pages/charity/[charitySlug].tsx | 2 +- web/pages/create.tsx | 2 +- web/pages/link/[slug].tsx | 2 +- web/pages/make-predictions.tsx | 2 +- web/pages/profile.tsx | 2 +- 28 files changed, 72 insertions(+), 79 deletions(-) create mode 100644 common/api.ts rename web/lib/firebase/{api-call.ts => api.ts} (71%) diff --git a/common/api.ts b/common/api.ts new file mode 100644 index 00000000..02dba409 --- /dev/null +++ b/common/api.ts @@ -0,0 +1,22 @@ +import { ENV_CONFIG } from 'common/envs/constants' + +export class APIError extends Error { + code: number + details?: unknown + constructor(code: number, message: string, details?: unknown) { + super(message) + this.code = code + this.name = 'APIError' + this.details = details + } +} + +export function getFunctionUrl(name: string) { + if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { + const { projectId, region } = ENV_CONFIG.firebaseConfig + return `http://localhost:5001/${projectId}/${region}/${name}` + } else { + const { cloudRunId, cloudRunRegion } = ENV_CONFIG + return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app` + } +} diff --git a/functions/src/api.ts b/functions/src/api.ts index 6ebffc24..8c01ea05 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -3,13 +3,14 @@ import { logger } from 'firebase-functions/v2' import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https' import { log } from './utils' import { z } from 'zod' - +import { APIError } from '../../common/api' import { PrivateUser } from '../../common/user' import { CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST, CORS_ORIGIN_VERCEL, } from '../../common/envs/constants' +export { APIError } from '../../common/api' type Output = Record<string, unknown> type AuthedUser = { @@ -21,17 +22,6 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } type KeyCredentials = { kind: 'key'; data: string } type Credentials = JwtCredentials | KeyCredentials -export class APIError { - code: number - msg: string - details: unknown - constructor(code: number, msg: string, details?: unknown) { - this.code = code - this.msg = msg - this.details = details - } -} - const auth = admin.auth() const firestore = admin.firestore() const privateUsers = firestore.collection( @@ -136,7 +126,7 @@ export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { res.status(200).json(await fn(req, authedUser)) } catch (e) { if (e instanceof APIError) { - const output: { [k: string]: unknown } = { message: e.msg } + const output: { [k: string]: unknown } = { message: e.message } if (e.details != null) { output.details = e.details } diff --git a/functions/src/email-templates/market-close.html b/functions/src/email-templates/market-close.html index 150987cc..711f7ccb 100644 --- a/functions/src/email-templates/market-close.html +++ b/functions/src/email-templates/market-close.html @@ -613,7 +613,7 @@ >our Discord</a >! Or, <a - href="https://unsubscribe-nggbo3neva-uc.a.run.app?id={{userId}}&type=market-resolve" + href="{{unsubscribeUrl}}" style=" font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; diff --git a/functions/src/email-templates/market-resolved.html b/functions/src/email-templates/market-resolved.html index f109d31e..e8b090b5 100644 --- a/functions/src/email-templates/market-resolved.html +++ b/functions/src/email-templates/market-resolved.html @@ -635,7 +635,7 @@ >our Discord</a >! Or, <a - href="https://unsubscribe-nggbo3neva-uc.a.run.app?id={{userId}}&type=market-resolved" + href="{{unsubscribeUrl}}" style=" font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; diff --git a/functions/src/emails.ts b/functions/src/emails.ts index e0e2da63..60534679 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,4 +1,4 @@ -import { DOMAIN, ENV_CONFIG } from '../../common/envs/constants' +import { DOMAIN } from '../../common/envs/constants' import { Answer } from '../../common/answer' import { Bet } from '../../common/bet' import { getProbability } from '../../common/calculate' @@ -16,6 +16,9 @@ import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' +import { getFunctionUrl } from '../../common/api' + +const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe') export const sendMarketResolutionEmail = async ( userId: string, @@ -53,6 +56,9 @@ export const sendMarketResolutionEmail = async ( ? ` (plus ${formatMoney(creatorPayout)} in commissions)` : '' + const emailType = 'market-resolved' + const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` + const templateData: market_resolved_template = { userId: user.id, name: user.name, @@ -62,6 +68,7 @@ export const sendMarketResolutionEmail = async ( investment: `${Math.floor(investment)}`, payout: `${Math.floor(payout)}${creatorPayoutText}`, url: `https://${DOMAIN}/${creator.username}/${contract.slug}`, + unsubscribeUrl, } // Modify template here: @@ -85,6 +92,7 @@ type market_resolved_template = { investment: string payout: string url: string + unsubscribeUrl: string } const toDisplayResolution = ( @@ -141,8 +149,7 @@ export const sendWelcomeEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` + const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -174,8 +181,7 @@ export const sendOneWeekBonusEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` + const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -207,8 +213,7 @@ export const sendThankYouEmail = async ( const firstName = name.split(' ')[0] const emailType = 'generic' - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - const unsubscribeLink = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` + const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -242,6 +247,8 @@ export const sendMarketCloseEmail = async ( const { question, slug, volume, mechanism, collectedFees } = contract const url = `https://${DOMAIN}/${username}/${slug}` + const emailType = 'market-resolve' + const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` await sendTemplateEmail( privateUser.email, @@ -250,6 +257,7 @@ export const sendMarketCloseEmail = async ( { question, url, + unsubscribeUrl, userId, name: firstName, volume: formatMoney(volume), @@ -281,8 +289,7 @@ export const sendNewCommentEmail = async ( const { question, creatorUsername, slug } = contract const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}` const emailType = 'market-comment' - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - const unsubscribeUrl = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` + const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator const { text } = comment @@ -364,8 +371,7 @@ export const sendNewAnswerEmail = async ( const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}` const emailType = 'market-answer' - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - const unsubscribeUrl = `https://unsubscribe-${cloudRunId}-${cloudRunRegion}.a.run.app?id=${userId}&type=${emailType}` + const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` const subject = `New answer on ${question}` const from = `${name} <info@manifold.markets>` diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 705433b1..6499ce36 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -6,7 +6,7 @@ import { Answer } from 'common/answer' import { FreeResponseContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' -import { APIError, placeBet } from 'web/lib/firebase/api-call' +import { APIError, placeBet } from 'web/lib/firebase/api' import { Row } from '../layout/row' import { Spacer } from '../layout/spacer' import { diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 6b8e2885..5b59f050 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { Contract, FreeResponse } from 'common/contract' import { Col } from '../layout/col' -import { APIError, resolveMarket } from 'web/lib/firebase/api-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api' import { Row } from '../layout/row' import { ChooseCancelSelector } from '../yes-no-selector' import { ResolveConfirmationButton } from '../confirmation-button' diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 41745b09..ce266778 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -6,7 +6,7 @@ import { findBestMatch } from 'string-similarity' import { FreeResponseContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' -import { APIError, createAnswer } from 'web/lib/firebase/api-call' +import { APIError, createAnswer } from 'web/lib/firebase/api' import { Row } from '../layout/row' import { formatMoney, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 271eeecc..558697ef 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -19,8 +19,8 @@ import { getBinaryCpmmBetInfo } from 'common/new-bet' import { Title } from './title' import { User } from 'web/lib/firebase/users' import { Bet, LimitBet } from 'common/bet' -import { APIError, placeBet } from 'web/lib/firebase/api-call' -import { sellShares } from 'web/lib/firebase/api-call' +import { APIError, placeBet } from 'web/lib/firebase/api' +import { sellShares } from 'web/lib/firebase/api' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { BinaryOutcomeLabel } from './outcome-label' diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 72ac23db..40eaf04b 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -23,7 +23,7 @@ import { } from 'web/lib/firebase/contracts' import { Row } from './layout/row' import { UserLink } from './user-page' -import { sellBet } from 'web/lib/firebase/api-call' +import { sellBet } from 'web/lib/firebase/api' import { ConfirmationButton } from './confirmation-button' import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' import { filterDefined } from 'common/util/array' diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 0ce1c3f5..09a5d4bc 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -22,14 +22,14 @@ import { import { useState } from 'react' import toast from 'react-hot-toast' import { useUserContractBets } from 'web/hooks/use-user-bets' -import { placeBet } from 'web/lib/firebase/api-call' +import { placeBet } from 'web/lib/firebase/api' import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts' import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon' import { Col } from '../layout/col' import { OUTCOME_TO_COLOR } from '../outcome-label' import { useSaveBinaryShares } from '../use-save-binary-shares' -import { sellShares } from 'web/lib/firebase/api-call' +import { sellShares } from 'web/lib/firebase/api' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { track } from 'web/lib/service/analytics' import { formatNumericProbability } from 'common/pseudo-numeric' diff --git a/web/components/groups/create-group-button.tsx b/web/components/groups/create-group-button.tsx index b6b11292..0685d8e4 100644 --- a/web/components/groups/create-group-button.tsx +++ b/web/components/groups/create-group-button.tsx @@ -9,7 +9,7 @@ import { Title } from '../title' import { FilterSelectUsers } from 'web/components/filter-select-users' import { User } from 'common/user' import { MAX_GROUP_NAME_LENGTH } from 'common/group' -import { createGroup } from 'web/lib/firebase/api-call' +import { createGroup } from 'web/lib/firebase/api' export function CreateGroupButton(props: { user: User diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 5a6a67c0..82ae627d 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -5,7 +5,7 @@ import { getFormattedMappedValue } from 'common/pseudo-numeric' import { formatMoney, formatPercent } from 'common/util/format' import { sortBy } from 'lodash' import { useState } from 'react' -import { cancelBet } from 'web/lib/firebase/api-call' +import { cancelBet } from 'web/lib/firebase/api' import { Col } from './layout/col' import { LoadingIndicator } from './loading-indicator' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' diff --git a/web/components/liquidity-panel.tsx b/web/components/liquidity-panel.tsx index d1e066be..7ecadeb7 100644 --- a/web/components/liquidity-panel.tsx +++ b/web/components/liquidity-panel.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import { CPMMContract } from 'common/contract' import { formatMoney } from 'common/util/format' import { useUser } from 'web/hooks/use-user' -import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api-call' +import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api' import { AmountInput } from './amount-input' import { Row } from './layout/row' import { useUserLiquidity } from 'web/hooks/use-liquidity' diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index 2938fd17..478b4ad4 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -6,7 +6,7 @@ import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useRouter } from 'next/router' import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' -import { requestBonuses } from 'web/lib/firebase/api-call' +import { requestBonuses } from 'web/lib/firebase/api' import { PrivateUser } from 'common/user' export default function NotificationsIcon(props: { className?: string }) { diff --git a/web/components/numeric-bet-panel.tsx b/web/components/numeric-bet-panel.tsx index 9246bc89..e3b4bc29 100644 --- a/web/components/numeric-bet-panel.tsx +++ b/web/components/numeric-bet-panel.tsx @@ -10,9 +10,9 @@ import { import { NumericContract } from 'common/contract' import { formatPercent, formatMoney } from 'common/util/format' -import { useUser } from '../hooks/use-user' -import { APIError, placeBet } from '../lib/firebase/api-call' -import { User } from '../lib/firebase/users' +import { useUser } from 'web/hooks/use-user' +import { APIError, placeBet } from 'web/lib/firebase/api' +import { User } from 'web/lib/firebase/users' import { BuyAmountInput } from './amount-input' import { BucketInput } from './bucket-input' import { Col } from './layout/col' diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index 98a2aabc..371dd94b 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -7,7 +7,7 @@ import { NumberCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' import { NumericContract, PseudoNumericContract } from 'common/contract' -import { APIError, resolveMarket } from 'web/lib/firebase/api-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api' import { BucketInput } from './bucket-input' import { getPseudoProbability } from 'common/pseudo-numeric' diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index a46d9478..10dee789 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users' import { YesNoCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ResolveConfirmationButton } from './confirmation-button' -import { APIError, resolveMarket } from 'web/lib/firebase/api-call' +import { APIError, resolveMarket } from 'web/lib/firebase/api' import { ProbabilitySelector } from './probability-selector' import { DPM_CREATOR_FEE } from 'common/fees' import { getProbability } from 'common/calculate' diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index e4b6580f..68ca5308 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -11,7 +11,7 @@ import { debounce, sum } from 'lodash' import { useEffect, useRef, useState } from 'react' import { CommentTips } from 'web/hooks/use-tip-txns' import { useUser } from 'web/hooks/use-user' -import { transact } from 'web/lib/firebase/api-call' +import { transact } from 'web/lib/firebase/api' import { track } from 'web/lib/service/analytics' import { Row } from './layout/row' import { Tooltip } from './tooltip' diff --git a/web/lib/api/proxy.ts b/web/lib/api/proxy.ts index 294868ac..98ea161d 100644 --- a/web/lib/api/proxy.ts +++ b/web/lib/api/proxy.ts @@ -1,7 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import { promisify } from 'util' import { pipeline } from 'stream' -import { getFunctionUrl } from 'web/lib/firebase/api-call' +import { getFunctionUrl } from 'common/api' import fetch, { Headers, Response } from 'node-fetch' function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) { diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api.ts similarity index 71% rename from web/lib/firebase/api-call.ts rename to web/lib/firebase/api.ts index fc1e78bd..a6bd4359 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api.ts @@ -1,16 +1,6 @@ import { auth } from './users' -import { ENV_CONFIG } from 'common/envs/constants' - -export class APIError extends Error { - code: number - details?: string - constructor(code: number, message: string, details?: string) { - super(message) - this.code = code - this.name = 'APIError' - this.details = details - } -} +import { APIError, getFunctionUrl } from 'common/api' +export { APIError } from 'common/api' export async function call(url: string, method: string, params: any) { const user = auth.currentUser @@ -35,21 +25,6 @@ export async function call(url: string, method: string, params: any) { }) } -// Our users access the API through the Vercel proxy routes at /api/v0/blah, -// but right now at least until we get performance under control let's have the -// app just hit the cloud functions directly -- there's no difference and it's -// one less hop - -export function getFunctionUrl(name: string) { - if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { - const { projectId, region } = ENV_CONFIG.firebaseConfig - return `http://localhost:5001/${projectId}/${region}/${name}` - } else { - const { cloudRunId, cloudRunRegion } = ENV_CONFIG - return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app` - } -} - export function createAnswer(params: any) { return call(getFunctionUrl('createanswer'), 'POST', params) } diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index d2e1ee04..29cc9266 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -23,7 +23,7 @@ import { import { zip } from 'lodash' import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' -import { createUser } from './api-call' +import { createUser } from './api' import { coll, getValue, diff --git a/web/lib/service/stripe.ts b/web/lib/service/stripe.ts index bedd68aa..64d79487 100644 --- a/web/lib/service/stripe.ts +++ b/web/lib/service/stripe.ts @@ -1,4 +1,4 @@ -import { getFunctionUrl } from 'web/lib/firebase/api-call' +import { getFunctionUrl } from 'common/api' export const checkoutURL = ( userId: string, diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index c3e0912a..2cefa13b 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -10,7 +10,7 @@ import { Spacer } from 'web/components/layout/spacer' import { User } from 'common/user' import { useUser } from 'web/hooks/use-user' import { Linkify } from 'web/components/linkify' -import { transact } from 'web/lib/firebase/api-call' +import { transact } from 'web/lib/firebase/api' import { charities, Charity } from 'common/charity' import { useRouter } from 'next/router' import Custom404 from '../404' diff --git a/web/pages/create.tsx b/web/pages/create.tsx index f26d5687..f9b0dd00 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -6,7 +6,7 @@ import Textarea from 'react-expanding-textarea' import { Spacer } from 'web/components/layout/spacer' import { useUser } from 'web/hooks/use-user' import { Contract, contractPath } from 'web/lib/firebase/contracts' -import { createMarket } from 'web/lib/firebase/api-call' +import { createMarket } from 'web/lib/firebase/api' import { FIXED_ANTE } from 'common/antes' import { InfoTooltip } from 'web/components/info-tooltip' import { Page } from 'web/components/page' diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index b36a9057..01597a15 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' -import { claimManalink } from 'web/lib/firebase/api-call' +import { claimManalink } from 'web/lib/firebase/api' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index ce694278..b22fe371 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -16,7 +16,7 @@ import { Linkify } from 'web/components/linkify' import { Page } from 'web/components/page' import { Title } from 'web/components/title' import { useUser } from 'web/hooks/use-user' -import { createMarket } from 'web/lib/firebase/api-call' +import { createMarket } from 'web/lib/firebase/api' import { contractPath } from 'web/lib/firebase/contracts' type Prediction = { diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 62177825..b80698ae 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -9,7 +9,7 @@ import { Title } from 'web/components/title' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' -import { changeUserInfo } from 'web/lib/firebase/api-call' +import { changeUserInfo } from 'web/lib/firebase/api' import { uploadImage } from 'web/lib/firebase/storage' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' From 78ceac0659c0f1ef11068afa4fbf4a8963c5a06e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 18:22:21 -0500 Subject: [PATCH 099/519] =?UTF-8?q?Don't=20load=20user=20bets=20twice=20?= =?UTF-8?q?=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/components/bets-list.tsx | 10 ++++++---- web/components/user-page.tsx | 14 +++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 40eaf04b..c240d422 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -4,7 +4,6 @@ import dayjs from 'dayjs' import { useEffect, useState } from 'react' import clsx from 'clsx' -import { useUserBets } from 'web/hooks/use-user-bets' import { Bet } from 'web/lib/firebase/bets' import { User } from 'web/lib/firebase/users' import { @@ -51,13 +50,16 @@ import { floatingEqual } from 'common/util/math' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' -export function BetsList(props: { user: User; hideBetsBefore?: number }) { - const { user, hideBetsBefore } = props +export function BetsList(props: { + user: User + bets: Bet[] | undefined + hideBetsBefore?: number +}) { + const { user, bets: allBets, hideBetsBefore } = props const signedInUser = useUser() const isYourBets = user.id === signedInUser?.id - const allBets = useUserBets(user.id, { includeRedemptions: true }) // Hide bets before 06-01-2022 if this isn't your own profile // NOTE: This means public profits also begin on 06-01-2022 as well. const bets = allBets?.filter( diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index af03eb46..64eab05c 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -76,7 +76,12 @@ export function UserPage(props: { const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>( 'loading' ) - const [usersBets, setUsersBets] = useState<Bet[] | 'loading'>('loading') + const [userBets, setUserBets] = useState<Bet[] | undefined>() + const betCount = + userBets === undefined + ? 0 + : userBets.filter((bet) => !bet.isRedemption && bet.amount !== 0).length + const [portfolioHistory, setUsersPortfolioHistory] = useState< PortfolioMetrics[] >([]) @@ -95,7 +100,7 @@ export function UserPage(props: { if (!user) return getUsersComments(user.id).then(setUsersComments) listContracts(user.id).then(setUsersContracts) - getUserBets(user.id, { includeRedemptions: false }).then(setUsersBets) + getUserBets(user.id, { includeRedemptions: true }).then(setUserBets) getPortfolioHistory(user.id).then(setUsersPortfolioHistory) }, [user]) @@ -307,13 +312,12 @@ export function UserPage(props: { /> <BetsList user={user} + bets={userBets} hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022} /> </div> ), - tabIcon: ( - <div className="px-0.5 font-bold">{usersBets.length}</div> - ), + tabIcon: <div className="px-0.5 font-bold">{betCount}</div>, }, ]} /> From 5c6a143614e771b07d2b279490a07f84e29f2ed2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 18:26:06 -0500 Subject: [PATCH 100/519] Change portfolio graph option labels --- web/components/portfolio/portfolio-value-section.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 55260bb5..903b3f3d 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -40,8 +40,8 @@ export const PortfolioValueSection = memo( }} > <option value="allTime">All time</option> - <option value="weekly">Weekly</option> - <option value="daily">Daily</option> + <option value="weekly">7 days</option> + <option value="daily">24 hours</option> </select> </Row> <PortfolioValueGraph From 162e73912ee51146cf250883881b8e3ef73b79ca Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 18:41:33 -0500 Subject: [PATCH 101/519] Paginate bets list --- web/components/bets-list.tsx | 29 +++++++++++++++++------- web/components/pagination.tsx | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 web/components/pagination.tsx diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index c240d422..6d19c1e4 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' import { uniq, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' import dayjs from 'dayjs' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import clsx from 'clsx' import { Bet } from 'web/lib/firebase/bets' @@ -46,10 +46,13 @@ 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' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' +const CONTRACTS_PER_PAGE = 20 + export function BetsList(props: { user: User bets: Bet[] | undefined @@ -62,13 +65,17 @@ export function BetsList(props: { // Hide bets before 06-01-2022 if this isn't your own profile // NOTE: This means public profits also begin on 06-01-2022 as well. - const bets = allBets?.filter( - (bet) => bet.createdTime >= (hideBetsBefore ?? 0) + const bets = useMemo( + () => allBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)), + [allBets, hideBetsBefore] ) const [contracts, setContracts] = useState<Contract[] | undefined>() const [sort, setSort] = useState<BetSort>('newest') const [filter, setFilter] = useState<BetFilter>('open') + const [page, setPage] = useState(0) + const start = page * CONTRACTS_PER_PAGE + const end = start + CONTRACTS_PER_PAGE useEffect(() => { if (bets) { @@ -85,16 +92,14 @@ export function BetsList(props: { disposed = true } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allBets, hideBetsBefore]) + }, [bets]) const getTime = useTimeSinceFirstRender() useEffect(() => { if (bets && contracts) { trackLatency('portfolio', getTime()) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [!!bets, !!contracts]) + }, [bets, contracts, getTime]) if (!bets || !contracts) { return <LoadingIndicator /> @@ -130,7 +135,7 @@ export function BetsList(props: { (filter === 'open' ? -1 : 1) * (c.resolutionTime ?? c.closeTime ?? Infinity), } - const displayedContracts = sortBy(contracts, SORTS[sort]) + const filteredContracts = sortBy(contracts, SORTS[sort]) .reverse() .filter(FILTERS[filter]) .filter((c) => { @@ -141,6 +146,7 @@ export function BetsList(props: { if (filter === 'sold') return !hasShares return hasShares }) + const displayedContracts = filteredContracts.slice(start, end) const unsettled = contracts.filter( (c) => !c.isResolved && contractsMetrics[c.id].invested !== 0 @@ -227,6 +233,13 @@ export function BetsList(props: { )) )} </Col> + + <Pagination + page={page} + itemsPerPage={CONTRACTS_PER_PAGE} + totalItems={filteredContracts.length} + setPage={setPage} + /> </Col> ) } diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx new file mode 100644 index 00000000..f5c5eeab --- /dev/null +++ b/web/components/pagination.tsx @@ -0,0 +1,42 @@ +export function Pagination(props: { + page: number + itemsPerPage: number + totalItems: number + setPage: (page: number) => void +}) { + const { page, itemsPerPage, totalItems, setPage } = props + + return ( + <nav + className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" + aria-label="Pagination" + > + <div className="hidden sm:block"> + <p className="text-sm text-gray-700"> + Showing{' '} + <span className="font-medium"> + {page === 0 ? page + 1 : page * itemsPerPage} + </span>{' '} + to <span className="font-medium">{(page + 1) * itemsPerPage}</span> of{' '} + <span className="font-medium">{totalItems}</span> results + </p> + </div> + <div className="flex flex-1 justify-between sm:justify-end"> + <a + href="#" + className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page > 1 && setPage(page - 1)} + > + Previous + </a> + <a + href="#" + className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page < totalItems / itemsPerPage && setPage(page + 1)} + > + Next + </a> + </div> + </nav> + ) +} From f294189e203bfda46c0ef1ccbcb43417318cca54 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 18:50:59 -0500 Subject: [PATCH 102/519] Refactor notifications to use Pagination component --- web/components/pagination.tsx | 9 +++--- web/pages/notifications.tsx | 54 +++++++---------------------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx index f5c5eeab..968e49a8 100644 --- a/web/components/pagination.tsx +++ b/web/components/pagination.tsx @@ -3,8 +3,9 @@ export function Pagination(props: { itemsPerPage: number totalItems: number setPage: (page: number) => void + scrollToTop?: boolean }) { - const { page, itemsPerPage, totalItems, setPage } = props + const { page, itemsPerPage, totalItems, setPage, scrollToTop } = props return ( <nav @@ -23,14 +24,14 @@ export function Pagination(props: { </div> <div className="flex flex-1 justify-between sm:justify-end"> <a - href="#" + href={scrollToTop ? '#' : undefined} className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => page > 1 && setPage(page - 1)} + onClick={() => page > 0 && setPage(page - 1)} > Previous </a> <a - href="#" + href={scrollToTop ? '#' : undefined} className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => page < totalItems / itemsPerPage && setPage(page + 1)} > diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index dd43d64f..7a8f6d2f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -36,6 +36,7 @@ import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { groupBy, sum, uniq } from 'lodash' import Custom404 from 'web/pages/404' import { track } from '@amplitude/analytics-browser' +import { Pagination } from 'web/components/pagination' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -82,14 +83,14 @@ export default function Notifications() { function NotificationsList(props: { privateUser: PrivateUser }) { const { privateUser } = props - const [page, setPage] = useState(1) + const [page, setPage] = useState(0) const allGroupedNotifications = usePreferredGroupedNotifications(privateUser) const [paginatedGroupedNotifications, setPaginatedGroupedNotifications] = useState<NotificationGroup[] | undefined>(undefined) useEffect(() => { if (!allGroupedNotifications) return - const start = (page - 1) * NOTIFICATIONS_PER_PAGE + const start = page * NOTIFICATIONS_PER_PAGE const end = start + NOTIFICATIONS_PER_PAGE const maxNotificationsToShow = allGroupedNotifications.slice(start, end) const remainingNotification = allGroupedNotifications.slice(end) @@ -132,48 +133,13 @@ function NotificationsList(props: { privateUser: PrivateUser }) { )} {paginatedGroupedNotifications.length > 0 && allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( - <nav - className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" - aria-label="Pagination" - > - <div className="hidden sm:block"> - <p className="text-sm text-gray-700"> - Showing{' '} - <span className="font-medium"> - {page === 1 ? page : (page - 1) * NOTIFICATIONS_PER_PAGE} - </span>{' '} - to{' '} - <span className="font-medium"> - {page * NOTIFICATIONS_PER_PAGE} - </span>{' '} - of{' '} - <span className="font-medium"> - {allGroupedNotifications.length} - </span>{' '} - results - </p> - </div> - <div className="flex flex-1 justify-between sm:justify-end"> - <a - href="#" - className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => page > 1 && setPage(page - 1)} - > - Previous - </a> - <a - href="#" - className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => - page < - allGroupedNotifications?.length / NOTIFICATIONS_PER_PAGE && - setPage(page + 1) - } - > - Next - </a> - </div> - </nav> + <Pagination + page={page} + itemsPerPage={NOTIFICATIONS_PER_PAGE} + totalItems={allGroupedNotifications.length} + setPage={setPage} + scrollToTop + /> )} </div> ) From 5e1ed17cdfaf8397183621468b32922fe5287f3a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 19:19:35 -0500 Subject: [PATCH 103/519] Load contracts at UserPage top level instead of in BetsList --- web/components/bets-list.tsx | 39 ++++++++------------------ web/components/comments-list.tsx | 15 ++++++++-- web/components/user-page.tsx | 47 ++++++++++++++++---------------- 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 6d19c1e4..17ba9fa3 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import { uniq, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' +import { groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' import dayjs from 'dayjs' import { useEffect, useMemo, useState } from 'react' import clsx from 'clsx' @@ -16,7 +16,6 @@ import { Col } from './layout/col' import { Spacer } from './layout/spacer' import { Contract, - getContractFromId, contractPath, getBinaryProbPercent, } from 'web/lib/firebase/contracts' @@ -25,7 +24,6 @@ import { UserLink } from './user-page' import { sellBet } from 'web/lib/firebase/api' import { ConfirmationButton } from './confirmation-button' import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' -import { filterDefined } from 'common/util/array' import { LoadingIndicator } from './loading-indicator' import { SiteLink } from './site-link' import { @@ -56,9 +54,10 @@ const CONTRACTS_PER_PAGE = 20 export function BetsList(props: { user: User bets: Bet[] | undefined + contractsById: { [id: string]: Contract } | undefined hideBetsBefore?: number }) { - const { user, bets: allBets, hideBetsBefore } = props + const { user, bets: allBets, contractsById, hideBetsBefore } = props const signedInUser = useUser() const isYourBets = user.id === signedInUser?.id @@ -69,7 +68,6 @@ export function BetsList(props: { () => allBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)), [allBets, hideBetsBefore] ) - const [contracts, setContracts] = useState<Contract[] | undefined>() const [sort, setSort] = useState<BetSort>('newest') const [filter, setFilter] = useState<BetFilter>('open') @@ -77,39 +75,26 @@ export function BetsList(props: { const start = page * CONTRACTS_PER_PAGE const end = start + CONTRACTS_PER_PAGE - useEffect(() => { - if (bets) { - const contractIds = uniq(bets.map((bet) => bet.contractId)) - - let disposed = false - Promise.all(contractIds.map((id) => getContractFromId(id))).then( - (contracts) => { - if (!disposed) setContracts(filterDefined(contracts)) - } - ) - - return () => { - disposed = true - } - } - }, [bets]) - const getTime = useTimeSinceFirstRender() useEffect(() => { - if (bets && contracts) { + if (bets && contractsById) { trackLatency('portfolio', getTime()) } - }, [bets, contracts, getTime]) + }, [bets, contractsById, getTime]) - if (!bets || !contracts) { + if (!bets || !contractsById) { return <LoadingIndicator /> } - if (bets.length === 0) return <NoBets user={user} /> + // Decending creation time. bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime) const contractBets = groupBy(bets, 'contractId') - const contractsById = Object.fromEntries(contracts.map((c) => [c.id, c])) + + // Keep only contracts that have bets. + const contracts = Object.values(contractsById).filter( + (c) => contractBets[c.id] + ) const contractsMetrics = mapValues(contractBets, (bets, contractId) => { const contract = contractsById[contractId] diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index bceb2d59..ab9ed523 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -9,16 +9,25 @@ import { UserLink } from './user-page' import { User } from 'common/user' import { Col } from './layout/col' import { Linkify } from './linkify' +import { groupBy } from 'lodash' export function UserCommentsList(props: { user: User - commentsByUniqueContracts: Map<Contract, Comment[]> + comments: Comment[] + contractsById: { [id: string]: Contract } }) { - const { commentsByUniqueContracts } = props + const { comments, contractsById } = props + const commentsByContract = groupBy(comments, 'contractId') + + const contractCommentPairs = Object.entries(commentsByContract) + .map( + ([contractId, comments]) => [contractsById[contractId], comments] as const + ) + .filter(([contract]) => contract) return ( <Col className={'bg-white'}> - {Array.from(commentsByUniqueContracts).map(([contract, comments]) => ( + {contractCommentPairs.map(([contract, comments]) => ( <div key={contract.id} className={'border-width-1 border-b p-5'}> <div className={'mb-2 text-sm text-indigo-700'}> <SiteLink href={contractPath(contract)}> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 64eab05c..3e455b03 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { uniq } from 'lodash' +import { Dictionary, keyBy, uniq } from 'lodash' import { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' @@ -39,6 +39,7 @@ import { PortfolioMetrics } from 'common/user' import { ReferralsButton } from 'web/components/referrals-button' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' +import { filterDefined } from 'common/util/array' export function UserLink(props: { name: string @@ -72,7 +73,7 @@ export function UserPage(props: { const router = useRouter() const isCurrentUser = user.id === currentUser?.id const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) - const [usersComments, setUsersComments] = useState<Comment[]>([] as Comment[]) + const [usersComments, setUsersComments] = useState<Comment[] | undefined>() const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>( 'loading' ) @@ -85,9 +86,9 @@ export function UserPage(props: { const [portfolioHistory, setUsersPortfolioHistory] = useState< PortfolioMetrics[] >([]) - const [commentsByContract, setCommentsByContract] = useState< - Map<Contract, Comment[]> | 'loading' - >('loading') + const [contractsById, setContractsById] = useState< + Dictionary<Contract> | undefined + >() const [showConfetti, setShowConfetti] = useState(false) const { width, height } = useWindowSize() @@ -106,25 +107,21 @@ export function UserPage(props: { // TODO: display comments on groups useEffect(() => { - const uniqueContractIds = uniq( - usersComments.map((comment) => comment.contractId) - ) - Promise.all( - uniqueContractIds.map( - (contractId) => contractId && getContractFromId(contractId) - ) - ).then((contracts) => { - const commentsByContract = new Map<Contract, Comment[]>() - contracts.forEach((contract) => { - if (!contract) return - commentsByContract.set( - contract, - usersComments.filter((comment) => comment.contractId === contract.id) + if (usersComments && userBets) { + const uniqueContractIds = uniq([ + ...usersComments.map((comment) => comment.contractId), + ...(userBets?.map((bet) => bet.contractId) ?? []), + ]) + Promise.all( + uniqueContractIds.map((contractId) => + contractId ? getContractFromId(contractId) : undefined ) + ).then((contracts) => { + const contractsById = keyBy(filterDefined(contracts), 'id') + setContractsById(contractsById) }) - setCommentsByContract(commentsByContract) - }) - }, [usersComments]) + } + }, [userBets, usersComments]) const yourFollows = useFollows(currentUser?.id) const isFollowing = yourFollows?.includes(user.id) @@ -265,7 +262,7 @@ export function UserPage(props: { <Spacer h={10} /> - {usersContracts !== 'loading' && commentsByContract != 'loading' ? ( + {usersContracts !== 'loading' && contractsById && usersComments ? ( <Tabs currentPageForAnalytics={'profile'} labelClassName={'pb-2 pt-1 '} @@ -296,7 +293,8 @@ export function UserPage(props: { content: ( <UserCommentsList user={user} - commentsByUniqueContracts={commentsByContract} + contractsById={contractsById} + comments={usersComments} /> ), tabIcon: ( @@ -314,6 +312,7 @@ export function UserPage(props: { user={user} bets={userBets} hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022} + contractsById={contractsById} /> </div> ), From 67edc7b6390182a670f9d663647cd07bbf41ba62 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 19:42:34 -0500 Subject: [PATCH 104/519] UserPage: Load user with getStatic props --- web/pages/[username]/index.tsx | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index c2f56d78..3c44a5cc 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -1,30 +1,45 @@ import { useRouter } from 'next/router' -import React, { useEffect, useState } from 'react' +import React from 'react' import { getUserByUsername, User } from 'web/lib/firebase/users' import { UserPage } from 'web/components/user-page' import { useUser } from 'web/hooks/use-user' import Custom404 from '../404' import { useTracking } from 'web/hooks/use-tracking' +import { fromPropz, usePropz } from 'web/hooks/use-propz' + +export const getStaticProps = fromPropz(getStaticPropz) +export async function getStaticPropz(props: { params: { username: string } }) { + const { username } = props.params + const user = await getUserByUsername(username) + + return { + props: { + user, + }, + + revalidate: 60, // regenerate after a minute + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: 'blocking' } +} + +export default function UserProfile(props: { user: User | null }) { + props = usePropz(props, getStaticPropz) ?? { user: undefined } + const { user } = props -export default function UserProfile() { const router = useRouter() - const [user, setUser] = useState<User | null | 'loading'>('loading') const { username, tab } = router.query as { username: string tab?: string | undefined } - useEffect(() => { - if (username) { - getUserByUsername(username).then(setUser) - } - }, [username]) - const currentUser = useUser() useTracking('view user profile', { username }) - if (user === 'loading') return <div /> + if (user === undefined) return <div /> return user ? ( <UserPage From fd7384a099034b4cfb4ea1c406867ced498a63d7 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 19:59:23 -0500 Subject: [PATCH 105/519] Hide referrals button on user page --- web/components/user-page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 3e455b03..aa6fd10d 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -36,7 +36,6 @@ import { FollowersButton, FollowingButton } from './following-button' import { useFollows } from 'web/hooks/use-follows' import { FollowButton } from './follow-button' import { PortfolioMetrics } from 'common/user' -import { ReferralsButton } from 'web/components/referrals-button' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' @@ -205,7 +204,7 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - <ReferralsButton user={user} currentUser={currentUser} /> + {/* <ReferralsButton user={user} currentUser={currentUser} /> */} <GroupsButton user={user} /> </Row> From 9586e81e95139d9142b6c7c3a5da396dcd1e4477 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 22:07:42 -0500 Subject: [PATCH 106/519] Show limit bets in bets table --- web/components/bet-panel.tsx | 7 +++---- web/components/bets-list.tsx | 20 +++++++++++++++++++- web/components/limit-bets.tsx | 26 ++++++++++++++------------ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 558697ef..17e41dff 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -124,10 +124,9 @@ export function SimpleBetPanel(props: { <Col className={className}> <Col className={clsx('rounded-b-md rounded-t-md bg-white px-8 py-6')}> <Row className="justify-between"> - <Title - className={clsx('!mt-0')} - text={isLimitOrder ? 'Limit bet' : 'Place a trade'} - /> + <div className="mb-6 text-2xl"> + {isLimitOrder ? <>Limit bet</> : <>Place your bet</>} + </div> <button className="btn btn-ghost btn-sm text-sm normal-case" diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 17ba9fa3..ffa536ca 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -45,6 +45,7 @@ import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBet } from 'common/bet' import { floatingEqual } from 'common/util/math' import { Pagination } from './pagination' +import { LimitBets } from './limit-bets' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' @@ -256,6 +257,9 @@ function ContractBets(props: { const { bets, contract, metric, isYourBets } = props const { resolution, outcomeType } = contract + const limitBets = bets.filter( + (bet) => bet.limitProb !== undefined + ) as LimitBet[] const resolutionValue = (contract as NumericContract).resolutionValue const [collapsed, setCollapsed] = useState(true) @@ -350,7 +354,21 @@ function ContractBets(props: { isYourBets={isYourBets} /> - <Spacer h={8} /> + <Spacer h={4} /> + + {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( + <> + <div className="bg-gray-50 px-4 py-2">Your limit bets</div> + <LimitBets + className="max-w-md px-2 py-0 sm:px-4" + contract={contract} + bets={limitBets} + hideLabel + /> + </> + )} + + <Spacer h={4} /> <ContractBetsTable contract={contract} diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 82ae627d..f25ce495 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -13,9 +13,10 @@ import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' export function LimitBets(props: { contract: CPMMBinaryContract | PseudoNumericContract bets: LimitBet[] + hideLabel?: boolean className?: string }) { - const { contract, bets, className } = props + const { contract, bets, hideLabel, className } = props const recentBets = sortBy( bets, (bet) => -1 * bet.limitProb, @@ -24,18 +25,19 @@ export function LimitBets(props: { return ( <Col - className={clsx(className, 'gap-2 overflow-hidden rounded bg-white py-3')} + className={clsx( + className, + 'gap-2 overflow-hidden rounded bg-white px-4 py-3' + )} > - <div className="px-6 py-3 text-2xl">Your limit bets</div> - <div className="px-4"> - <table className="table-compact table w-full rounded text-gray-500"> - <tbody> - {recentBets.map((bet) => ( - <LimitBet key={bet.id} bet={bet} contract={contract} /> - ))} - </tbody> - </table> - </div> + {!hideLabel && <div className="px-2 py-3 text-2xl">Your limit bets</div>} + <table className="table-compact table w-full rounded text-gray-500"> + <tbody> + {recentBets.map((bet) => ( + <LimitBet key={bet.id} bet={bet} contract={contract} /> + ))} + </tbody> + </table> </Col> ) } From 99fcfa6be7eebcfd54058928279eb5d02592d40a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 22:15:07 -0500 Subject: [PATCH 107/519] Add portfolio filter for limit bets. --- web/components/bet-panel.tsx | 1 - web/components/bets-list.tsx | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 17e41dff..4fa4774a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -16,7 +16,6 @@ import { formatWithCommas, } from 'common/util/format' import { getBinaryCpmmBetInfo } from 'common/new-bet' -import { Title } from './title' import { User } from 'web/lib/firebase/users' import { Bet, LimitBet } from 'common/bet' import { APIError, placeBet } from 'web/lib/firebase/api' diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index ffa536ca..2a7da76e 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -48,7 +48,7 @@ import { Pagination } from './pagination' import { LimitBets } from './limit-bets' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' -type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' +type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' const CONTRACTS_PER_PAGE = 20 @@ -110,6 +110,7 @@ export function BetsList(props: { open: (c) => !(FILTERS.closed(c) || FILTERS.resolved(c)), all: () => true, sold: () => true, + limit_bet: (c) => FILTERS.open(c), } const SORTS: Record<BetSort, (c: Contract) => number> = { profit: (c) => contractsMetrics[c.id].profit, @@ -130,6 +131,8 @@ export function BetsList(props: { const { hasShares } = contractsMetrics[c.id] if (filter === 'sold') return !hasShares + if (filter === 'limit_bet') + return (contractBets[c.id] ?? []).some((b) => b.limitProb !== undefined) return hasShares }) const displayedContracts = filteredContracts.slice(start, end) @@ -185,6 +188,7 @@ export function BetsList(props: { onChange={(e) => setFilter(e.target.value as BetFilter)} > <option value="open">Open</option> + <option value="limit_bet">Limit bets</option> <option value="sold">Sold</option> <option value="closed">Closed</option> <option value="resolved">Resolved</option> From 89d48d6c34032b2a7d018ddec61be99f99b56e96 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 22:28:04 -0500 Subject: [PATCH 108/519] Use hook to fetch user bets --- web/components/user-page.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index aa6fd10d..be3f3ac4 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -30,8 +30,6 @@ import { Contract } from 'common/contract' import { getContractFromId, listContracts } from 'web/lib/firebase/contracts' import { LoadingIndicator } from './loading-indicator' import { BetsList } from './bets-list' -import { Bet } from 'common/bet' -import { getUserBets } from 'web/lib/firebase/bets' import { FollowersButton, FollowingButton } from './following-button' import { useFollows } from 'web/hooks/use-follows' import { FollowButton } from './follow-button' @@ -39,6 +37,7 @@ import { PortfolioMetrics } from 'common/user' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' +import { useUserBets } from 'web/hooks/use-user-bets' export function UserLink(props: { name: string @@ -76,7 +75,7 @@ export function UserPage(props: { const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>( 'loading' ) - const [userBets, setUserBets] = useState<Bet[] | undefined>() + const userBets = useUserBets(user.id, { includeRedemptions: true }) const betCount = userBets === undefined ? 0 @@ -100,7 +99,6 @@ export function UserPage(props: { if (!user) return getUsersComments(user.id).then(setUsersComments) listContracts(user.id).then(setUsersContracts) - getUserBets(user.id, { includeRedemptions: true }).then(setUserBets) getPortfolioHistory(user.id).then(setUsersPortfolioHistory) }, [user]) From 098f20ccad23d81ffc8f90703bfa43a7112cc12d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 22:28:29 -0500 Subject: [PATCH 109/519] Fix limit bet filter to exclude cancelled and filled bets --- web/components/bets-list.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 2a7da76e..d5e64c46 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -132,7 +132,9 @@ export function BetsList(props: { if (filter === 'sold') return !hasShares if (filter === 'limit_bet') - return (contractBets[c.id] ?? []).some((b) => b.limitProb !== undefined) + return (contractBets[c.id] ?? []).some( + (b) => b.limitProb !== undefined && !b.isCancelled && !b.isFilled + ) return hasShares }) const displayedContracts = filteredContracts.slice(start, end) @@ -262,7 +264,7 @@ function ContractBets(props: { const { resolution, outcomeType } = contract const limitBets = bets.filter( - (bet) => bet.limitProb !== undefined + (bet) => bet.limitProb !== undefined && !bet.isCancelled && !bet.isFilled ) as LimitBet[] const resolutionValue = (contract as NumericContract).resolutionValue From 1e68267e8ef18448de79500d40fb627a345dde15 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 10 Jul 2022 23:09:46 -0500 Subject: [PATCH 110/519] Use relative import --- common/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/api.ts b/common/api.ts index 02dba409..b9376be5 100644 --- a/common/api.ts +++ b/common/api.ts @@ -1,4 +1,4 @@ -import { ENV_CONFIG } from 'common/envs/constants' +import { ENV_CONFIG } from './envs/constants' export class APIError extends Error { code: number From a2a08b90ffc44365e9b06d30f9c8782cb44889cc Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 11 Jul 2022 07:51:48 -0600 Subject: [PATCH 111/519] Show numeric resolution contract value --- web/pages/notifications.tsx | 50 +++++++++++-------------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 7a8f6d2f..362ed433 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -342,8 +342,6 @@ function IncomeNotificationItem(props: { <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> <div className={'mr-1 text-black'}> <NotificationTextLabel - contract={null} - defaultText={notification.sourceText ?? ''} className={'line-clamp-1'} notification={notification} justSummary={true} @@ -375,11 +373,7 @@ function IncomeNotificationItem(props: { <div className={'line-clamp-2 flex max-w-xl shrink '}> <div className={'inline'}> <span className={'mr-1'}> - <NotificationTextLabel - contract={null} - defaultText={notification.sourceText ?? ''} - notification={notification} - /> + <NotificationTextLabel notification={notification} /> </span> </div> <span> @@ -532,18 +526,6 @@ function NotificationItem(props: { sourceText, } = notification - const [defaultNotificationText, setDefaultNotificationText] = - useState<string>('') - - useEffect(() => { - if (sourceText) { - setDefaultNotificationText(sourceText) - } else if (reasonText) { - // Handle arbitrary notifications with reason text here. - setDefaultNotificationText(reasonText) - } - }, [reasonText, sourceText]) - const [highlighted] = useState(!notification.isSeen) useEffect(() => { @@ -569,8 +551,6 @@ function NotificationItem(props: { </span> <div className={'ml-1 text-black'}> <NotificationTextLabel - contract={null} - defaultText={defaultNotificationText} className={'line-clamp-1'} notification={notification} justSummary={true} @@ -648,11 +628,7 @@ function NotificationItem(props: { </div> </Row> <div className={'mt-1 ml-1 md:text-base'}> - <NotificationTextLabel - contract={null} - defaultText={defaultNotificationText} - notification={notification} - /> + <NotificationTextLabel notification={notification} /> </div> <div className={'mt-6 border-b border-gray-300'} /> @@ -770,18 +746,21 @@ function getSourceIdForLinkComponent( } function NotificationTextLabel(props: { - defaultText: string - contract?: Contract | null notification: Notification className?: string justSummary?: boolean }) { - const { contract, className, defaultText, notification, justSummary } = props - const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } = - notification + const { className, notification, justSummary } = props + const { + sourceUpdateType, + sourceType, + sourceText, + sourceContractTitle, + reasonText, + } = notification + const defaultText = sourceText ?? reasonText ?? '' if (sourceType === 'contract') { - if (justSummary) - return <span>{contract?.question || sourceContractTitle}</span> + if (justSummary) return <span>{sourceContractTitle}</span> if (!sourceText) return <div /> // Resolved contracts if (sourceType === 'contract' && sourceUpdateType === 'resolved') { @@ -795,9 +774,8 @@ function NotificationTextLabel(props: { ) if (sourceText === 'CANCEL') return <CancelLabel /> if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> - if (contract?.outcomeType === 'PSEUDO_NUMERIC') { - return <NumericValueLabel value={parseFloat(sourceText)} /> - } + // Numeric market + return <NumericValueLabel value={parseFloat(sourceText)} /> } } // Close date will be a number - it looks better without it From 86c256cbf7ba9cbc0c797b6496a0c6c1ccaa56c8 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 11 Jul 2022 08:01:26 -0600 Subject: [PATCH 112/519] Unused var --- web/pages/notifications.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 362ed433..8cdd2cb1 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -10,7 +10,6 @@ import { doc, updateDoc } from 'firebase/firestore' import { db } from 'web/lib/firebase/init' import { UserLink } from 'web/components/user-page' import { notification_subscribe_types, PrivateUser } from 'common/user' -import { Contract } from 'common/contract' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' import { LoadingIndicator } from 'web/components/loading-indicator' From 52d688885d65c9304210ccd9f8d6aa09f34e9f56 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 11 Jul 2022 08:11:52 -0600 Subject: [PATCH 113/519] Group income notifs by source title --- web/pages/notifications.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 8cdd2cb1..191747fe 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -179,15 +179,16 @@ function IncomeNotificationGroupItem(props: { (n) => n.sourceType ) for (const sourceType in groupedNotificationsBySourceType) { - const groupedNotificationsByContractId = groupBy( + // Source title splits by contracts and groups + const groupedNotificationsBySourceTitle = groupBy( groupedNotificationsBySourceType[sourceType], (notification) => { - return notification.sourceContractId + return notification.sourceTitle } ) - for (const contractId in groupedNotificationsByContractId) { + for (const contractId in groupedNotificationsBySourceTitle) { const notificationsForContractId = - groupedNotificationsByContractId[contractId] + groupedNotificationsBySourceTitle[contractId] if (notificationsForContractId.length === 1) { newNotifications.push(notificationsForContractId[0]) continue From dd6f5e5ef4f598b913781f3700803d030af2e272 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 10:49:33 -0500 Subject: [PATCH 114/519] Show better limit order stats in bets table --- web/components/bets-list.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index d5e64c46..8a461658 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -668,6 +668,14 @@ function BetRow(props: { ? 'N/A' : formatMoney(calculatePayout(contract, bet, bet.outcome)) + const hadPoolMatch = + bet.fills?.some((fill) => fill.matchedBetId === null) ?? false + + const ofTotalAmount = + bet.limitProb === undefined || bet.orderAmount === undefined + ? '' + : ` / ${formatMoney(bet.orderAmount)}` + return ( <tr> <td className="text-neutral"> @@ -694,13 +702,22 @@ function BetRow(props: { {isPseudoNumeric && ' than ' + formatNumericProbability(bet.probAfter, contract)} </td> - <td>{formatMoney(Math.abs(amount))}</td> + <td> + {formatMoney(Math.abs(amount))} + {ofTotalAmount} + </td> {!isCPMM && !isNumeric && <td>{saleDisplay}</td>} {!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>} <td>{formatWithCommas(Math.abs(shares))}</td> {!isPseudoNumeric && ( <td> - {formatPercent(probBefore)} → {formatPercent(probAfter)} + {outcomeType === 'FREE_RESPONSE' || hadPoolMatch ? ( + <> + {formatPercent(probBefore)} → {formatPercent(probAfter)} + </> + ) : ( + formatPercent(bet.limitProb ?? 0) + )} </td> )} <td>{dayjs(createdTime).format('MMM D, h:mma')}</td> From 9b252b93ab4ad63f2880d4890a59a3339e9d3ba2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 10:54:37 -0500 Subject: [PATCH 115/519] Fix fee calculation in bet panel tooltip --- web/components/bet-panel.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 4fa4774a..78f98390 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' -import { partition, sumBy } from 'lodash' +import { partition, sum, sumBy } from 'lodash' import { SwitchHorizontalIcon } from '@heroicons/react/solid' import { useUser } from 'web/hooks/use-user' @@ -26,11 +26,7 @@ import { BinaryOutcomeLabel } from './outcome-label' import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' -import { - calculateCpmmSale, - getCpmmProbability, - getCpmmFees, -} from 'common/calculate-cpmm' +import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { getFormattedMappedValue, getPseudoProbability, @@ -271,11 +267,7 @@ function BuyPanel(props: { const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturnPercent = formatPercent(currentReturn) - const cpmmFees = getCpmmFees( - contract, - betAmount ?? 0, - betChoice ?? 'YES' - ).totalFees + const totalFees = sum(Object.values(newBet.fees)) const format = getFormattedMappedValue(contract) @@ -362,7 +354,7 @@ function BuyPanel(props: { )} </div> <InfoTooltip - text={`Includes ${formatMoneyWithDecimals(cpmmFees)} in fees`} + text={`Includes ${formatMoneyWithDecimals(totalFees)} in fees`} /> </Row> <div> From 7b60cc63ce0b8e19e4600f50f3415b10ae5809f1 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 11 Jul 2022 09:56:10 -0600 Subject: [PATCH 116/519] Fix annoying create description scrolling on firefox --- web/pages/create.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index f9b0dd00..fd071310 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -101,7 +101,7 @@ export function NewContract(props: { (params?.outcomeType as outcomeType) ?? 'BINARY' ) const [initialProb] = useState(50) - + const [bottomRef, setBottomRef] = useState<HTMLDivElement | null>(null) const [minString, setMinString] = useState(params?.min ?? '') const [maxString, setMaxString] = useState(params?.max ?? '') const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale) @@ -185,7 +185,6 @@ export function NewContract(props: { if (!creator || !isValid) return setIsSubmitting(true) - // TODO: add contract id to the group contractIds try { const result = await createMarket( removeUndefinedProps({ @@ -410,8 +409,12 @@ export function NewContract(props: { value={description} disabled={isSubmitting} onClick={(e) => e.stopPropagation()} - onChange={(e) => setDescription(e.target.value || '')} + onChange={(e) => { + setDescription(e.target.value || '') + bottomRef?.scrollIntoView() + }} /> + <div ref={setBottomRef} /> </div> <Spacer h={6} /> From 61300e93a4101397251b1bea54878ae5f312a0be Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 11 Jul 2022 11:38:51 -0500 Subject: [PATCH 117/519] more validation for creating numeric markets --- functions/src/create-contract.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 0d78ab5c..9f14ea7a 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -46,7 +46,8 @@ const binarySchema = z.object({ initialProb: z.number().min(1).max(99), }) -const finite = () => z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER) +const finite = () => + z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER) const numericSchema = z.object({ min: finite(), @@ -67,10 +68,13 @@ export const createmarket = newEndpoint({}, async (req, auth) => { numericSchema, req.body )) - if (max - min <= 0.01 || initialValue < min || initialValue > max) + if (max - min <= 0.01 || initialValue <= min || initialValue >= max) throw new APIError(400, 'Invalid range.') initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 + + if (initialProb < 1 || initialProb > 99) + throw new APIError(400, 'Invalid initial value.') } if (outcomeType === 'BINARY') { ;({ initialProb } = validate(binarySchema, req.body)) From 90a75985dd112747906cfc33ee7916cef824f394 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 11:46:07 -0500 Subject: [PATCH 118/519] In market bets tab, show limit orders' total order amount --- web/components/feed/feed-bets.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 2ffdae8e..83656f8e 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -78,11 +78,19 @@ export function BetStatusText(props: { const { bet, contract, bettor, isSelf, hideOutcome } = props const { outcomeType } = contract const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const isFreeResponse = outcomeType === 'FREE_RESPONSE' const { amount, outcome, createdTime } = bet const bought = amount >= 0 ? 'bought' : 'sold' + const outOfTotalAmount = + bet.limitProb !== undefined && bet.orderAmount !== undefined + ? ` / ${formatMoney(bet.orderAmount)}` + : '' const money = formatMoney(Math.abs(amount)) + const hadPoolMatch = + bet.fills?.some((fill) => fill.matchedBetId === null) ?? false + return ( <div className="text-sm text-gray-500"> {bettor ? ( @@ -91,6 +99,7 @@ export function BetStatusText(props: { <span>{isSelf ? 'You' : 'A trader'}</span> )}{' '} {bought} {money} + {outOfTotalAmount} {!hideOutcome && ( <> {' '} @@ -103,7 +112,12 @@ export function BetStatusText(props: { />{' '} {isPseudoNumeric ? ' than ' + formatNumericProbability(bet.probAfter, contract) - : ' at ' + formatPercent(bet.probAfter)} + : ' at ' + + formatPercent( + hadPoolMatch || isFreeResponse + ? bet.probAfter + : bet.limitProb ?? bet.probAfter + )} </> )} <RelativeTimestamp time={createdTime} /> From ed9a2c0d355d2ce261ba737197f43408f2e303f8 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 11 Jul 2022 14:52:16 -0600 Subject: [PATCH 119/519] Set min height for group chat --- web/components/groups/group-chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 6e82b05c..4a7e9e90 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -105,7 +105,7 @@ export function GroupChat(props: { <Col className={'mt-2 flex-1'}> <Col className={ - 'max-h-[65vh] w-full space-y-2 overflow-x-hidden overflow-y-scroll' + 'max-h-[65vh] min-h-[65vh] w-full space-y-2 overflow-x-hidden overflow-y-scroll' } ref={setScrollToBottomRef} > From 24fac1fc0b65c58b134230a2fcfe0c608c84c710 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 15:53:11 -0500 Subject: [PATCH 120/519] Fix erronous 0 prob shown in table --- web/components/bets-list.tsx | 4 +++- web/components/feed/feed-bets.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 8a461658..158d14de 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -669,7 +669,9 @@ function BetRow(props: { : formatMoney(calculatePayout(contract, bet, bet.outcome)) const hadPoolMatch = - bet.fills?.some((fill) => fill.matchedBetId === null) ?? false + (bet.limitProb === undefined || + bet.fills?.some((fill) => fill.matchedBetId === null)) ?? + false const ofTotalAmount = bet.limitProb === undefined || bet.orderAmount === undefined diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 83656f8e..1520e57c 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -89,7 +89,9 @@ export function BetStatusText(props: { const money = formatMoney(Math.abs(amount)) const hadPoolMatch = - bet.fills?.some((fill) => fill.matchedBetId === null) ?? false + (bet.limitProb === undefined || + bet.fills?.some((fill) => fill.matchedBetId === null)) ?? + false return ( <div className="text-sm text-gray-500"> From b8d7c2ee17966bcc94b4ee15db2e960b31a6f8f1 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 18:40:25 -0500 Subject: [PATCH 121/519] Size group chat window & nav bar list of groups precisely. Update Page margin/padding. --- web/components/groups/group-chat.tsx | 16 +++++++++++++--- web/components/nav/nav-bar.tsx | 4 ++-- web/components/nav/sidebar.tsx | 16 +++++++++++++--- web/components/page.tsx | 16 +++++++++++----- web/pages/group/[...slugs]/index.tsx | 6 +++--- web/pages/notifications.tsx | 2 +- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 4a7e9e90..c98f1af1 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -22,6 +22,7 @@ import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' +import { useWindowSize } from 'web/hooks/use-window-size' export function GroupChat(props: { messages: Comment[] @@ -101,11 +102,20 @@ export function GroupChat(props: { inputRef?.focus() } + const { width, height } = useWindowSize() + const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) + // Subtract bottom bar when it's showing (less than lg screen) + const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 + const remainingHeight = + (height ?? window.innerHeight) - + (containerRef?.offsetTop ?? 0) - + bottomBarHeight + return ( - <Col className={'mt-2 flex-1'}> + <Col ref={setContainerRef} style={{ height: remainingHeight }}> <Col className={ - 'max-h-[65vh] min-h-[65vh] w-full space-y-2 overflow-x-hidden overflow-y-scroll' + 'w-full flex-1 space-y-2 overflow-x-hidden overflow-y-scroll pt-2' } ref={setScrollToBottomRef} > @@ -138,7 +148,7 @@ export function GroupChat(props: { )} </Col> {user && group.memberIds.includes(user.id) && ( - <div className=" flex w-full justify-start gap-2 p-2"> + <div className="flex w-full justify-start gap-2 p-2"> <div className="mt-1"> <Avatar username={user?.username} diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 2b065f1c..971aa89a 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -170,7 +170,7 @@ export function MobileSidebar(props: { leaveFrom="translate-x-0" leaveTo="-translate-x-full" > - <div className="relative flex w-full max-w-xs flex-1 flex-col bg-white pt-5 pb-4"> + <div className="relative flex w-full max-w-xs flex-1 flex-col bg-white"> <Transition.Child as={Fragment} enter="ease-in-out duration-300" @@ -191,7 +191,7 @@ export function MobileSidebar(props: { </button> </div> </Transition.Child> - <div className="mx-2 mt-5 h-0 flex-1 overflow-y-auto"> + <div className="mx-2 h-0 flex-1 overflow-y-auto"> <Sidebar className="pl-2" /> </div> </div> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 6ab095ef..253a58be 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' import { ProfileSummary } from './profile-menu' import NotificationsIcon from 'web/components/notifications-icon' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' @@ -29,6 +29,7 @@ import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' import { PrivateUser } from 'common/user' +import { useWindowSize } from 'web/hooks/use-window-size' function getNavigation() { return [ @@ -199,7 +200,7 @@ export default function Sidebar(props: { className?: string }) { return ( <nav aria-label="Sidebar" className={className}> - <ManifoldLogo className="pb-6" twoLine /> + <ManifoldLogo className="py-6" twoLine /> <CreateQuestionButton user={user} /> <Spacer h={4} /> @@ -282,6 +283,11 @@ function GroupsList(props: { }) }, [currentPage, preferredNotifications]) + const { height } = useWindowSize() + const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) + const remainingHeight = + (height ?? window.innerHeight) - (containerRef?.offsetTop ?? 0) + return ( <> <SidebarItem @@ -289,7 +295,11 @@ function GroupsList(props: { currentPage={currentPage} /> - <div className="mt-1 space-y-0.5"> + <div + className="flex-1 space-y-0.5 overflow-y-scroll" + style={{ height: remainingHeight }} + ref={setContainerRef} + > {memberItems.map((item) => ( <a key={item.href} diff --git a/web/components/page.tsx b/web/components/page.tsx index 78a3d5e7..24c866b8 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -7,28 +7,34 @@ import { Toaster } from 'react-hot-toast' export function Page(props: { rightSidebar?: ReactNode suspend?: boolean + className?: string children?: ReactNode }) { - const { children, rightSidebar, suspend } = props + const { children, rightSidebar, suspend, className } = props + const bottomBarPadding = 'pb-[58px] lg:pb-0 ' return ( <> <div - className="mx-auto w-full pb-14 lg:grid lg:grid-cols-12 lg:gap-2 lg:pt-6 xl:max-w-7xl xl:gap-8" + className={clsx( + className, + bottomBarPadding, + 'mx-auto w-full lg:grid lg:grid-cols-12 lg:gap-x-2 xl:max-w-7xl xl:gap-x-8' + )} style={suspend ? visuallyHiddenStyle : undefined} > <Toaster /> - <Sidebar className="sticky top-4 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" /> + <Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" /> <main className={clsx( - 'lg:col-span-8', + 'pt-6 lg:col-span-8', rightSidebar ? 'xl:col-span-7' : 'xl:col-span-8' )} > {children} {/* If right sidebar is hidden, place its content at the bottom of the page. */} - <div className="mt-4 block xl:hidden">{rightSidebar}</div> + <div className="block xl:hidden">{rightSidebar}</div> </main> <aside className="hidden xl:col-span-3 xl:block"> <div className="sticky top-4 space-y-4">{rightSidebar}</div> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index dec25ab1..5882159d 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -215,14 +215,14 @@ export default function GroupPage(props: { </Col> ) return ( - <Page rightSidebar={rightSidebar}> + <Page rightSidebar={rightSidebar} className="!pb-0"> <SEO title={group.name} description={`Created by ${creator.name}. ${group.about}`} url={groupPath(group.slug)} /> - <Col className="px-3 lg:px-1"> + <Col className="px-3"> <Row className={'items-center justify-between gap-4'}> <div className={'sm:mb-1'}> <div @@ -251,7 +251,7 @@ export default function GroupPage(props: { <Tabs currentPageForAnalytics={groupPath(group.slug)} - className={'mb-0 sm:mb-2'} + className={'mx-3 mb-0'} defaultIndex={ page === 'rankings' ? 2 diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 191747fe..6e32eb88 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -47,7 +47,7 @@ export default function Notifications() { if (!user) return <Custom404 /> return ( <Page> - <div className={'p-2 sm:p-4'}> + <div className={'px-2 sm:px-4'}> <Title text={'Notifications'} className={'hidden md:block'} /> <div> <Tabs From 0882f1c0d669ea5373ad7c98fa6ff8ee14274f73 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 11 Jul 2022 19:07:37 -0500 Subject: [PATCH 122/519] Remove top Pagepadding on small screens --- web/components/page.tsx | 2 +- web/pages/notifications.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/page.tsx b/web/components/page.tsx index 24c866b8..e76a4dc2 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -27,7 +27,7 @@ export function Page(props: { <Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" /> <main className={clsx( - 'pt-6 lg:col-span-8', + 'lg:col-span-8 lg:pt-6', rightSidebar ? 'xl:col-span-7' : 'xl:col-span-8' )} > diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 6e32eb88..db6c382d 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -47,7 +47,7 @@ export default function Notifications() { if (!user) return <Custom404 /> return ( <Page> - <div className={'px-2 sm:px-4'}> + <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> <Title text={'Notifications'} className={'hidden md:block'} /> <div> <Tabs From 43b30e6d0465273f16a8ce0f209182e79fe2a3ab Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 12 Jul 2022 12:36:10 -0700 Subject: [PATCH 123/519] Don't "warm up" resolveMarket anymore (#638) --- web/components/numeric-resolution-panel.tsx | 7 +------ web/components/resolution-panel.tsx | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index 371dd94b..dce36ab9 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { Col } from './layout/col' import { User } from 'web/lib/firebase/users' @@ -16,11 +16,6 @@ export function NumericResolutionPanel(props: { contract: NumericContract | PseudoNumericContract className?: string }) { - useEffect(() => { - // warm up cloud function - resolveMarket({} as any).catch(() => {}) - }, []) - const { contract, className } = props const { min, max, outcomeType } = contract diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 10dee789..7bb9f2d4 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { Col } from './layout/col' import { User } from 'web/lib/firebase/users' @@ -18,11 +18,6 @@ export function ResolutionPanel(props: { contract: BinaryContract className?: string }) { - useEffect(() => { - // warm up cloud function - resolveMarket({} as any).catch(() => {}) - }, []) - const { contract, className } = props const earnedFees = From 5fd42df1edc474b19d66bb7a63dcefa39307694b Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 12 Jul 2022 12:36:31 -0700 Subject: [PATCH 124/519] Don't run share redemption after adding liquidity (#631) --- functions/src/add-liquidity.ts | 114 ++++++++++++++++----------------- 1 file changed, 54 insertions(+), 60 deletions(-) diff --git a/functions/src/add-liquidity.ts b/functions/src/add-liquidity.ts index 3ef453c2..6746486e 100644 --- a/functions/src/add-liquidity.ts +++ b/functions/src/add-liquidity.ts @@ -4,7 +4,6 @@ import { z } from 'zod' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { removeUndefinedProps } from '../../common/util/object' -import { redeemShares } from './redeem-shares' import { getNewLiquidityProvision } from '../../common/add-liquidity' import { APIError, newEndpoint, validate } from './api' @@ -19,78 +18,73 @@ export const addliquidity = newEndpoint({}, async (req, auth) => { if (!isFinite(amount)) throw new APIError(400, 'Invalid amount') // run as transaction to prevent race conditions - return await firestore - .runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${auth.uid}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) throw new APIError(400, 'User not found') - const user = userSnap.data() as User + return await firestore.runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${auth.uid}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) throw new APIError(400, 'User not found') + const user = userSnap.data() as User - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) throw new APIError(400, 'Invalid contract') - const contract = contractSnap.data() as Contract - if ( - contract.mechanism !== 'cpmm-1' || - (contract.outcomeType !== 'BINARY' && - contract.outcomeType !== 'PSEUDO_NUMERIC') - ) - throw new APIError(400, 'Invalid contract') + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Invalid contract') + const contract = contractSnap.data() as Contract + if ( + contract.mechanism !== 'cpmm-1' || + (contract.outcomeType !== 'BINARY' && + contract.outcomeType !== 'PSEUDO_NUMERIC') + ) + throw new APIError(400, 'Invalid contract') - const { closeTime } = contract - if (closeTime && Date.now() > closeTime) - throw new APIError(400, 'Trading is closed') + const { closeTime } = contract + if (closeTime && Date.now() > closeTime) + throw new APIError(400, 'Trading is closed') - if (user.balance < amount) throw new APIError(400, 'Insufficient balance') + if (user.balance < amount) throw new APIError(400, 'Insufficient balance') - const newLiquidityProvisionDoc = firestore - .collection(`contracts/${contractId}/liquidity`) - .doc() + const newLiquidityProvisionDoc = firestore + .collection(`contracts/${contractId}/liquidity`) + .doc() - const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = - getNewLiquidityProvision( - user, - amount, - contract, - newLiquidityProvisionDoc.id - ) - - if (newP !== undefined && !isFinite(newP)) { - return { - status: 'error', - message: 'Liquidity injection rejected due to overflow error.', - } - } - - transaction.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - p: newP, - totalLiquidity: newTotalLiquidity, - }) + const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = + getNewLiquidityProvision( + user, + amount, + contract, + newLiquidityProvisionDoc.id ) - const newBalance = user.balance - amount - const newTotalDeposits = user.totalDeposits - amount - - if (!isFinite(newBalance)) { - throw new APIError(500, 'Invalid user balance for ' + user.username) + if (newP !== undefined && !isFinite(newP)) { + return { + status: 'error', + message: 'Liquidity injection rejected due to overflow error.', } + } - transaction.update(userDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + p: newP, + totalLiquidity: newTotalLiquidity, }) + ) - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + const newBalance = user.balance - amount + const newTotalDeposits = user.totalDeposits - amount - return newLiquidityProvision - }) - .then(async (result) => { - await redeemShares(auth.uid, contractId) - return result + if (!isFinite(newBalance)) { + throw new APIError(500, 'Invalid user balance for ' + user.username) + } + + transaction.update(userDoc, { + balance: newBalance, + totalDeposits: newTotalDeposits, }) + + transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + + return newLiquidityProvision + }) }) const firestore = admin.firestore() From 24896e44b42fab8c7b867a09ac8927b6177fa5df Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 12 Jul 2022 16:46:03 -0500 Subject: [PATCH 125/519] "limit bet" => "limit order" --- functions/src/cancel-bet.ts | 2 +- functions/src/place-bet.ts | 2 +- web/components/bet-panel.tsx | 8 +++++--- web/components/bets-list.tsx | 4 ++-- web/components/limit-bets.tsx | 2 +- web/pages/notifications.tsx | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/functions/src/cancel-bet.ts b/functions/src/cancel-bet.ts index 27e65ffb..d29a6cee 100644 --- a/functions/src/cancel-bet.ts +++ b/functions/src/cancel-bet.ts @@ -21,7 +21,7 @@ export const cancelbet = newEndpoint({}, async (req, auth) => { if (bet.userId !== auth.uid) throw new APIError(400, 'Not authorized to cancel bet.') if (bet.limitProb === undefined) - throw new APIError(400, 'Not a limit bet: Cannot cancel.') + throw new APIError(400, 'Not a limit order: Cannot cancel.') if (bet.isCancelled) throw new APIError(400, 'Bet already cancelled.') trans.update(betDoc.ref, { isCancelled: true }) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 52daf953..3c428f43 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -186,7 +186,7 @@ export const updateMakers = ( const totalAmount = sumBy(fills, 'amount') const isFilled = floatingEqual(totalAmount, bet.orderAmount) - log('Updated a matched limit bet.') + log('Updated a matched limit order.') trans.update(contractDoc.collection('bets').doc(bet.id), { fills, isFilled, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 78f98390..57218844 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -71,7 +71,7 @@ export function BetPanel(props: { > <Row className="align-center justify-between"> <div className="mb-6 text-2xl"> - {isLimitOrder ? <>Limit bet</> : <>Place your bet</>} + {isLimitOrder ? <>Limit order</> : <>Place your bet</>} </div> <button className="btn btn-ghost btn-sm text-sm normal-case" @@ -120,7 +120,7 @@ export function SimpleBetPanel(props: { <Col className={clsx('rounded-b-md rounded-t-md bg-white px-8 py-6')}> <Row className="justify-between"> <div className="mb-6 text-2xl"> - {isLimitOrder ? <>Limit bet</> : <>Place your bet</>} + {isLimitOrder ? <>Limit order</> : <>Place your bet</>} </div> <button @@ -385,7 +385,9 @@ function BuyPanel(props: { </button> )} - {wasSubmitted && <div className="mt-4">Bet submitted!</div>} + {wasSubmitted && ( + <div className="mt-4">{isLimitOrder ? 'Order' : 'Bet'} submitted!</div> + )} </> ) } diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 158d14de..a1164798 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -190,7 +190,7 @@ export function BetsList(props: { onChange={(e) => setFilter(e.target.value as BetFilter)} > <option value="open">Open</option> - <option value="limit_bet">Limit bets</option> + <option value="limit_bet">Limit orders</option> <option value="sold">Sold</option> <option value="closed">Closed</option> <option value="resolved">Resolved</option> @@ -364,7 +364,7 @@ function ContractBets(props: { {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( <> - <div className="bg-gray-50 px-4 py-2">Your limit bets</div> + <div className="bg-gray-50 px-4 py-2">Your limit orders</div> <LimitBets className="max-w-md px-2 py-0 sm:px-4" contract={contract} diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index f25ce495..22ac115e 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -30,7 +30,7 @@ export function LimitBets(props: { 'gap-2 overflow-hidden rounded bg-white px-4 py-3' )} > - {!hideLabel && <div className="px-2 py-3 text-2xl">Your limit bets</div>} + {!hideLabel && <div className="px-2 py-3 text-2xl">Your limit orders</div>} <table className="table-compact table w-full rounded text-gray-500"> <tbody> {recentBets.map((bet) => ( diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index db6c382d..10ae1ec8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -817,7 +817,7 @@ function NotificationTextLabel(props: { <span className="text-primary"> {formatMoney(parseInt(sourceText))} </span>{' '} - <span>of your limit bet was filled</span> + <span>of your limit order was filled</span> </> ) } From dd9fdc381fe51f1c4e84cbf2dd70e09aceb8257a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 12 Jul 2022 16:55:00 -0500 Subject: [PATCH 126/519] track limit orders --- web/components/bet-panel.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 57218844..6279fe2f 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -54,6 +54,10 @@ export function BetPanel(props: { const { sharesOutcome } = useSaveBinaryShares(contract, userBets) const [isLimitOrder, setIsLimitOrder] = useState(false) + const toggleLimitOrder = () => { + setIsLimitOrder(!isLimitOrder) + track('toggle limit order') + } return ( <Col className={className}> @@ -75,7 +79,7 @@ export function BetPanel(props: { </div> <button className="btn btn-ghost btn-sm text-sm normal-case" - onClick={() => setIsLimitOrder(!isLimitOrder)} + onClick={toggleLimitOrder} > <SwitchHorizontalIcon className="inline h-6 w-6" /> </button> @@ -242,6 +246,8 @@ function BuyPanel(props: { contractId: contract.id, amount: betAmount, outcome: betChoice, + isLimitOrder, + limitProb: limitProbScaled, }) } From 38aad405691875eb11d2c67b49d2b14ac41f4f21 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Tue, 12 Jul 2022 17:34:10 -0500 Subject: [PATCH 127/519] Simplify bet buttons (#644) * mono-button bet row * "bet yes" => "yes" * prettier --- web/components/bet-panel.tsx | 15 +++++++- web/components/bet-row.tsx | 61 ++++++++++++------------------ web/components/limit-bets.tsx | 4 +- web/components/yes-no-selector.tsx | 4 +- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 6279fe2f..91c6fe00 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -109,9 +109,10 @@ export function SimpleBetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract className?: string selected?: 'YES' | 'NO' + hasShares?: boolean onBetSuccess?: () => void }) { - const { contract, className, selected, onBetSuccess } = props + const { contract, className, selected, hasShares, onBetSuccess } = props const user = useUser() const [isLimitOrder, setIsLimitOrder] = useState(false) @@ -121,7 +122,17 @@ export function SimpleBetPanel(props: { return ( <Col className={className}> - <Col className={clsx('rounded-b-md rounded-t-md bg-white px-8 py-6')}> + <SellRow + contract={contract} + user={user} + className={'rounded-t-md bg-gray-100 px-4 py-5'} + /> + <Col + className={clsx( + !hasShares && 'rounded-t-md', + 'rounded-b-md bg-white px-8 py-6' + )} + > <Row className="justify-between"> <div className="mb-6 text-2xl"> {isLimitOrder ? <>Limit order</> : <>Place your bet</>} diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index 712d4a2c..f8624130 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -2,13 +2,12 @@ import { useState } from 'react' import clsx from 'clsx' import { SimpleBetPanel } from './bet-panel' -import { YesNoSelector } from './yes-no-selector' import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { Modal } from './layout/modal' -import { SellButton } from './sell-button' import { useUser } from 'web/hooks/use-user' import { useUserContractBets } from 'web/hooks/use-user-bets' import { useSaveBinaryShares } from './use-save-binary-shares' +import { Col } from './layout/col' // Inline version of a bet panel. Opens BetPanel in a new modal. export default function BetRow(props: { @@ -19,9 +18,7 @@ export default function BetRow(props: { }) { const { className, btnClassName, betPanelClassName, contract } = props const [open, setOpen] = useState(false) - const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>( - undefined - ) + const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) const { yesShares, noShares, hasYesShares, hasNoShares } = @@ -29,43 +26,33 @@ export default function BetRow(props: { return ( <> - <YesNoSelector - isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'} - className={clsx('justify-end', className)} - btnClassName={clsx('btn-sm w-24', btnClassName)} - onSelect={(choice) => { - setOpen(true) - setBetChoice(choice) - }} - replaceNoButton={ - hasYesShares ? ( - <SellButton - panelClassName={betPanelClassName} - contract={contract} - user={user} - sharesOutcome={'YES'} - shares={yesShares} - /> - ) : undefined - } - replaceYesButton={ - hasNoShares ? ( - <SellButton - panelClassName={betPanelClassName} - contract={contract} - user={user} - sharesOutcome={'NO'} - shares={noShares} - /> - ) : undefined - } - /> + <Col className={clsx('items-center', className)}> + <button + className={clsx( + 'btn btn-lg btn-outline my-auto inline-flex h-10 min-h-0 w-24', + btnClassName + )} + onClick={() => setOpen(true)} + > + Bet + </button> + + <div className={'mt-1 w-24 text-center text-sm text-gray-500'}> + {hasYesShares + ? `(${Math.floor(yesShares)} YES)` + : hasNoShares + ? `(${Math.floor(noShares)} NO)` + : ''} + </div> + </Col> + <Modal open={open} setOpen={setOpen}> <SimpleBetPanel className={betPanelClassName} contract={contract} - selected={betChoice} + selected={undefined} onBetSuccess={() => setOpen(false)} + hasShares={hasYesShares || hasNoShares} /> </Modal> </> diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 22ac115e..503b3d17 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -30,7 +30,9 @@ export function LimitBets(props: { 'gap-2 overflow-hidden rounded bg-white px-4 py-3' )} > - {!hideLabel && <div className="px-2 py-3 text-2xl">Your limit orders</div>} + {!hideLabel && ( + <div className="px-2 py-3 text-2xl">Your limit orders</div> + )} <table className="table-compact table w-full rounded text-gray-500"> <tbody> {recentBets.map((bet) => ( diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index cac7bf74..3b3cc21d 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -43,7 +43,7 @@ export function YesNoSelector(props: { )} onClick={() => onSelect('YES')} > - {isPseudoNumeric ? 'HIGHER' : 'Bet YES'} + {isPseudoNumeric ? 'HIGHER' : 'YES'} </button> )} {replaceNoButton ? ( @@ -60,7 +60,7 @@ export function YesNoSelector(props: { )} onClick={() => onSelect('NO')} > - {isPseudoNumeric ? 'LOWER' : 'Bet NO'} + {isPseudoNumeric ? 'LOWER' : 'NO'} </button> )} </Row> From 5c166b9dd52d87b69148bc1e375ca949043a1369 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 12 Jul 2022 17:47:28 -0500 Subject: [PATCH 128/519] bet row: 'higher' 'lower' labels --- web/components/bet-row.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index f8624130..974d9a63 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -24,6 +24,9 @@ export default function BetRow(props: { const { yesShares, noShares, hasYesShares, hasNoShares } = useSaveBinaryShares(contract, userBets) + const { outcomeType } = contract + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + return ( <> <Col className={clsx('items-center', className)}> @@ -39,9 +42,9 @@ export default function BetRow(props: { <div className={'mt-1 w-24 text-center text-sm text-gray-500'}> {hasYesShares - ? `(${Math.floor(yesShares)} YES)` + ? `(${Math.floor(yesShares)} ${isPseudoNumeric ? 'HIGHER' : 'YES'})` : hasNoShares - ? `(${Math.floor(noShares)} NO)` + ? `(${Math.floor(noShares)} ${isPseudoNumeric ? 'LOWER' : 'YES'})` : ''} </div> </Col> From 68343701caac30092897123fad5f07caa3b61853 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 12 Jul 2022 17:47:48 -0500 Subject: [PATCH 129/519] answer bet panel: scroll up on ios --- web/components/answers/answer-bet-panel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 6499ce36..8c1d0430 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -25,6 +25,7 @@ import { import { Bet } from 'common/bet' import { track } from 'web/lib/service/analytics' import { SignUpPrompt } from '../sign-up-prompt' +import { isIOS } from 'web/lib/util/device' export function AnswerBetPanel(props: { answer: Answer @@ -44,6 +45,7 @@ export function AnswerBetPanel(props: { const inputRef = useRef<HTMLElement>(null) useEffect(() => { + if (isIOS()) window.scrollTo(0, window.scrollY + 200) inputRef.current && inputRef.current.focus() }, []) From 10c510fc6bccda41a165583d9ee389ae68b9eed3 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 12 Jul 2022 18:27:22 -0700 Subject: [PATCH 130/519] Feature Wild Animal Initiative --- common/charity.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/common/charity.ts b/common/charity.ts index 0d8a0aa6..8c33cb17 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -300,10 +300,21 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Wild Animal Initiative', website: 'https://www.wildanimalinitiative.org/', ein: '82-2281466', + tags: ['Featured'] as CharityTag[], photo: 'https://i.imgur.com/bOVUnDm.png', - preview: 'We want to make life better for wild animals.', - description: - 'Wild Animal Initiative (WAI) currently operates in the U.S., where they work to strengthen the animal advocacy movement through creating an academic field dedicated to wild animal welfare. They compile literature reviews, write theoretical and opinion articles, and publish research results on their website and/or in peer-reviewed journals. WAI focuses on identifying and sharing possible research avenues and connecting with more established fields. They also work with researchers from various academic and non-academic institutions to identify potential collaborators, and they recently launched a grant assistance program.', + preview: + 'Our mission is to understand and improve the lives of wild animals.', + description: `Although the natural world is a source of great beauty and happiness, vast numbers of animals routinely face serious challenges such as disease, hunger, or natural disasters. There is no “one-size-fits-all” solution to these threats. However, even as we recognize that improving the welfare of free-ranging wild animals is difficult, we believe that humans have a responsibility to help whenever we can. + +Our staff explores how humans can beneficially coexist with animals through the lens of wild animal welfare. + +We respect wild animals as individuals with their own needs and preferences, rather than seeing them as mere parts of ecosystems. But this approach demands a richer understanding of wild animals’ lives. + +We want to take a proactive approach to managing the welfare benefits, threats, and uncertainties that are inherent to complex natural and urban environments. Yet, to take action safely, we must conduct research to understand the impacts of our actions. The transdisciplinary perspective of wild animal welfare draws upon ethics, ecology, and animal welfare science to gather the knowledge we need, facilitating evidence-based improvements to wild animals’ quality of life. + +Without sufficient public interest or research activity, solutions to the problems wild animals face will go undiscovered. + +Wild Animal Initiative currently focuses on helping scientists, grantors, and decision-makers investigate important and understudied questions about wild animal welfare. Our work catalyzes research and applied projects that will open the door to a clearer picture of wild animals’ needs and how to enhance their well-being. Ultimately, we envision a world in which people actively choose to help wild animals — and have the knowledge they need to do so responsibly.`, }, { name: 'New Incentives', From 1f2bdf40d0d94c19a4771fd8a2f159d7c0c9d33c Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 13 Jul 2022 00:07:12 -0500 Subject: [PATCH 131/519] bet row: fix labels --- web/components/bet-row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index 974d9a63..e538acbf 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -44,7 +44,7 @@ export default function BetRow(props: { {hasYesShares ? `(${Math.floor(yesShares)} ${isPseudoNumeric ? 'HIGHER' : 'YES'})` : hasNoShares - ? `(${Math.floor(noShares)} ${isPseudoNumeric ? 'LOWER' : 'YES'})` + ? `(${Math.floor(noShares)} ${isPseudoNumeric ? 'LOWER' : 'NO'})` : ''} </div> </Col> From 96a378f25fc8f367e361cdefe037d48e5a1c5017 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 07:41:58 -0600 Subject: [PATCH 132/519] Handle free response resolution --- web/pages/notifications.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 10ae1ec8..2001c557 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -774,8 +774,17 @@ function NotificationTextLabel(props: { ) if (sourceText === 'CANCEL') return <CancelLabel /> if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> + // Numeric market - return <NumericValueLabel value={parseFloat(sourceText)} /> + if (parseFloat(sourceText)) + return <NumericValueLabel value={parseFloat(sourceText)} /> + + // Free response market + return ( + <div className={className ? className : 'line-clamp-1 text-blue-400'}> + <Linkify text={sourceText} /> + </div> + ) } } // Close date will be a number - it looks better without it From 9e90f849a812f01a96a4a570cf8646db17e4630d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 07:57:51 -0600 Subject: [PATCH 133/519] Show group scrollbars only when needed --- web/components/nav/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 253a58be..430e98d2 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -296,7 +296,7 @@ function GroupsList(props: { /> <div - className="flex-1 space-y-0.5 overflow-y-scroll" + className="flex-1 space-y-0.5 overflow-auto" style={{ height: remainingHeight }} ref={setContainerRef} > From b3f4c2f0098c923673b5bbe10e0ba08a56821833 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 08:34:14 -0600 Subject: [PATCH 134/519] Disable enter to submit on mobile group chat --- web/components/feed/feed-comments.tsx | 14 +++++++++----- web/components/groups/group-chat.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index c327d8af..198e9c36 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -31,6 +31,7 @@ import { track } from 'web/lib/service/analytics' import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' +import useMediaQuery from 'react-query/types/devtools/useMediaQuery' export function FeedCommentThread(props: { contract: Contract @@ -472,7 +473,7 @@ export function CommentInputTextArea(props: { isSubmitting: boolean setRef?: (ref: HTMLTextAreaElement) => void presetId?: string - enterToSubmit?: boolean + enterToSubmitOnDesktop?: boolean }) { const { isReply, @@ -484,9 +485,9 @@ export function CommentInputTextArea(props: { presetId, isSubmitting, replyToUsername, - enterToSubmit, + enterToSubmitOnDesktop, } = props - + const isMobile = innerWidth < 768 const memoizedSetComment = useEvent(setComment) useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return @@ -507,7 +508,7 @@ export function CommentInputTextArea(props: { placeholder={ isReply ? 'Write a reply... ' - : enterToSubmit + : enterToSubmitOnDesktop ? 'Send a message' : 'Write a comment...' } @@ -516,7 +517,10 @@ export function CommentInputTextArea(props: { disabled={isSubmitting} onKeyDown={(e) => { if ( - (enterToSubmit && e.key === 'Enter' && !e.shiftKey) || + (enterToSubmitOnDesktop && + e.key === 'Enter' && + !e.shiftKey && + !isMobile) || (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) ) { e.preventDefault() diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index c98f1af1..2cf2d73d 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -165,7 +165,7 @@ export function GroupChat(props: { replyToUsername={replyToUsername} submitComment={submitMessage} isSubmitting={isSubmitting} - enterToSubmit={true} + enterToSubmitOnDesktop={true} setRef={setInputRef} /> </div> From e3f7f0efdad7320117ebd109b2c2630e2b7fecc8 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 08:44:27 -0600 Subject: [PATCH 135/519] Revert "Disable enter to submit on mobile group chat" This reverts commit b3f4c2f0098c923673b5bbe10e0ba08a56821833. --- web/components/feed/feed-comments.tsx | 14 +++++--------- web/components/groups/group-chat.tsx | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 198e9c36..c327d8af 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -31,7 +31,6 @@ import { track } from 'web/lib/service/analytics' import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' -import useMediaQuery from 'react-query/types/devtools/useMediaQuery' export function FeedCommentThread(props: { contract: Contract @@ -473,7 +472,7 @@ export function CommentInputTextArea(props: { isSubmitting: boolean setRef?: (ref: HTMLTextAreaElement) => void presetId?: string - enterToSubmitOnDesktop?: boolean + enterToSubmit?: boolean }) { const { isReply, @@ -485,9 +484,9 @@ export function CommentInputTextArea(props: { presetId, isSubmitting, replyToUsername, - enterToSubmitOnDesktop, + enterToSubmit, } = props - const isMobile = innerWidth < 768 + const memoizedSetComment = useEvent(setComment) useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return @@ -508,7 +507,7 @@ export function CommentInputTextArea(props: { placeholder={ isReply ? 'Write a reply... ' - : enterToSubmitOnDesktop + : enterToSubmit ? 'Send a message' : 'Write a comment...' } @@ -517,10 +516,7 @@ export function CommentInputTextArea(props: { disabled={isSubmitting} onKeyDown={(e) => { if ( - (enterToSubmitOnDesktop && - e.key === 'Enter' && - !e.shiftKey && - !isMobile) || + (enterToSubmit && e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) ) { e.preventDefault() diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2cf2d73d..c98f1af1 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -165,7 +165,7 @@ export function GroupChat(props: { replyToUsername={replyToUsername} submitComment={submitMessage} isSubmitting={isSubmitting} - enterToSubmitOnDesktop={true} + enterToSubmit={true} setRef={setInputRef} /> </div> From 490eabf977239e6728ab3f6982fbfef3f5676b8e Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 09:08:32 -0600 Subject: [PATCH 136/519] Revert "Revert "Disable enter to submit on mobile group chat"" This reverts commit e3f7f0efdad7320117ebd109b2c2630e2b7fecc8. --- web/components/feed/feed-comments.tsx | 14 +++++++++----- web/components/groups/group-chat.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index c327d8af..198e9c36 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -31,6 +31,7 @@ import { track } from 'web/lib/service/analytics' import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' +import useMediaQuery from 'react-query/types/devtools/useMediaQuery' export function FeedCommentThread(props: { contract: Contract @@ -472,7 +473,7 @@ export function CommentInputTextArea(props: { isSubmitting: boolean setRef?: (ref: HTMLTextAreaElement) => void presetId?: string - enterToSubmit?: boolean + enterToSubmitOnDesktop?: boolean }) { const { isReply, @@ -484,9 +485,9 @@ export function CommentInputTextArea(props: { presetId, isSubmitting, replyToUsername, - enterToSubmit, + enterToSubmitOnDesktop, } = props - + const isMobile = innerWidth < 768 const memoizedSetComment = useEvent(setComment) useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return @@ -507,7 +508,7 @@ export function CommentInputTextArea(props: { placeholder={ isReply ? 'Write a reply... ' - : enterToSubmit + : enterToSubmitOnDesktop ? 'Send a message' : 'Write a comment...' } @@ -516,7 +517,10 @@ export function CommentInputTextArea(props: { disabled={isSubmitting} onKeyDown={(e) => { if ( - (enterToSubmit && e.key === 'Enter' && !e.shiftKey) || + (enterToSubmitOnDesktop && + e.key === 'Enter' && + !e.shiftKey && + !isMobile) || (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) ) { e.preventDefault() diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index c98f1af1..2cf2d73d 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -165,7 +165,7 @@ export function GroupChat(props: { replyToUsername={replyToUsername} submitComment={submitMessage} isSubmitting={isSubmitting} - enterToSubmit={true} + enterToSubmitOnDesktop={true} setRef={setInputRef} /> </div> From cc1431da605eba1012059345eb3c24469a652662 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 09:12:43 -0600 Subject: [PATCH 137/519] Disable enter submit on mobile on group chat --- web/components/feed/feed-comments.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 198e9c36..a9f30ee5 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -32,6 +32,7 @@ import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import useMediaQuery from 'react-query/types/devtools/useMediaQuery' +import { useWindowSize } from 'web/hooks/use-window-size' export function FeedCommentThread(props: { contract: Contract @@ -487,7 +488,7 @@ export function CommentInputTextArea(props: { replyToUsername, enterToSubmitOnDesktop, } = props - const isMobile = innerWidth < 768 + const { width } = useWindowSize() const memoizedSetComment = useEvent(setComment) useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return @@ -520,7 +521,8 @@ export function CommentInputTextArea(props: { (enterToSubmitOnDesktop && e.key === 'Enter' && !e.shiftKey && - !isMobile) || + width && + width > 768) || (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) ) { e.preventDefault() From 18abad38b6f84d7ecf924664474801641149e54d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 09:13:34 -0600 Subject: [PATCH 138/519] Unused var --- web/components/feed/feed-comments.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index a9f30ee5..195c5343 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -31,7 +31,6 @@ import { track } from 'web/lib/service/analytics' import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' -import useMediaQuery from 'react-query/types/devtools/useMediaQuery' import { useWindowSize } from 'web/hooks/use-window-size' export function FeedCommentThread(props: { From 737d80390365f1ac392c37a0d0e561a7822ce208 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 13 Jul 2022 11:20:25 -0500 Subject: [PATCH 139/519] bet row: default to YES --- web/components/bet-row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index e538acbf..56fff9bd 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -53,7 +53,7 @@ export default function BetRow(props: { <SimpleBetPanel className={betPanelClassName} contract={contract} - selected={undefined} + selected="YES" onBetSuccess={() => setOpen(false)} hasShares={hasYesShares || hasNoShares} /> From f1eea665885075b54aaeb5a0d09b84898631da1a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 12:14:58 -0500 Subject: [PATCH 140/519] Show all limit orders in a tab --- web/components/bet-panel.tsx | 21 +++---- web/components/limit-bets.tsx | 105 ++++++++++++++++++++++++++-------- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 91c6fe00..c8356a06 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -59,6 +59,9 @@ export function BetPanel(props: { track('toggle limit order') } + const showLimitOrders = + (isLimitOrder && unfilledBets.length > 0) || yourUnfilledBets.length > 0 + return ( <Col className={className}> <SellRow @@ -94,12 +97,8 @@ export function BetPanel(props: { <SignUpPrompt /> </Col> - {yourUnfilledBets.length > 0 && ( - <LimitBets - className="mt-4" - contract={contract} - bets={yourUnfilledBets} - /> + {showLimitOrders && ( + <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> )} </Col> ) @@ -119,6 +118,8 @@ export function SimpleBetPanel(props: { const unfilledBets = useUnfilledBets(contract.id) ?? [] const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id) + const showLimitOrders = + (isLimitOrder && unfilledBets.length > 0) || yourUnfilledBets.length > 0 return ( <Col className={className}> @@ -158,12 +159,8 @@ export function SimpleBetPanel(props: { <SignUpPrompt /> </Col> - {yourUnfilledBets.length > 0 && ( - <LimitBets - className="mt-4" - contract={contract} - bets={yourUnfilledBets} - /> + {showLimitOrders && ( + <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> )} </Col> ) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 503b3d17..4f1f1893 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -5,8 +5,11 @@ import { getFormattedMappedValue } from 'common/pseudo-numeric' import { formatMoney, formatPercent } from 'common/util/format' import { sortBy } from 'lodash' import { useState } from 'react' +import { useUser, useUserById } from 'web/hooks/use-user' import { cancelBet } from 'web/lib/firebase/api' +import { Avatar } from './avatar' import { Col } from './layout/col' +import { Tabs } from './layout/tabs' import { LoadingIndicator } from './loading-indicator' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' @@ -16,12 +19,14 @@ export function LimitBets(props: { hideLabel?: boolean className?: string }) { - const { contract, bets, hideLabel, className } = props - const recentBets = sortBy( + const { contract, bets, className } = props + const sortedBets = sortBy( bets, (bet) => -1 * bet.limitProb, (bet) => -1 * bet.createdTime ) + const user = useUser() + const yourBets = sortedBets.filter((bet) => bet.userId === user?.id) return ( <Col @@ -30,25 +35,62 @@ export function LimitBets(props: { 'gap-2 overflow-hidden rounded bg-white px-4 py-3' )} > - {!hideLabel && ( - <div className="px-2 py-3 text-2xl">Your limit orders</div> - )} - <table className="table-compact table w-full rounded text-gray-500"> - <tbody> - {recentBets.map((bet) => ( - <LimitBet key={bet.id} bet={bet} contract={contract} /> - ))} - </tbody> - </table> + <Tabs + tabs={[ + ...(yourBets.length > 0 + ? [ + { + title: 'Your limit orders', + content: ( + <LimitOrderTable + limitBets={yourBets} + contract={contract} + isYou={true} + /> + ), + }, + ] + : []), + { + title: 'All limit orders', + content: ( + <LimitOrderTable + limitBets={sortedBets} + contract={contract} + isYou={false} + /> + ), + }, + ]} + /> </Col> ) } +function LimitOrderTable(props: { + limitBets: LimitBet[] + contract: CPMMBinaryContract | PseudoNumericContract + isYou: boolean +}) { + const { limitBets, contract, isYou } = props + + return ( + <table className="table-compact table w-full rounded text-gray-500"> + <tbody> + {limitBets.map((bet) => ( + <LimitBet key={bet.id} bet={bet} contract={contract} isYou={isYou} /> + ))} + </tbody> + </table> + ) +} + function LimitBet(props: { contract: CPMMBinaryContract | PseudoNumericContract bet: LimitBet + isYou: boolean }) { - const { contract, bet } = props + const { contract, bet, isYou } = props const { orderAmount, amount, limitProb, outcome } = bet const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' @@ -59,8 +101,19 @@ function LimitBet(props: { setIsCancelling(true) } + const user = useUserById(bet.userId) + return ( <tr> + {!isYou && ( + <td> + <Avatar + size={'sm'} + avatarUrl={user?.avatarUrl} + username={user?.username} + /> + </td> + )} <td> <div className="pl-2"> {isPseudoNumeric ? ( @@ -76,18 +129,20 @@ function LimitBet(props: { ? getFormattedMappedValue(contract)(limitProb) : formatPercent(limitProb)} </td> - <td> - {isCancelling ? ( - <LoadingIndicator /> - ) : ( - <button - className="btn btn-xs btn-outline my-auto normal-case" - onClick={onCancel} - > - Cancel - </button> - )} - </td> + {isYou && ( + <td> + {isCancelling ? ( + <LoadingIndicator /> + ) : ( + <button + className="btn btn-xs btn-outline my-auto normal-case" + onClick={onCancel} + > + Cancel + </button> + )} + </td> + )} </tr> ) } From 50eee33a6e13b1cca4e52985ace15c9c45572975 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 12:51:19 -0500 Subject: [PATCH 141/519] Redeem shares of makers after matching with limit bets --- functions/src/place-bet.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 3c428f43..73c60187 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -144,13 +144,20 @@ export const placebet = newEndpoint({}, async (req, auth) => { ) log('Updated contract properties.') - return { betId: betDoc.id } + return { betId: betDoc.id, makers } }) log('Main transaction finished.') await redeemShares(auth.uid, contractId) + + const userIds = [ + auth.uid, + ...(result.makers ?? []).map((maker) => maker.bet.userId), + ] + await Promise.all(userIds.map((userId) => redeemShares(userId, contractId))) log('Share redemption transaction finished.') - return result + + return { betId: result.betId } }) const firestore = admin.firestore() From 83d8f18bd7627cd09bd7c73301d6798f562d7d08 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 13 Jul 2022 11:56:59 -0500 Subject: [PATCH 142/519] fix bet summary selling --- web/components/bets-list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index a1164798..e57b0c38 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -417,8 +417,8 @@ export function BetsSummary(props: { const [showSellModal, setShowSellModal] = useState(false) const user = useUser() - const sharesOutcome = floatingEqual(totalShares.YES, 0) - ? floatingEqual(totalShares.NO, 0) + const sharesOutcome = floatingEqual(totalShares.YES ?? 0, 0) + ? floatingEqual(totalShares.NO ?? 0, 0) ? undefined : 'NO' : 'YES' @@ -498,7 +498,7 @@ export function BetsSummary(props: { {formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} /> {isYourBets && isCpmm && - isBinary && + (isBinary || isPseudoNumeric) && !isClosed && !resolution && hasShares && From 9a11f557624867bf3d593d68deb69f7daa820c4a Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 11:58:22 -0700 Subject: [PATCH 143/519] Rich content (#620) * Add TipTap editor and renderer components * Change market description editor to rich text * Type description as JSON, fix string-based logic - Delete make-predictions.tsx - Delete feed logic that showed descriptions * wip Fix API validation * fix type error * fix extension import (backend) In firebase, typescript compiles imports into common js imports like `const StarterKit = require("@tiptap/starter-kit")` Even though StarterKit is exported from the cjs file, it gets imported as undefined. But it magically works if we import * If you're reading this in the future, consider replacing StarterKit with the entire list of extensions. * Stop load on fail create market, improve warning * Refactor editor as hook / fix infinite submit bug Move state of editor back up to parent We have to do this later anyways to allow parent to edit * Add images - display, paste + uploading * add uploading state of image * Fix placeholder, misc styling min height, quote * Fix appending to description * code review fixes: rename, refactor, chop carets * Add hint & upload button on new lines - bump to Tailwind 3.1 for arbitrary variants * clean up, run prettier * rename FileButton to FileUploadButton * add image extension as functions dependency --- common/contract.ts | 3 +- common/new-contract.ts | 13 +- common/package.json | 2 + common/util/parse.ts | 47 ++ functions/package.json | 3 + functions/src/create-contract.ts | 29 +- functions/src/on-create-contract.ts | 4 +- .../contract/contract-description.tsx | 24 +- web/components/contract/contract-details.tsx | 15 +- web/components/editor.tsx | 152 +++++++ web/components/feed/activity-items.ts | 1 - web/components/feed/feed-items.tsx | 11 +- web/components/file-upload-button.tsx | 26 ++ web/lib/firebase/storage.ts | 1 + web/package.json | 7 +- web/pages/[username]/[contractSlug].tsx | 10 +- web/pages/api/v0/_types.ts | 3 +- web/pages/contract-search-firestore.tsx | 1 - web/pages/create.tsx | 52 +-- web/pages/group/[...slugs]/index.tsx | 1 - web/pages/make-predictions.tsx | 292 ------------- yarn.lock | 404 ++++++++++++++++-- 22 files changed, 700 insertions(+), 401 deletions(-) create mode 100644 web/components/editor.tsx create mode 100644 web/components/file-upload-button.tsx delete mode 100644 web/pages/make-predictions.tsx diff --git a/common/contract.ts b/common/contract.ts index 3a90d01f..52ca91d6 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -1,5 +1,6 @@ import { Answer } from './answer' import { Fees } from './fees' +import { JSONContent } from '@tiptap/core' export type AnyMechanism = DPM | CPMM export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric @@ -20,7 +21,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { creatorAvatarUrl?: string question: string - description: string // More info about what the contract is about + description: string | JSONContent // More info about what the contract is about tags: string[] lowercaseTags: string[] visibility: 'public' | 'unlisted' diff --git a/common/new-contract.ts b/common/new-contract.ts index 6c89c8c4..abfafaf8 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -10,8 +10,9 @@ import { PseudoNumeric, } from './contract' import { User } from './user' -import { parseTags } from './util/parse' +import { parseTags, richTextToString } from './util/parse' import { removeUndefinedProps } from './util/object' +import { JSONContent } from '@tiptap/core' export function getNewContract( id: string, @@ -19,7 +20,7 @@ export function getNewContract( creator: User, question: string, outcomeType: outcomeType, - description: string, + description: JSONContent, initialProb: number, ante: number, closeTime: number, @@ -32,7 +33,11 @@ export function getNewContract( isLogScale: boolean ) { const tags = parseTags( - `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` + [ + question, + richTextToString(description), + ...extraTags.map((tag) => `#${tag}`), + ].join(' ') ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) @@ -56,7 +61,7 @@ export function getNewContract( creatorAvatarUrl: creator.avatarUrl, question: question.trim(), - description: description.trim(), + description, tags, lowercaseTags, visibility: 'public', diff --git a/common/package.json b/common/package.json index c8115d84..25992cb6 100644 --- a/common/package.json +++ b/common/package.json @@ -8,6 +8,8 @@ }, "sideEffects": false, "dependencies": { + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/starter-kit": "2.0.0-beta.190", "lodash": "4.17.21" }, "devDependencies": { diff --git a/common/util/parse.ts b/common/util/parse.ts index b73bdfb3..48b68fdd 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -1,4 +1,24 @@ import { MAX_TAG_LENGTH } from '../contract' +import { generateText, JSONContent } from '@tiptap/core' +// Tiptap starter extensions +import { Blockquote } from '@tiptap/extension-blockquote' +import { Bold } from '@tiptap/extension-bold' +import { BulletList } from '@tiptap/extension-bullet-list' +import { Code } from '@tiptap/extension-code' +import { CodeBlock } from '@tiptap/extension-code-block' +import { Document } from '@tiptap/extension-document' +import { HardBreak } from '@tiptap/extension-hard-break' +import { Heading } from '@tiptap/extension-heading' +import { History } from '@tiptap/extension-history' +import { HorizontalRule } from '@tiptap/extension-horizontal-rule' +import { Italic } from '@tiptap/extension-italic' +import { ListItem } from '@tiptap/extension-list-item' +import { OrderedList } from '@tiptap/extension-ordered-list' +import { Paragraph } from '@tiptap/extension-paragraph' +import { Strike } from '@tiptap/extension-strike' +import { Text } from '@tiptap/extension-text' +// other tiptap extensions +import { Image } from '@tiptap/extension-image' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -27,3 +47,30 @@ export function parseWordsAsTags(text: string) { .join(' ') return parseTags(taggedText) } + +// can't just do [StarterKit, Image...] because it doesn't work with cjs imports +export const exhibitExts = [ + Blockquote, + Bold, + BulletList, + Code, + CodeBlock, + Document, + HardBreak, + Heading, + History, + HorizontalRule, + Italic, + ListItem, + OrderedList, + Paragraph, + Strike, + Text, + + Image, +] +// export const exhibitExts = [StarterKit as unknown as Extension, Image] + +export function richTextToString(text?: JSONContent) { + return !text ? '' : generateText(text, exhibitExts) +} diff --git a/functions/package.json b/functions/package.json index 4c9f4338..d7ebb663 100644 --- a/functions/package.json +++ b/functions/package.json @@ -24,6 +24,9 @@ "dependencies": { "@amplitude/node": "1.10.0", "@google-cloud/functions-framework": "3.1.2", + "@tiptap/core": "2.0.0-beta.181", + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/starter-kit": "2.0.0-beta.190", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 9f14ea7a..c8cfc7c4 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -5,7 +5,6 @@ import { CPMMBinaryContract, Contract, FreeResponseContract, - MAX_DESCRIPTION_LENGTH, MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, NumericContract, @@ -29,10 +28,34 @@ import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { User } from '../../common/user' import { Group, MAX_ID_LENGTH } from '../../common/group' import { getPseudoProbability } from '../../common/pseudo-numeric' +import { JSONContent } from '@tiptap/core' + +const descScehma: z.ZodType<JSONContent> = z.lazy(() => + z.intersection( + z.record(z.any()), + z.object({ + type: z.string().optional(), + attrs: z.record(z.any()).optional(), + content: z.array(descScehma).optional(), + marks: z + .array( + z.intersection( + z.record(z.any()), + z.object({ + type: z.string(), + attrs: z.record(z.any()).optional(), + }) + ) + ) + .optional(), + text: z.string().optional(), + }) + ) +) const bodySchema = z.object({ question: z.string().min(1).max(MAX_QUESTION_LENGTH), - description: z.string().max(MAX_DESCRIPTION_LENGTH), + description: descScehma.optional(), tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(), closeTime: zTimestamp().refine( (date) => date.getTime() > new Date().getTime(), @@ -131,7 +154,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { user, question, outcomeType, - description, + description ?? {}, initialProb ?? 0, ante, closeTime.getTime(), diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 20c7ceba..28682793 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -2,6 +2,8 @@ import * as functions from 'firebase-functions' import { getUser } from './utils' import { createNotification } from './create-notification' import { Contract } from '../../common/contract' +import { richTextToString } from '../../common/util/parse' +import { JSONContent } from '@tiptap/core' export const onCreateContract = functions.firestore .document('contracts/{contractId}') @@ -18,7 +20,7 @@ export const onCreateContract = functions.firestore 'created', contractCreator, eventId, - contract.description, + richTextToString(contract.description as JSONContent), contract ) }) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index d8b657cb..a427afe1 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -5,12 +5,13 @@ import Textarea from 'react-expanding-textarea' import { CATEGORY_LIST } from '../../../common/categories' import { Contract } from 'common/contract' -import { parseTags } from 'common/util/parse' +import { parseTags, exhibitExts } from 'common/util/parse' import { useAdmin } from 'web/hooks/use-admin' import { updateContract } from 'web/lib/firebase/contracts' import { Row } from '../layout/row' -import { Linkify } from '../linkify' import { TagsList } from '../tags-list' +import { Content } from '../editor' +import { Editor } from '@tiptap/react' export function ContractDescription(props: { contract: Contract @@ -21,22 +22,31 @@ export function ContractDescription(props: { const descriptionTimestamp = () => `${dayjs().format('MMM D, h:mma')}: ` const isAdmin = useAdmin() + const desc = contract.description ?? '' + // Append the new description (after a newline) async function saveDescription(newText: string) { - const newDescription = `${contract.description}\n\n${newText}`.trim() + const editor = new Editor({ content: desc, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('<br /><br />') + .insertContent(newText.trim()) + .run() + const tags = parseTags( - `${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` + `${editor.getText()} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) await updateContract(contract.id, { - description: newDescription, + description: editor.getJSON(), tags, lowercaseTags, }) } - if (!isCreator && !contract.description.trim()) return null + if (!isCreator) return null const { tags } = contract const categories = tags.filter((tag) => @@ -50,7 +60,7 @@ export function ContractDescription(props: { className )} > - <Linkify text={contract.description} /> + <Content content={desc} /> {categories.length > 0 && ( <div className="mt-4"> diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index f908918e..b4d67520 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -31,6 +31,8 @@ import { DAY_MS } from 'common/util/time' import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' +import { Editor } from '@tiptap/react' +import { exhibitExts } from 'common/util/parse' export type ShowTime = 'resolve-date' | 'close-date' @@ -268,13 +270,20 @@ function EditableCloseDate(props: { const newCloseTime = dayjs(closeDate).valueOf() if (newCloseTime === closeTime) setIsEditingCloseTime(false) else if (newCloseTime > Date.now()) { - const { description } = contract + const content = contract.description const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') - const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}` + + const editor = new Editor({ content, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('<br /><br />') + .insertContent(`Close date updated to ${formattedCloseDate}`) + .run() updateContract(contract.id, { closeTime: newCloseTime, - description: newDescription, + description: editor.getJSON(), }) setIsEditingCloseTime(false) diff --git a/web/components/editor.tsx b/web/components/editor.tsx new file mode 100644 index 00000000..bd4d97c0 --- /dev/null +++ b/web/components/editor.tsx @@ -0,0 +1,152 @@ +import CharacterCount from '@tiptap/extension-character-count' +import Placeholder from '@tiptap/extension-placeholder' +import { + useEditor, + EditorContent, + FloatingMenu, + JSONContent, + Content, + Editor, +} from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import { Image } from '@tiptap/extension-image' +import clsx from 'clsx' +import { useEffect } from 'react' +import { Linkify } from './linkify' +import { uploadImage } from 'web/lib/firebase/storage' +import { useMutation } from 'react-query' +import { exhibitExts } from 'common/util/parse' +import { FileUploadButton } from './file-upload-button' + +const proseClass = + 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' + +export function useTextEditor(props: { + placeholder?: string + max?: number + defaultValue?: Content + disabled?: boolean +}) { + const { placeholder, max, defaultValue = '', disabled } = props + + const editorClass = clsx( + proseClass, + 'box-content min-h-[6em] textarea textarea-bordered' + ) + + const editor = useEditor({ + editorProps: { attributes: { class: editorClass } }, + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + }), + Placeholder.configure({ + placeholder, + emptyEditorClass: + 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', + }), + CharacterCount.configure({ limit: max }), + Image, + ], + content: defaultValue, + }) + + const upload = useUploadMutation(editor) + + editor?.setOptions({ + editorProps: { + handlePaste(view, event) { + const imageFiles = Array.from(event.clipboardData?.files ?? []).filter( + (file) => file.type.startsWith('image') + ) + + if (!imageFiles.length) { + return // if no files pasted, use default paste handler + } + + event.preventDefault() + upload.mutate(imageFiles) + }, + }, + }) + + useEffect(() => { + editor?.setEditable(!disabled) + }, [editor, disabled]) + + return { editor, upload } +} + +export function TextEditor(props: { + editor: Editor | null + upload: ReturnType<typeof useUploadMutation> +}) { + const { editor, upload } = props + + return ( + <> + {/* hide placeholder when focused */} + <div className="w-full [&:focus-within_p.is-empty]:before:content-none"> + {editor && ( + <FloatingMenu + editor={editor} + className="w-full text-sm text-slate-300" + > + Type <em>*anything*</em> or even paste or{' '} + <FileUploadButton + className="link text-blue-300" + onFiles={upload.mutate} + > + upload an image + </FileUploadButton> + </FloatingMenu> + )} + <EditorContent editor={editor} /> + </div> + {upload.isLoading && <span className="text-xs">Uploading image...</span>} + {upload.isError && ( + <span className="text-error text-xs">Error uploading image :(</span> + )} + </> + ) +} + +const useUploadMutation = (editor: Editor | null) => + useMutation( + (files: File[]) => + Promise.all(files.map((file) => uploadImage('default', file))), + { + onSuccess(urls) { + if (!editor) return + let trans = editor.view.state.tr + urls.forEach((src: any) => { + const node = editor.view.state.schema.nodes.image.create({ src }) + trans = trans.insert(editor.view.state.selection.to, node) + }) + editor.view.dispatch(trans) + }, + } + ) + +function RichContent(props: { content: JSONContent }) { + const { content } = props + const editor = useEditor({ + editorProps: { attributes: { class: proseClass } }, + extensions: exhibitExts, + content, + editable: false, + }) + useEffect(() => void editor?.commands?.setContent(content), [editor, content]) + + return <EditorContent editor={editor} /> +} + +// backwards compatibility: we used to store content as strings +export function Content(props: { content: JSONContent | string }) { + const { content } = props + return typeof content === 'string' ? ( + <Linkify text={content} /> + ) : ( + <RichContent content={content} /> + ) +} diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 68dfcb2d..511767c6 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -37,7 +37,6 @@ export type DescriptionItem = BaseActivityItem & { export type QuestionItem = BaseActivityItem & { type: 'question' - showDescription: boolean contractPath?: string } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index a9618f8c..ff5f5440 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -31,7 +31,6 @@ import { FeedAnswerCommentGroup } from 'web/components/feed/feed-answer-comment- import { FeedCommentThread, CommentInput, - TruncatedComment, } from 'web/components/feed/feed-comments' import { FeedBet } from 'web/components/feed/feed-bets' import { CPMMBinaryContract, NumericContract } from 'common/contract' @@ -104,10 +103,9 @@ export function FeedItem(props: { item: ActivityItem }) { export function FeedQuestion(props: { contract: Contract - showDescription: boolean contractPath?: string }) { - const { contract, showDescription } = props + const { contract } = props const { creatorName, creatorUsername, @@ -163,13 +161,6 @@ export function FeedQuestion(props: { /> )} </Col> - {showDescription && ( - <TruncatedComment - comment={contract.description} - moreHref={contractPath(contract)} - shouldTruncate - /> - )} </div> </div> ) diff --git a/web/components/file-upload-button.tsx b/web/components/file-upload-button.tsx new file mode 100644 index 00000000..3ff15d91 --- /dev/null +++ b/web/components/file-upload-button.tsx @@ -0,0 +1,26 @@ +import { useRef } from 'react' + +/** button that opens file upload window */ +export function FileUploadButton(props: { + onFiles: (files: File[]) => void + className?: string + children?: React.ReactNode +}) { + const { onFiles, className, children } = props + const ref = useRef<HTMLInputElement>(null) + return ( + <> + <button className={className} onClick={() => ref.current?.click()}> + {children} + </button> + <input + ref={ref} + type="file" + accept=".gif,.jpg,.jpeg,.png,.webp, image/*" + multiple + className="hidden" + onChange={(e) => onFiles(Array.from(e.target.files || []))} + /> + </> + ) +} diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index e7794580..5293a6bc 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -7,6 +7,7 @@ import { const storage = getStorage() +// TODO: compress large images export const uploadImage = async ( username: string, file: File, diff --git a/web/package.json b/web/package.json index 454db57c..f81950bf 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,11 @@ "@nivo/core": "0.74.0", "@nivo/line": "0.74.0", "@react-query-firebase/firestore": "0.4.2", + "@tiptap/extension-character-count": "2.0.0-beta.31", + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/extension-placeholder": "2.0.0-beta.53", + "@tiptap/react": "2.0.0-beta.114", + "@tiptap/starter-kit": "2.0.0-beta.190", "algoliasearch": "4.13.0", "clsx": "1.1.1", "cors": "2.8.5", @@ -61,7 +66,7 @@ "next-sitemap": "^2.5.14", "postcss": "8.3.5", "prettier-plugin-tailwindcss": "^0.1.5", - "tailwindcss": "3.0.1", + "tailwindcss": "3.1.6", "tsc-files": "1.1.3" }, "lint-staged": { diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index e8b290f3..bfe13837 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -45,6 +45,7 @@ import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' import { useRouter } from 'next/router' import { useLiquidity } from 'web/hooks/use-liquidity' +import { richTextToString } from 'common/util/parse' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -396,15 +397,18 @@ const getOpenGraphProps = (contract: Contract) => { creatorUsername, outcomeType, creatorAvatarUrl, + description: desc, } = contract const probPercent = outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined + const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) + const description = resolution - ? `Resolved ${resolution}. ${contract.description}` + ? `Resolved ${resolution}. ${stringDesc}` : probPercent - ? `${probPercent} chance. ${contract.description}` - : contract.description + ? `${probPercent} chance. ${stringDesc}` + : stringDesc return { question, diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index 7f52077d..5b9a7dab 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -6,6 +6,7 @@ import { Contract } from 'common/contract' import { User } from 'common/user' import { removeUndefinedProps } from 'common/util/object' import { ENV_CONFIG } from 'common/envs/constants' +import { JSONContent } from '@tiptap/core' export type LiteMarket = { // Unique identifer for this market @@ -20,7 +21,7 @@ export type LiteMarket = { // Market attributes. All times are in milliseconds since epoch closeTime?: number question: string - description: string + description: string | JSONContent tags: string[] url: string outcomeType: string diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 8cd80f7a..2fa4844e 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -36,7 +36,6 @@ export default function ContractSearchFirestore(props: { let matches = (contracts ?? []).filter( (c) => check(c.question) || - check(c.description) || check(c.creatorName) || check(c.creatorUsername) || check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) || diff --git a/web/pages/create.tsx b/web/pages/create.tsx index fd071310..705ef0eb 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -26,6 +26,7 @@ import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' import { User } from 'common/user' +import { TextEditor, useTextEditor } from 'web/components/editor' type NewQuestionParams = { groupId?: string @@ -101,13 +102,11 @@ export function NewContract(props: { (params?.outcomeType as outcomeType) ?? 'BINARY' ) const [initialProb] = useState(50) - const [bottomRef, setBottomRef] = useState<HTMLDivElement | null>(null) const [minString, setMinString] = useState(params?.min ?? '') const [maxString, setMaxString] = useState(params?.max ?? '') const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale) const [initialValueString, setInitialValueString] = useState(initValue) - const [description, setDescription] = useState(params?.description ?? '') useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { @@ -152,9 +151,6 @@ export function NewContract(props: { // get days from today until the end of this year: const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') - const hasUnsavedChanges = !isSubmitting && Boolean(question || description) - useWarnUnsavedChanges(hasUnsavedChanges) - const isValid = (outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) && question.length > 0 && @@ -175,6 +171,20 @@ export function NewContract(props: { min < initialValue && initialValue < max)) + const descriptionPlaceholder = + outcomeType === 'BINARY' + ? `e.g. This question resolves to "YES" if they receive the majority of votes...` + : `e.g. I will choose the answer according to...` + + const { editor, upload } = useTextEditor({ + max: MAX_DESCRIPTION_LENGTH, + placeholder: descriptionPlaceholder, + disabled: isSubmitting, + }) + + const isEditorFilled = editor != null && !editor.isEmpty + useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled)) + function setCloseDateInDays(days: number) { const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') setCloseDate(newCloseDate) @@ -183,14 +193,13 @@ export function NewContract(props: { async function submit() { // TODO: Tell users why their contract is invalid if (!creator || !isValid) return - setIsSubmitting(true) try { const result = await createMarket( removeUndefinedProps({ question, outcomeType, - description, + description: editor?.getJSON(), initialProb, ante, closeTime, @@ -213,15 +222,11 @@ export function NewContract(props: { await router.push(contractPath(result as Contract)) } catch (e) { - console.log('error creating contract', e) + console.error('error creating contract', e, (e as any).details) + setIsSubmitting(false) } } - const descriptionPlaceholder = - outcomeType === 'BINARY' - ? `e.g. This question resolves to "YES" if they receive the majority of votes...` - : `e.g. I will choose the answer according to...` - if (!creator) return <></> return ( @@ -396,25 +401,12 @@ export function NewContract(props: { <Spacer h={6} /> - <div className="form-control mb-1 items-start"> - <label className="label mb-1 gap-2"> + <div className="form-control mb-1 items-start gap-1"> + <label className="label gap-2"> <span className="mb-1">Description</span> <InfoTooltip text="Optional. Describe how you will resolve this question." /> </label> - <Textarea - className="textarea textarea-bordered w-full resize-none" - rows={3} - maxLength={MAX_DESCRIPTION_LENGTH} - placeholder={descriptionPlaceholder} - value={description} - disabled={isSubmitting} - onClick={(e) => e.stopPropagation()} - onChange={(e) => { - setDescription(e.target.value || '') - bottomRef?.scrollIntoView() - }} - /> - <div ref={setBottomRef} /> + <TextEditor editor={editor} upload={upload} /> </div> <Spacer h={6} /> @@ -451,7 +443,7 @@ export function NewContract(props: { 'btn btn-primary normal-case', isSubmitting && 'loading disabled' )} - disabled={isSubmitting || !isValid} + disabled={isSubmitting || !isValid || upload.isLoading} onClick={(e) => { e.preventDefault() submit() diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 5882159d..4266b808 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -159,7 +159,6 @@ export default function GroupPage(props: { ? contracts.filter( (c) => checkAgainstQuery(query, c.question) || - checkAgainstQuery(query, c.description || '') || checkAgainstQuery(query, c.creatorName) || checkAgainstQuery(query, c.creatorUsername) ) diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx deleted file mode 100644 index b22fe371..00000000 --- a/web/pages/make-predictions.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import clsx from 'clsx' -import dayjs from 'dayjs' -import Link from 'next/link' -import { useState } from 'react' -import Textarea from 'react-expanding-textarea' - -import { getProbability } from 'common/calculate' -import { BinaryContract } from 'common/contract' -import { parseWordsAsTags } from 'common/util/parse' -import { BuyAmountInput } from 'web/components/amount-input' -import { InfoTooltip } from 'web/components/info-tooltip' -import { Col } from 'web/components/layout/col' -import { Row } from 'web/components/layout/row' -import { Spacer } from 'web/components/layout/spacer' -import { Linkify } from 'web/components/linkify' -import { Page } from 'web/components/page' -import { Title } from 'web/components/title' -import { useUser } from 'web/hooks/use-user' -import { createMarket } from 'web/lib/firebase/api' -import { contractPath } from 'web/lib/firebase/contracts' - -type Prediction = { - question: string - description: string - initialProb: number - createdUrl?: string -} - -function toPrediction(contract: BinaryContract): Prediction { - const startProb = getProbability(contract) - return { - question: contract.question, - description: contract.description, - initialProb: startProb * 100, - createdUrl: contractPath(contract), - } -} - -function PredictionRow(props: { prediction: Prediction }) { - const { prediction } = props - return ( - <Row className="justify-between gap-4 p-4 hover:bg-gray-300"> - <Col className="justify-between"> - <div className="mb-2 font-medium text-indigo-700"> - <Linkify text={prediction.question} /> - </div> - <div className="text-sm text-gray-500">{prediction.description}</div> - </Col> - {/* Initial probability */} - <div className="ml-auto"> - <div className="text-3xl"> - <div className="text-primary"> - {prediction.initialProb.toFixed(0)}% - <div className="text-lg">chance</div> - </div> - </div> - </div> - {/* Current probability; hidden for now */} - {/* <div> - <div className="text-3xl"> - <div className="text-primary"> - {prediction.initialProb}%<div className="text-lg">chance</div> - </div> - </div> - </div> */} - </Row> - ) -} - -function PredictionList(props: { predictions: Prediction[] }) { - const { predictions } = props - return ( - <Col className="divide-y divide-gray-300 rounded-md border border-gray-300"> - {predictions.map((prediction) => - prediction.createdUrl ? ( - <Link href={prediction.createdUrl}> - <a> - <PredictionRow - key={prediction.question} - prediction={prediction} - /> - </a> - </Link> - ) : ( - <PredictionRow key={prediction.question} prediction={prediction} /> - ) - )} - </Col> - ) -} - -const TEST_VALUE = `1. Biden approval rating (as per 538) is greater than 50%: 80% -2. Court packing is clearly going to happen (new justices don't have to be appointed by end of year): 5% -3. Yang is New York mayor: 80% -4. Newsom recalled as CA governor: 5% -5. At least $250 million in damage from BLM protests this year: 30% -6. Significant capital gains tax hike (above 30% for highest bracket): 20%` - -export default function MakePredictions() { - const user = useUser() - const [predictionsString, setPredictionsString] = useState('') - const [description, setDescription] = useState('') - const [tags, setTags] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const [createdContracts, setCreatedContracts] = useState<BinaryContract[]>([]) - - const [ante, setAnte] = useState<number | undefined>(100) - const [anteError, setAnteError] = useState<string | undefined>() - // By default, close the market a week from today - const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DDT23:59') - const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday) - - const closeTime = closeDate ? dayjs(closeDate).valueOf() : undefined - - const bulkPlaceholder = `e.g. -${TEST_VALUE} -... -` - - const predictions: Prediction[] = [] - - // Parse bulkContracts, then run createContract for each - const lines = predictionsString ? predictionsString.split('\n') : [] - for (const line of lines) { - // Parse line with regex - const matches = line.match(/^(.*):\s*(\d+)%\s*$/) || ['', '', ''] - const [_, question, prob] = matches - - if (!question || !prob) { - console.error('Invalid prediction: ', line) - continue - } - - predictions.push({ - question, - description, - initialProb: parseInt(prob), - }) - } - - async function createMarkets() { - if (!user) { - // TODO: Convey error with snackbar/toast - console.error('You need to be signed in!') - return - } - setIsSubmitting(true) - for (const prediction of predictions) { - const contract = (await createMarket({ - question: prediction.question, - description: prediction.description, - initialProb: prediction.initialProb, - ante, - closeTime, - tags: parseWordsAsTags(tags), - })) as BinaryContract - - setCreatedContracts((prev) => [...prev, contract]) - } - setPredictionsString('') - setIsSubmitting(false) - } - - return ( - <Page> - <Title text="Make Predictions" /> - <div className="w-full rounded-lg bg-gray-100 px-6 py-4 shadow-xl"> - <form> - <div className="form-control"> - <label className="label"> - <span className="label-text">Prediction</span> - <div className="ml-1 text-sm text-gray-500"> - One prediction per line, each formatted like "The sun will rise - tomorrow: 99%" - </div> - </label> - - <textarea - className="textarea textarea-bordered h-60" - placeholder={bulkPlaceholder} - value={predictionsString} - onChange={(e) => setPredictionsString(e.target.value || '')} - ></textarea> - </div> - </form> - - <Spacer h={4} /> - - <div className="form-control w-full"> - <label className="label"> - <span className="label-text">Description</span> - </label> - - <Textarea - placeholder="e.g. This market is part of the ACX predictions for 2022..." - className="input resize-none" - value={description} - onChange={(e) => setDescription(e.target.value || '')} - /> - </div> - - <div className="form-control w-full"> - <label className="label"> - <span className="label-text">Tags</span> - </label> - - <input - type="text" - placeholder="e.g. ACX2021 World" - className="input" - value={tags} - onChange={(e) => setTags(e.target.value || '')} - /> - </div> - - <div className="form-control mb-1 items-start"> - <label className="label mb-1 gap-2"> - <span>Market close</span> - <InfoTooltip text="Trading will be halted after this time (local timezone)." /> - </label> - <input - type="datetime-local" - className="input input-bordered" - onClick={(e) => e.stopPropagation()} - onChange={(e) => setCloseDate(e.target.value || '')} - min={Date.now()} - disabled={isSubmitting} - value={closeDate} - /> - </div> - - <Spacer h={4} /> - - <div className="form-control mb-1 items-start"> - <label className="label mb-1 gap-2"> - <span>Market ante</span> - <InfoTooltip - text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability. - You earn ${0.01 * 100}% of trading volume.`} - /> - </label> - <BuyAmountInput - amount={ante} - minimumAmount={10} - onChange={setAnte} - error={anteError} - setError={setAnteError} - disabled={isSubmitting} - /> - </div> - - {predictions.length > 0 && ( - <div> - <Spacer h={4} /> - <label className="label"> - <span className="label-text">Preview</span> - </label> - <PredictionList predictions={predictions} /> - </div> - )} - - <Spacer h={4} /> - - <div className="my-4 flex justify-end"> - <button - type="submit" - className={clsx('btn btn-primary', { - loading: isSubmitting, - })} - disabled={predictions.length === 0 || isSubmitting} - onClick={(e) => { - e.preventDefault() - createMarkets() - }} - > - Create all - </button> - </div> - </div> - - {createdContracts.length > 0 && ( - <> - <Spacer h={16} /> - <Title text="Created Predictions" /> - <div className="w-full rounded-lg bg-gray-100 px-6 py-4 shadow-xl"> - <PredictionList predictions={createdContracts.map(toPrediction)} /> - </div> - </> - )} - </Page> - ) -} diff --git a/yarn.lock b/yarn.lock index 0ee2aa0f..e84f3616 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2588,6 +2588,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@popperjs/core@^2.9.0": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" + integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2860,6 +2865,193 @@ lodash.isplainobject "^4.0.6" lodash.merge "^4.6.2" +"@tiptap/core@2.0.0-beta.181", "@tiptap/core@^2.0.0-beta.181": + version "2.0.0-beta.181" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.181.tgz#07aeea26336814ab82eb7f4199b17538187c6fbb" + integrity sha512-tbwRqjTVvY9v31TNAH6W0Njhr/OVwI28zWXmH55/USrwyU2CB1iCVfXktZKOhB+8WyvOaBv1JA5YplMIhstYTw== + dependencies: + prosemirror-commands "1.3.0" + prosemirror-keymap "1.2.0" + prosemirror-model "1.18.1" + prosemirror-schema-list "1.2.0" + prosemirror-state "1.4.1" + prosemirror-transform "1.6.0" + prosemirror-view "1.26.2" + +"@tiptap/extension-blockquote@^2.0.0-beta.29": + version "2.0.0-beta.29" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.29.tgz#6f1c4b17efa6457c7776f32d0807e96d848d4389" + integrity sha512-zMYT5TtpKWav9VhTn4JLyMvXmhEdbD6on0MdhcTjRm0I5ugyR4ZbJwh2aelM7G9DZVYzB8jZU18OSDJmo7Af7w== + +"@tiptap/extension-bold@^2.0.0-beta.28": + version "2.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.28.tgz#cf67c264a80434ffb2368f3dd37cf357ae0c2064" + integrity sha512-DY8GOzw9xjmTFrnvTbgHUNxTnDfKrkDgrhe0SUvdkT2udntWp8umPdhPiD3vczLgHOJw6tX68qMRjbsR1ZPcHQ== + +"@tiptap/extension-bubble-menu@^2.0.0-beta.61": + version "2.0.0-beta.61" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.61.tgz#cc61ce8b094fdbcec58f44f0fa39172a726c024c" + integrity sha512-T3Yx+y1sUnXAJjK1CUfsQewSxOpDca9KzKqN2H9c9RZ9UlorR9XmZg6YYW7m9a7adeihj+o3cCO9jRd8dV+nnA== + dependencies: + prosemirror-state "1.4.1" + prosemirror-view "1.26.2" + tippy.js "^6.3.7" + +"@tiptap/extension-bullet-list@^2.0.0-beta.29": + version "2.0.0-beta.29" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.29.tgz#640883e4fffc1a86c7cbd78792688e7edee5ee41" + integrity sha512-R8VB2l1ZB6VeGWx/t/04nBS5Wg3qjIDEZCpPihj2fccJOw99Lu0Ub2UJg/SfdGmeNNpBh4ZYYFv1g/XjyzlXKg== + +"@tiptap/extension-character-count@2.0.0-beta.31": + version "2.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.0.0-beta.31.tgz#fac9ba809ddc38cf67c8a05a42d94e062a1967d2" + integrity sha512-NNA9MN1IjZe+yYQLuYVAg9RNG/3RonYrHiM5mL6vsegd+PF4uMqyZLgsM0/9dMhxh9K/pDPaCRxhuDoZC8V1wA== + dependencies: + prosemirror-model "1.18.1" + prosemirror-state "1.4.1" + +"@tiptap/extension-code-block@^2.0.0-beta.42": + version "2.0.0-beta.42" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.42.tgz#2abfd92eb22399fa542aafb3b76dddfb41d87ab5" + integrity sha512-4wzLup4mI8w9ypIceekUV/8g41cQIPn31qs1iC9u1/JuTkjMj/tA+TFUyp6IMugLxoI/P2DlTztU6/6m7n9DyQ== + dependencies: + prosemirror-state "1.4.1" + +"@tiptap/extension-code@^2.0.0-beta.28": + version "2.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.28.tgz#a22c0e873497ac0bbcd77e4a855322f8591f954e" + integrity sha512-QPJ2Gwb1+3NgcC1ZIhvVcb+FsnWWDu5VZXTKXM4mz892i9V2x48uHg5anPiUV6pcolXsW1F5VNbXIHGTUUO6CQ== + +"@tiptap/extension-document@^2.0.0-beta.17": + version "2.0.0-beta.17" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.17.tgz#ded4182dd860762bcf41c588f712d83908c472a3" + integrity sha512-L6sg0FNchbtIpQkCSjMmItVGs3/vep8Fq56WRtDc1wBSGUSmtHaxQG7F2FZLnNIUMuvzVMRD81m2vYG73WkY6A== + +"@tiptap/extension-dropcursor@^2.0.0-beta.29": + version "2.0.0-beta.29" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.29.tgz#9ccc9d82cb9f8fa28a59ffc061c4c83ee059a12c" + integrity sha512-I+joyoFB8pfdXUPLMqdNO08nlB5m2lbu0VQ5dpqdi/HzgVThMZPZA1cW0X8vAUvrALs5/JFRiFoR9hrLN5R5ng== + dependencies: + prosemirror-dropcursor "1.5.0" + +"@tiptap/extension-floating-menu@^2.0.0-beta.56": + version "2.0.0-beta.56" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.56.tgz#c7428d9109d215bdbd9033f69782c4aadb2aabec" + integrity sha512-j/evHE/6UPGkIgXny9IGcAh0IrcnQmg0b2NBYebs2mqx9xYKYoe+0jVgNdLp/0M3MRgQCzyWTyatBDBFOUR2mw== + dependencies: + prosemirror-state "1.4.1" + prosemirror-view "1.26.2" + tippy.js "^6.3.7" + +"@tiptap/extension-gapcursor@^2.0.0-beta.39": + version "2.0.0-beta.39" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.39.tgz#b8585d2936df7ca90446758c3af90b46d552a1fb" + integrity sha512-oCyz5WEeQXrEIoa1WXaD52yf1EwMFCXaK1cVzFgUj8lkXJ+nJj+O/Zp0Mg+9/MVR0LYu/kifqVorKNXM4AFA/g== + dependencies: + prosemirror-gapcursor "1.3.0" + +"@tiptap/extension-hard-break@^2.0.0-beta.33": + version "2.0.0-beta.33" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.33.tgz#e2f355a22aaaec6e831cf2880c52aa5b0b860573" + integrity sha512-41xf0vSV9hcyTFd01ItLq/CjhjgmOFLCrO3UWN/P2E/cIxuDTyXcvjTE/KXeqRCOV3OYd9fVr0wO91hc8Ij1Yg== + +"@tiptap/extension-heading@^2.0.0-beta.29": + version "2.0.0-beta.29" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.29.tgz#d017d216c0fd1962c266f6f61a335093f9749862" + integrity sha512-q92jYcsT5bPhvuQaB0h44Z9r+Ii22tDYo082KMVnR4+tknHT/3xx+p4JC8KHjh+/5W8Quyafqy6mS8L8VX0zsQ== + +"@tiptap/extension-history@^2.0.0-beta.26": + version "2.0.0-beta.26" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.26.tgz#ae4c0ee8d19b3530e72d99cb5d0f69aefcf96d04" + integrity sha512-ly19uwvdmXG8Fw1KcavXIHi3Qx6JBASOR7394zghOEpW3atpY8nd/8I373rZ8eDUcGOClfaF7bCx2xvIotAAnw== + dependencies: + prosemirror-history "1.3.0" + +"@tiptap/extension-horizontal-rule@^2.0.0-beta.36": + version "2.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.36.tgz#daf8e2d0f30b210a90fdb8f015646653661cfa04" + integrity sha512-o+Zp7dcn3zAQhtlhZiFB69mTHuH3ZRbGEF7Cbf1D3uX1izotni5zIZbPaFFUT4r6OmVe/vDDt/nopfcGc10ktQ== + dependencies: + prosemirror-state "1.4.1" + +"@tiptap/extension-image@2.0.0-beta.30": + version "2.0.0-beta.30" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.30.tgz#60c6cfd09bfd017a3d8b1feaf0931462ffd71a60" + integrity sha512-VhEmgiKkZMiKR7hbpJgIlIUS/QNjSGI5ER7mKDAbuV1IB5yb6nGjZ6o3Exrr2/CaTaW5hQarBC1z2Xgdu05EGg== + +"@tiptap/extension-italic@^2.0.0-beta.28": + version "2.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.28.tgz#bf88ecae64c8f2f69f1f508b802c1efd7454a84e" + integrity sha512-/pKRiCfewh7nqiXRD3N4hQHfGrGNOiWPFYZfY35bSpvTms7PDb/MF7xT1CWW23hSpY31BBS+R/a66vlR/gqu7Q== + +"@tiptap/extension-list-item@^2.0.0-beta.23": + version "2.0.0-beta.23" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843" + integrity sha512-AkzvdELz3ZnrlZM0r9+ritBDOnAjXHR/8zCZhW0ZlWx4zyKPMsNG5ygivY+xr4QT65NEGRT8P8b2zOhXrMjjMQ== + +"@tiptap/extension-ordered-list@^2.0.0-beta.30": + version "2.0.0-beta.30" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.30.tgz#1f656b664302d90272c244b2e478d7056203f2a8" + integrity sha512-GRxGQdq1u0Rp5N8TjthCqoZ//460m343A0HCN7UwfQOnX7Ipv0UJemwNkSHWrl7Pexym9vy3yPWgrn7oRRmgEw== + +"@tiptap/extension-paragraph@^2.0.0-beta.26": + version "2.0.0-beta.26" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.26.tgz#5199c8cedb9c076347a2e15cc67442ef7c3c3fbb" + integrity sha512-WcYsuUa7LLfk0vi7I1dVjdMRu53B52FMMqd+UL1qPdDKVkU3DBsZVwPj+yyfQyqN8Mc/xyg9VacGaiKFLmWNDg== + +"@tiptap/extension-placeholder@2.0.0-beta.53": + version "2.0.0-beta.53" + resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.53.tgz#df29d813044da9a0e30bf8409335e77f6857c2b2" + integrity sha512-NGU/a+GvcJVBjFqb2vI45+rNa3Cjsq/M+R/2xg9olb1w/HBr17NKf/5WSoqcc1S2cdnmMH6rB0/mVhG7Ciur+Q== + dependencies: + prosemirror-model "1.18.1" + prosemirror-state "1.4.1" + prosemirror-view "1.26.2" + +"@tiptap/extension-strike@^2.0.0-beta.29": + version "2.0.0-beta.29" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.29.tgz#7004d0c5d126b0517fa78efc5a333a4b8e3334bf" + integrity sha512-zqFuY7GfNmZ/KClt6kxQ+msGo3syqucP/Xnlihxi+/h/G+oTvEwyOIXCtDOltvxcsWH/TUsdr5vzLp0j+Mdc6Q== + +"@tiptap/extension-text@^2.0.0-beta.17": + version "2.0.0-beta.17" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.17.tgz#4fdd1bdf62c82c1af6feef91c689906a8f5b171e" + integrity sha512-OyKL+pqWJEtjyd9/mrsuY1kZh2b3LWpOQDWKtd4aWR4EA0efmQG+7FPwcIeAVEh7ZoqM+/ABCnPjN6IjzIrSfg== + +"@tiptap/react@2.0.0-beta.114": + version "2.0.0-beta.114" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.114.tgz#fa2b3fcdf379bf7ee25388c0eddbda49249977d5" + integrity sha512-9JbRE+16WM6RxbBxzY74SrJtLodvjeRBnEbWxuhxVgGKxMunRj6r8oED87ODJgqLmkpofwE0KFHTPGdEXfdcKA== + dependencies: + "@tiptap/extension-bubble-menu" "^2.0.0-beta.61" + "@tiptap/extension-floating-menu" "^2.0.0-beta.56" + prosemirror-view "1.26.2" + +"@tiptap/starter-kit@2.0.0-beta.190": + version "2.0.0-beta.190" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.190.tgz#fe0021e29d070fc5707722513a398c8884e15f71" + integrity sha512-jaFMkE6mjCHmCJsXUyLiXGYRVDcHF+PbH/5hEu1riUIAT0Hmm7uak5TYsPeuoCVN7P/tmDEBbBRASZ5CzEQpvw== + dependencies: + "@tiptap/core" "^2.0.0-beta.181" + "@tiptap/extension-blockquote" "^2.0.0-beta.29" + "@tiptap/extension-bold" "^2.0.0-beta.28" + "@tiptap/extension-bullet-list" "^2.0.0-beta.29" + "@tiptap/extension-code" "^2.0.0-beta.28" + "@tiptap/extension-code-block" "^2.0.0-beta.42" + "@tiptap/extension-document" "^2.0.0-beta.17" + "@tiptap/extension-dropcursor" "^2.0.0-beta.29" + "@tiptap/extension-gapcursor" "^2.0.0-beta.39" + "@tiptap/extension-hard-break" "^2.0.0-beta.33" + "@tiptap/extension-heading" "^2.0.0-beta.29" + "@tiptap/extension-history" "^2.0.0-beta.26" + "@tiptap/extension-horizontal-rule" "^2.0.0-beta.36" + "@tiptap/extension-italic" "^2.0.0-beta.28" + "@tiptap/extension-list-item" "^2.0.0-beta.23" + "@tiptap/extension-ordered-list" "^2.0.0-beta.30" + "@tiptap/extension-paragraph" "^2.0.0-beta.26" + "@tiptap/extension-strike" "^2.0.0-beta.29" + "@tiptap/extension-text" "^2.0.0-beta.17" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -3425,7 +3617,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-node@^1.6.1: +acorn-node@^1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== @@ -3623,11 +3815,16 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@^5.0.0, arg@^5.0.1: +arg@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -4174,7 +4371,7 @@ cheerio@^1.0.0-rc.10: parse5-htmlparser2-tree-adapter "^7.0.0" tslib "^2.4.0" -chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -5017,14 +5214,14 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" -detective@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" - integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== +detective@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" + integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== dependencies: - acorn-node "^1.6.1" + acorn-node "^1.8.2" defined "^1.0.0" - minimist "^1.1.1" + minimist "^1.2.6" dicer@^0.3.0: version "0.3.1" @@ -5745,7 +5942,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -8026,7 +8223,7 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -8259,11 +8456,6 @@ object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-hash@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" - integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== - object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" @@ -8398,6 +8590,11 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +orderedmap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.0.0.tgz#12ff5ef6ea9d12d6430b80c701b35475e1c9ff34" + integrity sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -8683,6 +8880,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -8750,15 +8952,23 @@ postcss-discard-unused@^5.1.0: dependencies: postcss-selector-parser "^6.0.5" -postcss-js@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-3.0.3.tgz#2f0bd370a2e8599d45439f6970403b5873abda33" - integrity sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw== +postcss-import@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" + integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== dependencies: camelcase-css "^2.0.1" - postcss "^8.1.6" -postcss-load-config@^3.1.0: +postcss-load-config@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== @@ -8961,7 +9171,7 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== @@ -8991,7 +9201,7 @@ postcss-unique-selectors@^5.1.1: dependencies: postcss-selector-parser "^6.0.5" -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -9019,7 +9229,7 @@ postcss@8.4.5: picocolors "^1.0.0" source-map-js "^1.0.1" -postcss@^8.1.6, postcss@^8.3.11, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.7: +postcss@^8.3.11, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.14, postcss@^8.4.7: version "8.4.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== @@ -9134,6 +9344,91 @@ property-information@^5.0.0, property-information@^5.3.0: dependencies: xtend "^4.0.0" +prosemirror-commands@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz#361b2e2b2a347ce7453386459f97c3f549a1113b" + integrity sha512-BwBbZ5OAScPcm0x7H8SPbqjuEJnCU2RJT9LDyOiiIl/3NbL1nJZI4SFNHwU2e/tRr2Xe7JsptpzseqvZvToLBQ== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-dropcursor@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.5.0.tgz#edbc61d6f71f9f924130eec8e85b0861357957c9" + integrity sha512-vy7i77ddKyXlu8kKBB3nlxLBnsWyKUmQIPB5x8RkYNh01QNp/qqGmdd5yZefJs0s3rtv5r7Izfu2qbtr+tYAMQ== + dependencies: + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + prosemirror-view "^1.1.0" + +prosemirror-gapcursor@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.0.tgz#e07c22ad959b86ec0c4cfc590cc5f484dd984d56" + integrity sha512-9Tdx83xB2W4Oqchm12FtCkSizbqvi64cjs1I9TRPblqdA5TUWoVZ4ZI+t71Jh6HSEh4cDMPzx3UwfryJtKlb/w== + dependencies: + prosemirror-keymap "^1.0.0" + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-view "^1.0.0" + +prosemirror-history@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1" + integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA== + dependencies: + prosemirror-state "^1.2.2" + prosemirror-transform "^1.0.0" + rope-sequence "^1.3.0" + +prosemirror-keymap@1.2.0, prosemirror-keymap@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz#d5cc9da9b712020690a994b50b92a0e448a60bf5" + integrity sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg== + dependencies: + prosemirror-state "^1.0.0" + w3c-keyname "^2.2.0" + +prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0: + version "1.18.1" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" + integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== + dependencies: + orderedmap "^2.0.0" + +prosemirror-schema-list@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.0.tgz#1932268593a7396c0ac168cbe31f28187406ce24" + integrity sha512-8PT/9xOx1HHdC7fDNNfhQ50Z8Mzu7nKyA1KCDltSpcZVZIbB0k7KtsHrnXyuIhbLlScoymBiLZ00c5MH6wdFsA== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1" + integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-transform@1.6.0, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.6.0.tgz#8162dbfaf124f9253a7ab28605a9460411a96a53" + integrity sha512-MAp7AjsjEGEqQY0sSMufNIUuEyB1ZR9Fqlm8dTwwWwpEJRv/plsKjWXBbx52q3Ml8MtaMcd7ic14zAHVB3WaMw== + dependencies: + prosemirror-model "^1.0.0" + +prosemirror-view@1.26.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0: + version "1.26.2" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.2.tgz#e673894ecf26aea330b727622d561c51b41d31eb" + integrity sha512-CGKw+GadkfSBEwRAJTHCEKJ4DlV6/3IhAdjpwGyZHUHtbP7jX4Ol4zmi7xa2c6GOabDlIJLYXJydoNYLX7lNeQ== + dependencies: + prosemirror-model "^1.16.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + proto3-json-serializer@^0.1.8: version "0.1.9" resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34" @@ -9545,6 +9840,13 @@ react@17.0.2, react@^17.0.1: loose-envify "^1.1.0" object-assign "^4.1.1" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -9868,7 +10170,7 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.10.0: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -9917,6 +10219,11 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rope-sequence@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.3.tgz#3f67fc106288b84b71532b4a5fd9d4881e4457f0" + integrity sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q== + rtl-detect@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" @@ -10628,32 +10935,33 @@ svgo@^2.5.0, svgo@^2.7.0: picocolors "^1.0.0" stable "^0.1.8" -tailwindcss@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.1.tgz#bef72ff45d5cfed79bb648d30da952e521e98da4" - integrity sha512-EVDXVZkcueZ77/zfOJw7XkzCuxe5TCiT/S9pw9P183oRzSuwMZ7WO+W/L76jbJQA5qxGeUBJOVOLVBuAUfeZ3g== +tailwindcss@3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.6.tgz#bcb719357776c39e6376a8d84e9834b2b19a49f1" + integrity sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg== dependencies: - arg "^5.0.1" - chalk "^4.1.2" - chokidar "^3.5.2" + arg "^5.0.2" + chokidar "^3.5.3" color-name "^1.1.4" - cosmiconfig "^7.0.1" - detective "^5.2.0" + detective "^5.2.1" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.7" + fast-glob "^3.2.11" glob-parent "^6.0.2" is-glob "^4.0.3" + lilconfig "^2.0.5" normalize-path "^3.0.0" - object-hash "^2.2.0" - postcss-js "^3.0.3" - postcss-load-config "^3.1.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.14" + postcss-import "^14.1.0" + postcss-js "^4.0.0" + postcss-load-config "^3.1.4" postcss-nested "5.0.6" - postcss-selector-parser "^6.0.6" + postcss-selector-parser "^6.0.10" postcss-value-parser "^4.2.0" quick-lru "^5.1.1" - resolve "^1.20.0" - tmp "^0.2.1" + resolve "^1.22.1" tapable@^1.0.0: version "1.1.3" @@ -10722,6 +11030,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tippy.js@^6.3.7: + version "6.3.7" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" + integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== + dependencies: + "@popperjs/core" "^2.9.0" + tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -11198,6 +11513,11 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +w3c-keyname@^2.2.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b" + integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw== + wait-on@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" From a92eda3af243e3e0183e4b262203cb94b57bc46c Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 12:36:01 -0700 Subject: [PATCH 144/519] fix bug where descriptions not showing --- web/components/contract/contract-description.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index a427afe1..b2f839e9 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -46,8 +46,6 @@ export function ContractDescription(props: { }) } - if (!isCreator) return null - const { tags } = contract const categories = tags.filter((tag) => CATEGORY_LIST.includes(tag.toLowerCase()) From 87b669e3580f05037a3f047aa8178183c910d83c Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 13 Jul 2022 12:44:22 -0700 Subject: [PATCH 145/519] Add FYXX Foundation (h/t Holly Elmore) --- common/charity.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/charity.ts b/common/charity.ts index 8c33cb17..f1223b04 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -316,6 +316,14 @@ Without sufficient public interest or research activity, solutions to the proble Wild Animal Initiative currently focuses on helping scientists, grantors, and decision-makers investigate important and understudied questions about wild animal welfare. Our work catalyzes research and applied projects that will open the door to a clearer picture of wild animals’ needs and how to enhance their well-being. Ultimately, we envision a world in which people actively choose to help wild animals — and have the knowledge they need to do so responsibly.`, }, + { + name: 'FYXX Foundation', + website: 'https://www.fyxxfoundation.org/', + photo: 'https://i.imgur.com/ROmWO7m.png', + preview: + 'FYXX Foundation: wildlife population management, without killing.', + description: `The future of our planet depends on the innovations of today, and the health of our wildlife are the first indication of our successful stewardship, which we believe can be improved by safe population management utilizing fertility control instead of poison and culling.`, + }, { name: 'New Incentives', website: 'https://www.newincentives.org/', From 9075a6f33afa636985272ae50468fc17221c708f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 14:59:49 -0500 Subject: [PATCH 146/519] Add headers to limit orders table --- web/components/limit-bets.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 4f1f1893..f81d4294 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -76,6 +76,13 @@ function LimitOrderTable(props: { return ( <table className="table-compact table w-full rounded text-gray-500"> + <thead> + {!isYou && <th>User</th>} + <th>Outcome</th> + <th>Amount</th> + <th>Prob</th> + {isYou && <th></th>} + </thead> <tbody> {limitBets.map((bet) => ( <LimitBet key={bet.id} bet={bet} contract={contract} isYou={isYou} /> From e868f0a15ab5e265c33b253a7606f304695154b0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 15:15:03 -0500 Subject: [PATCH 147/519] Fix pagination component going one page too far + tweaks --- web/components/pagination.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx index 968e49a8..a585985d 100644 --- a/web/components/pagination.tsx +++ b/web/components/pagination.tsx @@ -7,6 +7,8 @@ export function Pagination(props: { }) { const { page, itemsPerPage, totalItems, setPage, scrollToTop } = props + const maxPage = Math.ceil(totalItems / itemsPerPage) - 1 + return ( <nav className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" @@ -14,26 +16,26 @@ export function Pagination(props: { > <div className="hidden sm:block"> <p className="text-sm text-gray-700"> - Showing{' '} + Showing <span className="font-medium">{page * itemsPerPage + 1}</span>{' '} + to{' '} <span className="font-medium"> - {page === 0 ? page + 1 : page * itemsPerPage} + {Math.min(totalItems, (page + 1) * itemsPerPage)} </span>{' '} - to <span className="font-medium">{(page + 1) * itemsPerPage}</span> of{' '} - <span className="font-medium">{totalItems}</span> results + of <span className="font-medium">{totalItems}</span> results </p> </div> <div className="flex flex-1 justify-between sm:justify-end"> <a href={scrollToTop ? '#' : undefined} - className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + className="relative inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => page > 0 && setPage(page - 1)} > Previous </a> <a href={scrollToTop ? '#' : undefined} - className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => page < totalItems / itemsPerPage && setPage(page + 1)} + className="relative ml-3 inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page < maxPage && setPage(page + 1)} > Next </a> From 55c91dfcdd4a5d497d1afea38b5f3db4eaa89ccc Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 15:11:22 -0600 Subject: [PATCH 148/519] Categories to groups (#641) * start on script * Revert "Remove category filters" This reverts commit d6e808e1a39e20193c97f713b1710ed687f7a5a4. * Convert categories to official default groups * Add new users to default groups * Rework group cards * Cleanup * Add unique bettors to contract and sort by them * Most bettors to most popular * Unused vars * Track unique bettor ids on contracts * Add followed users' bets to personal markets * Add new users to welcome, bugs, and updates groups * Add users to fewer default cats --- common/calculate-dpm.ts | 2 +- common/categories.ts | 17 +- common/contract.ts | 15 +- common/group.ts | 3 + firestore.rules | 2 +- functions/src/create-user.ts | 62 +++++- functions/src/get-daily-bonuses.ts | 142 ------------- functions/src/index.ts | 1 - functions/src/on-create-bet.ts | 120 ++++++++++- functions/src/scripts/convert-categories.ts | 110 ++++++++++ web/components/contract-search.tsx | 59 +++++- web/components/leaderboard.tsx | 5 +- web/components/nav/sidebar.tsx | 4 +- web/components/notifications-icon.tsx | 10 - web/hooks/use-group.ts | 19 +- web/hooks/use-sort-and-query-params.tsx | 3 +- web/lib/firebase/api.ts | 4 - web/lib/firebase/groups.ts | 20 +- web/pages/contract-search-firestore.tsx | 5 + web/pages/create.tsx | 4 +- web/pages/group/[...slugs]/index.tsx | 210 +++++++++++--------- web/pages/groups.tsx | 180 +++++++++-------- web/pages/home.tsx | 2 +- 23 files changed, 617 insertions(+), 382 deletions(-) delete mode 100644 functions/src/get-daily-bonuses.ts create mode 100644 functions/src/scripts/convert-categories.ts diff --git a/common/calculate-dpm.ts b/common/calculate-dpm.ts index 497f1155..d38a7b67 100644 --- a/common/calculate-dpm.ts +++ b/common/calculate-dpm.ts @@ -2,7 +2,7 @@ import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash' import { Bet, NumericBet } from './bet' import { DPMContract, DPMBinaryContract, NumericContract } from './contract' import { DPM_FEES } from './fees' -import { normpdf } from '../common/util/math' +import { normpdf } from './util/math' import { addObjects } from './util/object' export function getDpmProbability(totalShares: { [outcome: string]: number }) { diff --git a/common/categories.ts b/common/categories.ts index 2bd6d25a..232aa526 100644 --- a/common/categories.ts +++ b/common/categories.ts @@ -1,5 +1,6 @@ import { difference } from 'lodash' +export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default' export const CATEGORIES = { politics: 'Politics', technology: 'Technology', @@ -24,9 +25,15 @@ export const TO_CATEGORY = Object.fromEntries( export const CATEGORY_LIST = Object.keys(CATEGORIES) -export const EXCLUDED_CATEGORIES: category[] = ['fun', 'manifold', 'personal'] +export const EXCLUDED_CATEGORIES: category[] = [ + 'fun', + 'manifold', + 'personal', + 'covid', + 'culture', + 'gaming', + 'crypto', + 'world', +] -export const DEFAULT_CATEGORIES = difference( - CATEGORY_LIST, - EXCLUDED_CATEGORIES -) +export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES) diff --git a/common/contract.ts b/common/contract.ts index 52ca91d6..5ddcf0b8 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -44,10 +44,14 @@ export type Contract<T extends AnyContractType = AnyContractType> = { volume7Days: number collectedFees: Fees + + groupSlugs?: string[] + uniqueBettorIds?: string[] + uniqueBettorCount?: number } & T -export type BinaryContract = Contract & Binary -export type PseudoNumericContract = Contract & PseudoNumeric +export type BinaryContract = Contract & Binary +export type PseudoNumericContract = Contract & PseudoNumeric export type NumericContract = Contract & Numeric export type FreeResponseContract = Contract & FreeResponse export type DPMContract = Contract & DPM @@ -109,7 +113,12 @@ export type Numeric = { export type outcomeType = AnyOutcomeType['outcomeType'] export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const -export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const +export const OUTCOME_TYPES = [ + 'BINARY', + 'FREE_RESPONSE', + 'PSEUDO_NUMERIC', + 'NUMERIC', +] as const export const MAX_QUESTION_LENGTH = 480 export const MAX_DESCRIPTION_LENGTH = 10000 diff --git a/common/group.ts b/common/group.ts index f06fdd15..15348d5a 100644 --- a/common/group.ts +++ b/common/group.ts @@ -9,7 +9,10 @@ export type Group = { memberIds: string[] // User ids anyoneCanJoin: boolean contractIds: string[] + + chatDisabled?: boolean } export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 +export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] diff --git a/firestore.rules b/firestore.rules index 28ff4485..918448d6 100644 --- a/firestore.rules +++ b/firestore.rules @@ -71,7 +71,7 @@ service cloud.firestore { match /contracts/{contractId} { allow read; allow update: if request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['tags', 'lowercaseTags']); + .hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); allow update: if request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['description', 'closeTime']) && resource.data.creatorId == request.auth.uid; diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index e70371ca..332c1872 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -6,7 +6,7 @@ import { SUS_STARTING_BALANCE, User, } from '../../common/user' -import { getUser, getUserByUsername } from './utils' +import { getUser, getUserByUsername, getValues, isProd } from './utils' import { randomString } from '../../common/util/random' import { cleanDisplayName, @@ -14,10 +14,19 @@ import { } from '../../common/util/clean-username' import { sendWelcomeEmail } from './emails' import { isWhitelisted } from '../../common/envs/constants' -import { DEFAULT_CATEGORIES } from '../../common/categories' +import { + CATEGORIES_GROUP_SLUG_POSTFIX, + DEFAULT_CATEGORIES, +} from '../../common/categories' import { track } from './analytics' import { APIError, newEndpoint, validate } from './api' +import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group' +import { uniq } from 'lodash' +import { + DEV_HOUSE_LIQUIDITY_PROVIDER_ID, + HOUSE_LIQUIDITY_PROVIDER_ID, +} from '../../common/antes' const bodySchema = z.object({ deviceToken: z.string().optional(), @@ -85,7 +94,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => { await firestore.collection('private-users').doc(auth.uid).create(privateUser) await sendWelcomeEmail(user, privateUser) - + await addUserToDefaultGroups(user) await track(auth.uid, 'create user', { username }, { ip: req.ip }) return user @@ -110,3 +119,50 @@ const numberUsersWithIp = async (ipAddress: string) => { return snap.docs.length } + +const addUserToDefaultGroups = async (user: User) => { + for (const category of Object.values(DEFAULT_CATEGORIES)) { + const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX + const groups = await getValues<Group>( + firestore.collection('groups').where('slug', '==', slug) + ) + await firestore + .collection('groups') + .doc(groups[0].id) + .update({ + memberIds: uniq(groups[0].memberIds.concat(user.id)), + }) + } + + for (const slug of NEW_USER_GROUP_SLUGS) { + const groups = await getValues<Group>( + firestore.collection('groups').where('slug', '==', slug) + ) + const group = groups[0] + await firestore + .collection('groups') + .doc(group.id) + .update({ + memberIds: uniq(group.memberIds.concat(user.id)), + }) + const manifoldAccount = isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + + if (slug === 'welcome') { + const welcomeCommentDoc = firestore + .collection(`groups/${group.id}/comments`) + .doc() + await welcomeCommentDoc.create({ + id: welcomeCommentDoc.id, + groupId: group.id, + userId: manifoldAccount, + text: `Welcome, ${user.name} (@${user.username})!`, + createdTime: Date.now(), + userName: 'Manifold Markets', + userUsername: 'ManifoldMarkets', + userAvatarUrl: 'https://manifold.markets/logo-bg-white.png', + }) + } + } +} diff --git a/functions/src/get-daily-bonuses.ts b/functions/src/get-daily-bonuses.ts deleted file mode 100644 index 017c32fc..00000000 --- a/functions/src/get-daily-bonuses.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { APIError, newEndpoint } from './api' -import { isProd, log } from './utils' -import * as admin from 'firebase-admin' -import { PrivateUser } from '../../common/lib/user' -import { uniq } from 'lodash' -import { Bet } from '../../common/lib/bet' -const firestore = admin.firestore() -import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, - HOUSE_LIQUIDITY_PROVIDER_ID, -} from '../../common/antes' -import { runTxn, TxnData } from './transact' -import { createNotification } from './create-notification' -import { User } from '../../common/lib/user' -import { Contract } from '../../common/lib/contract' -import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants' - -const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime() -const QUERY_LIMIT_SECONDS = 60 - -export const getdailybonuses = newEndpoint({}, async (req, auth) => { - const { user, lastTimeCheckedBonuses } = await firestore.runTransaction( - async (trans) => { - const userSnap = await trans.get( - firestore.doc(`private-users/${auth.uid}`) - ) - if (!userSnap.exists) throw new APIError(400, 'User not found.') - const user = userSnap.data() as PrivateUser - const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0 - if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000) - throw new APIError( - 400, - `Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.` - ) - await trans.update(userSnap.ref, { - lastTimeCheckedBonuses: Date.now(), - }) - return { - user, - lastTimeCheckedBonuses, - } - } - ) - const fromUserId = isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID - const fromSnap = await firestore.doc(`users/${fromUserId}`).get() - if (!fromSnap.exists) throw new APIError(400, 'From user not found.') - const fromUser = fromSnap.data() as User - // Get all users contracts made since implementation time - const userContractsSnap = await firestore - .collection(`contracts`) - .where('creatorId', '==', user.id) - .where('createdTime', '>=', BONUS_START_DATE) - .get() - const userContracts = userContractsSnap.docs.map( - (doc) => doc.data() as Contract - ) - const nullReturn = { status: 'no bets', txn: null } - for (const contract of userContracts) { - const result = await firestore.runTransaction(async (trans) => { - const contractId = contract.id - // Get all bets made on user's contracts - const bets = ( - await firestore - .collection(`contracts/${contractId}/bets`) - .where('userId', '!=', user.id) - .get() - ).docs.map((bet) => bet.ref) - if (bets.length === 0) { - return nullReturn - } - const contractBetsSnap = await trans.getAll(...bets) - const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet) - - const uniqueBettorIdsBeforeLastResetTime = uniq( - contractBets - .filter((bet) => bet.createdTime < lastTimeCheckedBonuses) - .map((bet) => bet.userId) - ) - - // Filter users for ONLY those that have made bets since the last daily bonus received time - const uniqueBettorIdsWithBetsAfterLastResetTime = uniq( - contractBets - .filter((bet) => bet.createdTime > lastTimeCheckedBonuses) - .map((bet) => bet.userId) - ) - - // Filter for users only present in the above list - const newUniqueBettorIds = - uniqueBettorIdsWithBetsAfterLastResetTime.filter( - (userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId) - ) - newUniqueBettorIds.length > 0 && - log( - `Got ${newUniqueBettorIds.length} new unique bettors since last bonus` - ) - if (newUniqueBettorIds.length === 0) { - return nullReturn - } - // Create combined txn for all unique bettors - const bonusTxnDetails = { - contractId: contractId, - uniqueBettors: newUniqueBettorIds.length, - } - const bonusTxn: TxnData = { - fromId: fromUser.id, - fromType: 'BANK', - toId: user.id, - toType: 'USER', - amount: UNIQUE_BETTOR_BONUS_AMOUNT * newUniqueBettorIds.length, - token: 'M$', - category: 'UNIQUE_BETTOR_BONUS', - description: JSON.stringify(bonusTxnDetails), - } - return await runTxn(trans, bonusTxn) - }) - - if (result.status != 'success' || !result.txn) { - result.status != nullReturn.status && - log(`No bonus for user: ${user.id} - reason:`, result.status) - } else { - log(`Bonus txn for user: ${user.id} completed:`, result.txn?.id) - await createNotification( - result.txn.id, - 'bonus', - 'created', - fromUser, - result.txn.id, - result.txn.amount + '', - contract, - undefined, - // No need to set the user id, we'll use the contract creator id - undefined, - contract.slug, - contract.question - ) - } - } - - return { userId: user.id, message: 'success' } -}) diff --git a/functions/src/index.ts b/functions/src/index.ts index e5ae78ec..cf75802e 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -38,6 +38,5 @@ export * from './add-liquidity' export * from './withdraw-liquidity' export * from './create-group' export * from './resolve-market' -export * from './get-daily-bonuses' export * from './unsubscribe' export * from './stripe' diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index 5789ed0b..adf22d56 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -1,13 +1,26 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { keyBy } from 'lodash' +import { keyBy, uniq } from 'lodash' import { Bet, LimitBet } from '../../common/bet' -import { getContract, getUser, getValues } from './utils' -import { createBetFillNotification } from './create-notification' +import { getContract, getUser, getValues, isProd, log } from './utils' +import { + createBetFillNotification, + createNotification, +} from './create-notification' import { filterDefined } from '../../common/util/array' +import { Contract } from '../../common/contract' +import { runTxn, TxnData } from './transact' +import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants' +import { + DEV_HOUSE_LIQUIDITY_PROVIDER_ID, + HOUSE_LIQUIDITY_PROVIDER_ID, +} from '../../common/antes' +import { APIError } from '../../common/api' +import { User } from '../../common/user' const firestore = admin.firestore() +const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime() export const onCreateBet = functions.firestore .document('contracts/{contractId}/bets/{betId}') @@ -26,8 +39,109 @@ export const onCreateBet = functions.firestore .update({ lastBetTime, lastUpdatedTime: Date.now() }) await notifyFills(bet, contractId, eventId) + await updateUniqueBettorsAndGiveCreatorBonus( + contractId, + eventId, + bet.userId + ) }) +const updateUniqueBettorsAndGiveCreatorBonus = async ( + contractId: string, + eventId: string, + bettorId: string +) => { + const userContractSnap = await firestore + .collection(`contracts`) + .doc(contractId) + .get() + const contract = userContractSnap.data() as Contract + if (!contract) { + log(`Could not find contract ${contractId}`) + return + } + let previousUniqueBettorIds = contract.uniqueBettorIds + + if (!previousUniqueBettorIds) { + const contractBets = ( + await firestore + .collection(`contracts/${contractId}/bets`) + .where('userId', '!=', contract.creatorId) + .get() + ).docs.map((doc) => doc.data() as Bet) + + if (contractBets.length === 0) { + log(`No bets for contract ${contractId}`) + return + } + + previousUniqueBettorIds = uniq( + contractBets + .filter((bet) => bet.createdTime < BONUS_START_DATE) + .map((bet) => bet.userId) + ) + } + + const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId) + + const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId]) + // Update contract unique bettors + if (!contract.uniqueBettorIds || isNewUniqueBettor) { + log(`Got ${previousUniqueBettorIds} unique bettors`) + isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`) + await firestore.collection(`contracts`).doc(contractId).update({ + uniqueBettorIds: newUniqueBettorIds, + uniqueBettorCount: newUniqueBettorIds.length, + }) + } + if (!isNewUniqueBettor) return + + // Create combined txn for all new unique bettors + const bonusTxnDetails = { + contractId: contractId, + uniqueBettorIds: newUniqueBettorIds, + } + const fromUserId = isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + const fromSnap = await firestore.doc(`users/${fromUserId}`).get() + if (!fromSnap.exists) throw new APIError(400, 'From user not found.') + const fromUser = fromSnap.data() as User + const result = await firestore.runTransaction(async (trans) => { + const bonusTxn: TxnData = { + fromId: fromUser.id, + fromType: 'BANK', + toId: contract.creatorId, + toType: 'USER', + amount: UNIQUE_BETTOR_BONUS_AMOUNT, + token: 'M$', + category: 'UNIQUE_BETTOR_BONUS', + description: JSON.stringify(bonusTxnDetails), + } + return await runTxn(trans, bonusTxn) + }) + + if (result.status != 'success' || !result.txn) { + log(`No bonus for user: ${contract.creatorId} - reason:`, result.status) + } else { + log(`Bonus txn for user: ${contract.creatorId} completed:`, result.txn?.id) + await createNotification( + result.txn.id, + 'bonus', + 'created', + fromUser, + eventId + '-bonus', + result.txn.amount + '', + contract, + undefined, + // No need to set the user id, we'll use the contract creator id + undefined, + contract.slug, + contract.question + ) + } +} + const notifyFills = async (bet: Bet, contractId: string, eventId: string) => { if (!bet.fills) return diff --git a/functions/src/scripts/convert-categories.ts b/functions/src/scripts/convert-categories.ts new file mode 100644 index 00000000..8fe90807 --- /dev/null +++ b/functions/src/scripts/convert-categories.ts @@ -0,0 +1,110 @@ +import * as admin from 'firebase-admin' + +import { initAdmin } from './script-init' +initAdmin() + +import { getValues, isProd } from '../utils' +import { + CATEGORIES_GROUP_SLUG_POSTFIX, + DEFAULT_CATEGORIES, +} from 'common/categories' +import { Group } from 'common/group' +import { uniq } from 'lodash' +import { Contract } from 'common/contract' +import { User } from 'common/user' +import { filterDefined } from 'common/util/array' +import { + DEV_HOUSE_LIQUIDITY_PROVIDER_ID, + HOUSE_LIQUIDITY_PROVIDER_ID, +} from 'common/antes' + +const adminFirestore = admin.firestore() + +async function convertCategoriesToGroups() { + const groups = await getValues<Group>(adminFirestore.collection('groups')) + const contracts = await getValues<Contract>( + adminFirestore.collection('contracts') + ) + for (const group of groups) { + const groupContracts = contracts.filter((contract) => + group.contractIds.includes(contract.id) + ) + for (const contract of groupContracts) { + await adminFirestore + .collection('contracts') + .doc(contract.id) + .update({ + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + }) + } + } + + for (const category of Object.values(DEFAULT_CATEGORIES)) { + const markets = await getValues<Contract>( + adminFirestore + .collection('contracts') + .where('lowercaseTags', 'array-contains', category.toLowerCase()) + ) + const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX + const oldGroup = await getValues<Group>( + adminFirestore.collection('groups').where('slug', '==', slug) + ) + if (oldGroup.length > 0) { + console.log(`Found old group for ${category}`) + await adminFirestore.collection('groups').doc(oldGroup[0].id).delete() + } + + const allUsers = await getValues<User>(adminFirestore.collection('users')) + const groupUsers = filterDefined( + allUsers.map((user: User) => { + if (!user.followedCategories || user.followedCategories.length === 0) + return user.id + if (!user.followedCategories.includes(category.toLowerCase())) + return null + return user.id + }) + ) + + const manifoldAccount = isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + const newGroupRef = await adminFirestore.collection('groups').doc() + const newGroup: Group = { + id: newGroupRef.id, + name: category, + slug, + creatorId: manifoldAccount, + createdTime: Date.now(), + anyoneCanJoin: true, + memberIds: [manifoldAccount], + about: 'Official group for all things related to ' + category, + mostRecentActivityTime: Date.now(), + contractIds: markets.map((market) => market.id), + chatDisabled: true, + } + + await adminFirestore.collection('groups').doc(newGroupRef.id).set(newGroup) + // Update group with new memberIds to avoid notifying everyone + await adminFirestore + .collection('groups') + .doc(newGroupRef.id) + .update({ + memberIds: uniq(groupUsers), + }) + + for (const market of markets) { + await adminFirestore + .collection('contracts') + .doc(market.id) + .update({ + groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]), + }) + } + } +} + +if (require.main === module) { + convertCategoriesToGroups() + .then(() => process.exit()) + .catch(console.log) +} diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 220a95ab..834a4db1 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -20,8 +20,12 @@ import { Row } from './layout/row' import { useEffect, useMemo, useRef, useState } from 'react' import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' +import { useUser } from 'web/hooks/use-user' +import { useFollows } from 'web/hooks/use-follows' import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' +import { useMemberGroups } from 'web/hooks/use-group' +import { NEW_USER_GROUP_SLUGS } from 'common/group' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -33,6 +37,7 @@ const indexPrefix = ENV === 'DEV' ? 'dev-' : '' const sortIndexes = [ { label: 'Newest', value: indexPrefix + 'contracts-newest' }, { label: 'Oldest', value: indexPrefix + 'contracts-oldest' }, + { label: 'Most popular', value: indexPrefix + 'contracts-most-popular' }, { label: 'Most traded', value: indexPrefix + 'contracts-most-traded' }, { label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' }, { label: 'Last updated', value: indexPrefix + 'contracts-last-updated' }, @@ -40,7 +45,7 @@ const sortIndexes = [ { label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' }, ] -type filter = 'open' | 'closed' | 'resolved' | 'all' +type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' export function ContractSearch(props: { querySortOptions?: { @@ -69,13 +74,19 @@ export function ContractSearch(props: { hideQuickBet, } = props + const user = useUser() + const memberGroupSlugs = useMemberGroups(user?.id) + ?.map((g) => g.slug) + .filter((s) => !NEW_USER_GROUP_SLUGS.includes(s)) + const follows = useFollows(user?.id) + console.log(memberGroupSlugs, follows) const { initialSort } = useInitialQueryAndSort(querySortOptions) const sort = sortIndexes .map(({ value }) => value) .includes(`${indexPrefix}contracts-${initialSort ?? ''}`) ? initialSort - : querySortOptions?.defaultSort ?? '24-hour-vol' + : querySortOptions?.defaultSort ?? 'most-popular' const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' @@ -86,10 +97,21 @@ export function ContractSearch(props: { filter === 'open' ? 'isResolved:false' : '', filter === 'closed' ? 'isResolved:false' : '', filter === 'resolved' ? 'isResolved:true' : '', + filter === 'personal' + ? // Show contracts in groups that the user is a member of + (memberGroupSlugs?.map((slug) => `groupSlugs:${slug}`) ?? []) + // Show contracts created by users the user follows + .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) + // Show contracts bet on by users the user follows + .concat( + follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] + // Show contracts bet on by the user + ) + .concat(user ? `uniqueBettorIds:${user.id}` : []) + : '', additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` : '', - additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', ].filter((f) => f) // Hack to make Algolia work. filters = ['', ...filters] @@ -100,7 +122,12 @@ export function ContractSearch(props: { ].filter((f) => f) return { filters, numericFilters } - }, [filter, Object.values(additionalFilter ?? {}).join(',')]) + }, [ + filter, + Object.values(additionalFilter ?? {}).join(','), + (memberGroupSlugs ?? []).join(','), + (follows ?? []).join(','), + ]) const indexName = `${indexPrefix}contracts-${sort}` @@ -125,6 +152,7 @@ export function ContractSearch(props: { resetIcon: 'mt-2 hidden sm:flex', }} /> + {/*// TODO track WHICH filter users are using*/} <select className="!select !select-bordered" value={filter} @@ -134,6 +162,7 @@ export function ContractSearch(props: { <option value="open">Open</option> <option value="closed">Closed</option> <option value="resolved">Resolved</option> + <option value="personal">For you</option> <option value="all">All</option> </select> {!hideOrderSelector && ( @@ -155,13 +184,21 @@ export function ContractSearch(props: { <Spacer h={3} /> - <ContractSearchInner - querySortOptions={querySortOptions} - onContractClick={onContractClick} - overrideGridClassName={overrideGridClassName} - hideQuickBet={hideQuickBet} - excludeContractIds={additionalFilter?.excludeContractIds} - /> + {/*<Spacer h={4} />*/} + + {filter === 'personal' && + (follows ?? []).length === 0 && + (memberGroupSlugs ?? []).length === 0 ? ( + <>You're not following anyone, nor in any of your own groups yet.</> + ) : ( + <ContractSearchInner + querySortOptions={querySortOptions} + onContractClick={onContractClick} + overrideGridClassName={overrideGridClassName} + hideQuickBet={hideQuickBet} + excludeContractIds={additionalFilter?.excludeContractIds} + /> + )} </InstantSearch> ) } diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx index fb104060..b8c725e0 100644 --- a/web/components/leaderboard.tsx +++ b/web/components/leaderboard.tsx @@ -13,9 +13,12 @@ export function Leaderboard(props: { renderCell: (user: User) => any }[] className?: string + maxToShow?: number }) { // TODO: Ideally, highlight your own entry on the leaderboard - const { title, users, columns, className } = props + const { title, columns, className } = props + const maxToShow = props.maxToShow ?? props.users.length + const users = props.users.slice(0, maxToShow) return ( <div className={clsx('w-full px-1', className)}> <Title text={title} className="!mt-0" /> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 430e98d2..784eb63a 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -193,7 +193,9 @@ export default function Sidebar(props: { className?: string }) { const mobileNavigationOptions = !user ? signedOutMobileNavigation : signedInMobileNavigation - const memberItems = (useMemberGroups(user?.id) ?? []).map((group: Group) => ({ + const memberItems = ( + useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] + ).map((group: Group) => ({ name: group.name, href: groupPath(group.slug), })) diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index 478b4ad4..0dfc5054 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -6,22 +6,12 @@ import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useRouter } from 'next/router' import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' -import { requestBonuses } from 'web/lib/firebase/api' import { PrivateUser } from 'common/user' export default function NotificationsIcon(props: { className?: string }) { const user = useUser() const privateUser = usePrivateUser(user?.id) - useEffect(() => { - if ( - privateUser && - privateUser.lastTimeCheckedBonuses && - Date.now() - privateUser.lastTimeCheckedBonuses > 1000 * 70 - ) - requestBonuses({}).catch(() => console.log('no bonuses for you (yet)')) - }, [privateUser]) - return ( <Row className={clsx('justify-center')}> <div className={'relative'}> diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 41f84707..1fde9f4e 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -2,12 +2,15 @@ import { useEffect, useState } from 'react' import { Group } from 'common/group' import { User } from 'common/user' import { + getGroupBySlug, getGroupsWithContractId, listenForGroup, listenForGroups, listenForMemberGroups, } from 'web/lib/firebase/groups' import { getUser } from 'web/lib/firebase/users' +import { CATEGORIES, CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' +import { filterDefined } from 'common/util/array' export const useGroup = (groupId: string | undefined) => { const [group, setGroup] = useState<Group | null | undefined>() @@ -29,11 +32,21 @@ export const useGroups = () => { return groups } -export const useMemberGroups = (userId: string | null | undefined) => { +export const useMemberGroups = ( + userId: string | null | undefined, + options?: { withChatEnabled: boolean } +) => { const [memberGroups, setMemberGroups] = useState<Group[] | undefined>() useEffect(() => { - if (userId) return listenForMemberGroups(userId, setMemberGroups) - }, [userId]) + if (userId) + return listenForMemberGroups(userId, (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined(groups.filter((group) => group.chatDisabled !== true)) + ) + return setMemberGroups(groups) + }) + }, [options?.withChatEnabled, userId]) return memberGroups } diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index b7bfb288..a2590249 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -10,6 +10,7 @@ export type Sort = | 'newest' | 'oldest' | 'most-traded' + | 'most-popular' | '24-hour-vol' | 'close-date' | 'resolve-date' @@ -35,7 +36,7 @@ export function useInitialQueryAndSort(options?: { shouldLoadFromStorage?: boolean }) { const { defaultSort, shouldLoadFromStorage } = defaults(options, { - defaultSort: '24-hour-vol', + defaultSort: 'most-popular', shouldLoadFromStorage: true, }) const router = useRouter() diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts index a6bd4359..27d6caa3 100644 --- a/web/lib/firebase/api.ts +++ b/web/lib/firebase/api.ts @@ -80,7 +80,3 @@ export function claimManalink(params: any) { export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } - -export function requestBonuses(params: any) { - return call(getFunctionUrl('getdailybonuses'), 'POST', params) -} diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index fbb11520..708096b3 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -8,7 +8,7 @@ import { } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' import { Group } from 'common/group' -import { getContractFromId } from './contracts' +import { getContractFromId, updateContract } from './contracts' import { coll, getValue, @@ -17,6 +17,7 @@ import { listenForValues, } from './utils' import { filterDefined } from 'common/util/array' +import { Contract } from 'common/contract' export const groups = coll<Group>('groups') @@ -129,7 +130,22 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> { return newGroup } -export async function addContractToGroup(group: Group, contractId: string) { +export async function addContractToGroup(group: Group, contract: Contract) { + await updateContract(contract.id, { + groupSlugs: [...(contract.groupSlugs ?? []), group.slug], + }) + return await updateGroup(group, { + contractIds: uniq([...group.contractIds, contract.id]), + }) + .then(() => group) + .catch((err) => { + console.error('error adding contract to group', err) + return err + }) +} + +export async function setContractGroupSlugs(group: Group, contractId: string) { + await updateContract(contractId, { groupSlugs: [group.slug] }) return await updateGroup(group, { contractIds: uniq([...group.contractIds, contractId]), }) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 2fa4844e..3ac11993 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -61,6 +61,10 @@ export default function ContractSearchFirestore(props: { ) } else if (sort === 'most-traded') { matches.sort((a, b) => b.volume - a.volume) + } else if (sort === 'most-popular') { + matches.sort( + (a, b) => (b.uniqueBettorCount ?? 0) - (a.uniqueBettorCount ?? 0) + ) } else if (sort === '24-hour-vol') { // Use lodash for stable sort, so previous sort breaks all ties. matches = sortBy(matches, ({ volume7Days }) => -1 * volume7Days) @@ -107,6 +111,7 @@ export default function ContractSearchFirestore(props: { > <option value="newest">Newest</option> <option value="oldest">Oldest</option> + <option value="most-popular">Most popular</option> <option value="most-traded">Most traded</option> <option value="24-hour-vol">24h volume</option> <option value="close-date">Closing soon</option> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 705ef0eb..7040dff0 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { addContractToGroup, getGroup } from 'web/lib/firebase/groups' +import { setContractGroupSlugs, getGroup } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -217,7 +217,7 @@ export function NewContract(props: { isFree: false, }) if (result && selectedGroup) { - await addContractToGroup(selectedGroup, result.id) + await setContractGroupSlugs(selectedGroup, result.id) } await router.push(contractPath(result as Contract)) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 4266b808..fc76df48 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -9,8 +9,8 @@ import { getGroupBySlug, getGroupContracts, updateGroup, - addContractToGroup, addUserToGroup, + addContractToGroup, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' @@ -54,6 +54,7 @@ import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' +import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -145,7 +146,7 @@ export default function GroupPage(props: { const router = useRouter() const { slugs } = router.query as { slugs: string[] } - const page = (slugs?.[1] ?? 'chat') as typeof groupSubpages[number] + const page = slugs?.[1] as typeof groupSubpages[number] const group = useGroup(props.group?.id) ?? props.group const [contracts, setContracts] = useState<Contract[] | undefined>(undefined) @@ -213,6 +214,75 @@ export default function GroupPage(props: { /> </Col> ) + + const tabs = [ + ...(group.chatDisabled + ? [] + : [ + { + title: 'Chat', + content: messages ? ( + <GroupChat + messages={messages} + user={user} + group={group} + tips={tips} + /> + ) : ( + <LoadingIndicator /> + ), + href: groupPath(group.slug, 'chat'), + }, + ]), + { + title: 'Questions', + content: ( + <div className={'mt-2 px-1'}> + {contracts ? ( + contracts.length > 0 ? ( + <> + <input + type="text" + onChange={(e) => debouncedQuery(e.target.value)} + placeholder="Search the group's questions" + className="input input-bordered mb-4 w-full" + /> + <ContractsGrid + contracts={query != '' ? filteredContracts : contracts} + hasMore={false} + loadMore={() => {}} + /> + </> + ) : ( + <div className="p-2 text-gray-500"> + No questions yet. Why not{' '} + <SiteLink + href={`/create/?groupId=${group.id}`} + className={'font-bold text-gray-700'} + > + add one? + </SiteLink> + </div> + ) + ) : ( + <LoadingIndicator /> + )} + </div> + ), + href: groupPath(group.slug, 'questions'), + }, + { + title: 'Rankings', + content: leaderboard, + href: groupPath(group.slug, 'rankings'), + }, + { + title: 'About', + content: aboutTab, + href: groupPath(group.slug, 'about'), + }, + ] + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') return ( <Page rightSidebar={rightSidebar} className="!pb-0"> <SEO @@ -250,79 +320,9 @@ export default function GroupPage(props: { <Tabs currentPageForAnalytics={groupPath(group.slug)} - className={'mx-3 mb-0'} - defaultIndex={ - page === 'rankings' - ? 2 - : page === 'about' - ? 3 - : page === 'questions' - ? 1 - : 0 - } - tabs={[ - { - title: 'Chat', - content: messages ? ( - <GroupChat - messages={messages} - user={user} - group={group} - tips={tips} - /> - ) : ( - <LoadingIndicator /> - ), - href: groupPath(group.slug, 'chat'), - }, - { - title: 'Questions', - content: ( - <div className={'mt-2 px-1'}> - {contracts ? ( - contracts.length > 0 ? ( - <> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search the group's questions" - className="input input-bordered mb-4 w-full" - /> - <ContractsGrid - contracts={query != '' ? filteredContracts : contracts} - hasMore={false} - loadMore={() => {}} - /> - </> - ) : ( - <div className="p-2 text-gray-500"> - No questions yet. Why not{' '} - <SiteLink - href={`/create/?groupId=${group.id}`} - className={'font-bold text-gray-700'} - > - add one? - </SiteLink> - </div> - ) - ) : ( - <LoadingIndicator /> - )} - </div> - ), - href: groupPath(group.slug, 'questions'), - }, - { - title: 'Rankings', - content: leaderboard, - href: groupPath(group.slug, 'rankings'), - }, - { - title: 'About', - content: aboutTab, - href: groupPath(group.slug, 'about'), - }, - ]} + className={'mb-0 sm:mb-2'} + defaultIndex={tabIndex > 0 ? tabIndex : 0} + tabs={tabs} /> </Page> ) @@ -391,7 +391,16 @@ function GroupOverview(props: { username={creator.username} /> </div> - {isCreator && <EditGroupButton className={'ml-1'} group={group} />} + {isCreator ? ( + <EditGroupButton className={'ml-1'} group={group} /> + ) : ( + user && + group.memberIds.includes(user?.id) && ( + <Row> + <JoinOrLeaveGroupButton group={group} /> + </Row> + ) + )} </Row> <div className={'block sm:hidden'}> <Linkify text={group.about} /> @@ -461,12 +470,19 @@ function GroupMemberSearch(props: { group: Group }) { (m) => checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) ) + const matchLimit = 25 + return ( <div> <SearchBar setQuery={setQuery} /> <Col className={'gap-2'}> {matches.length > 0 && ( - <FollowList userIds={matches.map((m) => m.id)} /> + <FollowList userIds={matches.slice(0, matchLimit).map((m) => m.id)} /> + )} + {matches.length > 25 && ( + <div className={'text-center'}> + And {matches.length - matchLimit} more... + </div> )} </Col> </div> @@ -475,25 +491,21 @@ function GroupMemberSearch(props: { group: Group }) { export function GroupMembersList(props: { group: Group }) { const { group } = props - const members = useMembers(group) - const maxMambersToShow = 5 + const members = useMembers(group).filter((m) => m.id !== group.creatorId) + const maxMembersToShow = 3 if (group.memberIds.length === 1) return <div /> return ( - <div> - <div> - <div className="text-neutral flex flex-wrap gap-1"> - <span className={'text-gray-500'}>Other members</span> - {members.slice(0, maxMambersToShow).map((member, i) => ( - <div key={member.id} className={'flex-shrink'}> - <UserLink name={member.name} username={member.username} /> - {members.length > 1 && i !== members.length - 1 && <span>,</span>} - </div> - ))} - {members.length > maxMambersToShow && ( - <span> & {members.length - maxMambersToShow} more</span> - )} + <div className="text-neutral flex flex-wrap gap-1"> + <span className={'text-gray-500'}>Other members</span> + {members.slice(0, maxMembersToShow).map((member, i) => ( + <div key={member.id} className={'flex-shrink'}> + <UserLink name={member.name} username={member.username} /> + {members.length > 1 && i !== members.length - 1 && <span>,</span>} </div> - </div> + ))} + {members.length > maxMembersToShow && ( + <span> & {members.length - maxMembersToShow} more</span> + )} </div> ) } @@ -503,8 +515,9 @@ function SortedLeaderboard(props: { scoreFunction: (user: User) => number title: string header: string + maxToShow?: number }) { - const { users, scoreFunction, title, header } = props + const { users, scoreFunction, title, header, maxToShow } = props const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a)) return ( <Leaderboard @@ -514,6 +527,7 @@ function SortedLeaderboard(props: { columns={[ { header, renderCell: (user) => formatMoney(scoreFunction(user)) }, ]} + maxToShow={maxToShow} /> ) } @@ -528,7 +542,7 @@ function GroupLeaderboards(props: { }) { const { traderScores, creatorScores, members, topTraders, topCreators } = props - + const maxToShow = 50 // Consider hiding M$0 // If it's just one member (curator), show all bettors, otherwise just show members return ( @@ -541,12 +555,14 @@ function GroupLeaderboards(props: { scoreFunction={(user) => traderScores[user.id] ?? 0} title="🏅 Bettor rankings" header="Profit" + maxToShow={maxToShow} /> <SortedLeaderboard users={members} scoreFunction={(user) => creatorScores[user.id] ?? 0} title="🏅 Creator rankings" header="Market volume" + maxToShow={maxToShow} /> </> ) : ( @@ -561,6 +577,7 @@ function GroupLeaderboards(props: { renderCell: (user) => formatMoney(traderScores[user.id] ?? 0), }, ]} + maxToShow={maxToShow} /> <Leaderboard className="max-w-xl" @@ -573,6 +590,7 @@ function GroupLeaderboards(props: { formatMoney(creatorScores[user.id] ?? 0), }, ]} + maxToShow={maxToShow} /> </> )} @@ -586,7 +604,7 @@ function AddContractButton(props: { group: Group; user: User }) { const [open, setOpen] = useState(false) async function addContractToCurrentGroup(contract: Contract) { - await addContractToGroup(group, contract.id) + await addContractToGroup(group, contract) setOpen(false) } diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index ae64cc76..8f2fe424 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -7,7 +7,6 @@ import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' import { Title } from 'web/components/title' -import { UserLink } from 'web/components/user-page' import { useGroups, useMemberGroupIds } from 'web/hooks/use-group' import { useUser } from 'web/hooks/use-user' import { groupPath, listAllGroups } from 'web/lib/firebase/groups' @@ -17,6 +16,8 @@ import { GroupMembersList } from 'web/pages/group/[...slugs]' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { SiteLink } from 'web/components/site-link' import clsx from 'clsx' +import { Avatar } from 'web/components/avatar' +import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -92,79 +93,75 @@ export default function Groups(props: { return ( <Page> <Col className="items-center"> - <Col className="w-full max-w-xl"> - <Col className="px-4 sm:px-0"> - <Row className="items-center justify-between"> - <Title text="Explore groups" /> - {user && ( - <CreateGroupButton user={user} goToGroupOnSubmit={true} /> - )} - </Row> + <Col className="w-full max-w-2xl px-4 sm:px-2"> + <Row className="items-center justify-between"> + <Title text="Explore groups" /> + {user && <CreateGroupButton user={user} goToGroupOnSubmit={true} />} + </Row> - <div className="mb-6 text-gray-500"> - Discuss and compete on questions with a group of friends. - </div> + <div className="mb-6 text-gray-500"> + Discuss and compete on questions with a group of friends. + </div> - <Tabs - currentPageForAnalytics={'groups'} - tabs={[ - ...(user && memberGroupIds.length > 0 - ? [ - { - title: 'My Groups', - content: ( - <Col> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search your groups" - className="input input-bordered mb-4 w-full" - /> - - <Col className="gap-4"> - {matchesOrderedByRecentActivity - .filter((match) => - memberGroupIds.includes(match.id) - ) - .map((group) => ( - <GroupCard - key={group.id} - group={group} - creator={creatorsDict[group.creatorId]} - /> - ))} - </Col> - </Col> - ), - }, - ] - : []), - { - title: 'All', - content: ( - <Col> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search groups" - className="input input-bordered mb-4 w-full" - /> - - <Col className="gap-4"> - {matches.map((group) => ( - <GroupCard - key={group.id} - group={group} - creator={creatorsDict[group.creatorId]} + <Tabs + currentPageForAnalytics={'groups'} + tabs={[ + ...(user && memberGroupIds.length > 0 + ? [ + { + title: 'My Groups', + content: ( + <Col> + <input + type="text" + onChange={(e) => debouncedQuery(e.target.value)} + placeholder="Search your groups" + className="input input-bordered mb-4 w-full" /> - ))} - </Col> - </Col> - ), - }, - ]} - /> - </Col> + + <div className="flex flex-wrap justify-center gap-4"> + {matchesOrderedByRecentActivity + .filter((match) => + memberGroupIds.includes(match.id) + ) + .map((group) => ( + <GroupCard + key={group.id} + group={group} + creator={creatorsDict[group.creatorId]} + /> + ))} + </div> + </Col> + ), + }, + ] + : []), + { + title: 'All', + content: ( + <Col> + <input + type="text" + onChange={(e) => debouncedQuery(e.target.value)} + placeholder="Search groups" + className="input input-bordered mb-4 w-full" + /> + + <div className="flex flex-wrap justify-center gap-4"> + {matches.map((group) => ( + <GroupCard + key={group.id} + group={group} + creator={creatorsDict[group.creatorId]} + /> + ))} + </div> + </Col> + ), + }, + ]} + /> </Col> </Col> </Page> @@ -176,32 +173,33 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) { return ( <Col key={group.id} - className="relative gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100" + className="relative min-w-[20rem] max-w-xs gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100" > <Link href={groupPath(group.slug)}> - <a className="absolute left-0 right-0 top-0 bottom-0" /> + <a className="absolute left-0 right-0 top-0 bottom-0 z-0" /> </Link> + <div> + <Avatar + className={'absolute top-2 right-2'} + username={creator?.username} + avatarUrl={creator?.avatarUrl} + noLink={false} + size={12} + /> + </div> <Row className="items-center justify-between gap-2"> <span className="text-xl">{group.name}</span> </Row> - <div className="flex flex-col items-start justify-start gap-2 text-sm text-gray-500 "> - <Row> - {group.contractIds.length} questions - <div className={'mx-2'}>•</div> - <div className="mr-1">Created by</div> - <UserLink - className="text-neutral" - name={creator?.name ?? ''} - username={creator?.username ?? ''} - /> - </Row> - {group.memberIds.length > 1 && ( - <Row> - <GroupMembersList group={group} /> - </Row> - )} - </div> - <div className="text-sm text-gray-500">{group.about}</div> + <Row>{group.contractIds.length} questions</Row> + <Row className="text-sm text-gray-500"> + <GroupMembersList group={group} /> + </Row> + <Row> + <div className="text-sm text-gray-500">{group.about}</div> + </Row> + <Col className={'mt-2 h-full items-start justify-end'}> + <JoinOrLeaveGroupButton group={group} className={'z-10 w-24'} /> + </Col> </Col> ) } diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 12bd46a2..98d5036e 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -32,7 +32,7 @@ const Home = () => { <ContractSearch querySortOptions={{ shouldLoadFromStorage: true, - defaultSort: getSavedSort() ?? '24-hour-vol', + defaultSort: getSavedSort() ?? 'most-popular', }} onContractClick={(c) => { // Show contract without navigating to contract page. From a4e2cce4aab5232db21788175ed338ee7163a97c Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 13 Jul 2022 14:57:34 -0700 Subject: [PATCH 149/519] initial commit for manalinks UI improvements (#642) * manalinks UI improvements * made manalink look more like card * changed new link to pulsing indigo instead of green --- web/components/button.tsx | 37 +++ web/components/manalink-card.tsx | 41 +++ .../manalinks/create-links-button.tsx | 197 +++++++++++++++ web/components/subtitle.tsx | 15 ++ web/get-manalink-url.ts | 3 + web/pages/links.tsx | 236 ++++-------------- 6 files changed, 340 insertions(+), 189 deletions(-) create mode 100644 web/components/button.tsx create mode 100644 web/components/manalinks/create-links-button.tsx create mode 100644 web/components/subtitle.tsx create mode 100644 web/get-manalink-url.ts diff --git a/web/components/button.tsx b/web/components/button.tsx new file mode 100644 index 00000000..3b59581b --- /dev/null +++ b/web/components/button.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react' +import clsx from 'clsx' + +export default function Button(props: { + className?: string + onClick?: () => void + color: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' + children?: ReactNode + type?: 'button' | 'reset' | 'submit' +}) { + const { + className, + onClick, + children, + color = 'indigo', + type = 'button', + } = props + + return ( + <button + type={type} + className={clsx( + 'font-md items-center justify-center rounded-md border border-transparent px-4 py-2 shadow-sm hover:transition-colors', + color === 'green' && 'btn-primary text-white', + color === 'red' && 'bg-red-400 text-white hover:bg-red-500', + color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500', + color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500', + color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600', + color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300', + className + )} + onClick={onClick} + > + {children} + </button> + ) +} diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index 97f5951c..fec05919 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -67,3 +67,44 @@ export function ManalinkCard(props: { </div> ) } + +export function ManalinkCardPreview(props: { + className?: string + info: ManalinkInfo + defaultMessage: string +}) { + const { className, defaultMessage, info } = props + const { expiresTime, maxUses, uses, amount, message } = info + return ( + <div + className={clsx( + className, + ' group flex flex-col rounded-lg bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all' + )} + > + <Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100"> + <div> + {maxUses != null + ? `${maxUses - uses}/${maxUses} uses left` + : `Unlimited use`} + </div> + <div> + {expiresTime != null + ? `Expires ${fromNow(expiresTime)}` + : 'Never expires'} + </div> + </Col> + + <img + className="my-2 block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12" + src="/logo-white.svg" + /> + <Row className="rounded-b-lg bg-white p-2"> + <Col className="text-md"> + <div className="mb-1 text-indigo-500">{formatMoney(amount)}</div> + <div className="text-xs">{message || defaultMessage}</div> + </Col> + </Row> + </div> + ) +} diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx new file mode 100644 index 00000000..d74980cf --- /dev/null +++ b/web/components/manalinks/create-links-button.tsx @@ -0,0 +1,197 @@ +import clsx from 'clsx' +import { useState } from 'react' +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { Title } from '../title' +import { User } from 'common/user' +import { ManalinkCardPreview, ManalinkInfo } from 'web/components/manalink-card' +import { createManalink } from 'web/lib/firebase/manalinks' +import { Modal } from 'web/components/layout/modal' +import Textarea from 'react-expanding-textarea' +import dayjs from 'dayjs' +import Button from '../button' +import { getManalinkUrl } from 'web/pages/links' +import { DuplicateIcon } from '@heroicons/react/outline' + +export function CreateLinksButton(props: { + user: User + highlightedSlug: string + setHighlightedSlug: (slug: string) => void +}) { + const { user, highlightedSlug, setHighlightedSlug } = props + const [open, setOpen] = useState(false) + + return ( + <> + <Modal open={open} setOpen={(newOpen) => setOpen(newOpen)}> + <Col className="gap-4 rounded-md bg-white px-8 py-6"> + <CreateManalinkForm + highlightedSlug={highlightedSlug} + user={user} + onCreate={async (newManalink) => { + const slug = await createManalink({ + fromId: user.id, + amount: newManalink.amount, + expiresTime: newManalink.expiresTime, + maxUses: newManalink.maxUses, + message: newManalink.message, + }) + setHighlightedSlug(slug || '') + }} + /> + </Col> + </Modal> + + <Button + color={'indigo'} + onClick={() => setOpen(true)} + className={clsx('whitespace-nowrap')} + > + Create a Manalink + </Button> + </> + ) +} + +function CreateManalinkForm(props: { + highlightedSlug: string + user: User + onCreate: (m: ManalinkInfo) => Promise<void> +}) { + const { user, onCreate, highlightedSlug } = props + const [isCreating, setIsCreating] = useState(false) + const [finishedCreating, setFinishedCreating] = useState(false) + const [copyPressed, setCopyPressed] = useState(false) + setTimeout(() => setCopyPressed(false), 300) + + const [newManalink, setNewManalink] = useState<ManalinkInfo>({ + expiresTime: null, + amount: 100, + maxUses: 1, + uses: 0, + message: '', + }) + + return ( + <> + {!finishedCreating && ( + <form + onSubmit={(e) => { + e.preventDefault() + setIsCreating(true) + onCreate(newManalink).finally(() => setIsCreating(false)) + setFinishedCreating(true) + }} + > + <Title className="!my-0" text="Create a Manalink" /> + <div className="flex flex-col flex-wrap gap-x-5 gap-y-2"> + <div className="form-control flex-auto"> + <label className="label">Amount</label> + <input + className="input input-bordered" + type="number" + value={newManalink.amount} + onChange={(e) => + setNewManalink((m) => { + return { ...m, amount: parseInt(e.target.value) } + }) + } + ></input> + </div> + <div className="flex flex-col gap-2 md:flex-row"> + <div className="form-control"> + <label className="label">Uses</label> + <input + className="input input-bordered w-full" + type="number" + value={newManalink.maxUses ?? ''} + onChange={(e) => + setNewManalink((m) => { + return { ...m, maxUses: parseInt(e.target.value) } + }) + } + ></input> + </div> + <div className="form-control"> + <label className="label">Expires in</label> + <input + value={ + newManalink.expiresTime != null + ? dayjs(newManalink.expiresTime).format( + 'YYYY-MM-DDTHH:mm' + ) + : '' + } + className="input input-bordered" + type="datetime-local" + onChange={(e) => { + setNewManalink((m) => { + return { + ...m, + expiresTime: e.target.value + ? dayjs(e.target.value, 'YYYY-MM-DDTHH:mm').valueOf() + : null, + } + }) + }} + ></input> + </div> + </div> + <div className="form-control w-full"> + <label className="label">Message</label> + <Textarea + placeholder={`From ${user.name}`} + className="input input-bordered resize-none" + autoFocus + value={newManalink.message} + rows="3" + onChange={(e) => + setNewManalink((m) => { + return { ...m, message: e.target.value } + }) + } + /> + </div> + </div> + <Button + type="submit" + color={'indigo'} + className={clsx( + 'mt-8 whitespace-nowrap drop-shadow-md', + isCreating ? 'disabled' : '' + )} + > + Create + </Button> + </form> + )} + {finishedCreating && ( + <> + <Title className="!my-0" text="Manalink Created!" /> + <ManalinkCardPreview + className="my-4" + defaultMessage={`From ${user.name}`} + info={newManalink} + /> + <Row + className={clsx( + 'rounded border bg-gray-50 py-2 px-3 text-sm text-gray-500 transition-colors duration-700', + copyPressed ? 'bg-indigo-50 text-indigo-500 transition-none' : '' + )} + > + <div className="w-full select-text truncate"> + {getManalinkUrl(highlightedSlug)} + </div> + <DuplicateIcon + onClick={() => { + navigator.clipboard.writeText(getManalinkUrl(highlightedSlug)) + setCopyPressed(true) + }} + className="my-auto ml-2 h-5 w-5 cursor-pointer transition hover:opacity-50" + /> + </Row> + </> + )} + </> + ) +} diff --git a/web/components/subtitle.tsx b/web/components/subtitle.tsx new file mode 100644 index 00000000..85d19778 --- /dev/null +++ b/web/components/subtitle.tsx @@ -0,0 +1,15 @@ +import clsx from 'clsx' + +export function Subtitle(props: { text: string; className?: string }) { + const { text, className } = props + return ( + <h1 + className={clsx( + 'mt-6 mb-2 inline-block text-lg text-indigo-500 sm:mt-6 sm:mb-2 sm:text-xl', + className + )} + > + {text} + </h1> + ) +} diff --git a/web/get-manalink-url.ts b/web/get-manalink-url.ts new file mode 100644 index 00000000..89a0c8a6 --- /dev/null +++ b/web/get-manalink-url.ts @@ -0,0 +1,3 @@ +export default function getManalinkUrl(slug: string) { + return `${location.protocol}//${location.host}/link/${slug}` +} diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 12cde274..ede997df 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -4,41 +4,29 @@ import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid' import { Claim, Manalink } from 'common/manalink' import { formatMoney } from 'common/util/format' import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' +import { Subtitle } from 'web/components/subtitle' import { useUser } from 'web/hooks/use-user' -import { createManalink, useUserManalinks } from 'web/lib/firebase/manalinks' +import { useUserManalinks } from 'web/lib/firebase/manalinks' import { fromNow } from 'web/lib/util/time' import { useUserById } from 'web/hooks/use-users' import { ManalinkTxn } from 'common/txn' -import { User } from 'common/user' -import { Tabs } from 'web/components/layout/tabs' import { Avatar } from 'web/components/avatar' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { UserLink } from 'web/components/user-page' -import { ManalinkCard, ManalinkInfo } from 'web/components/manalink-card' - -import Textarea from 'react-expanding-textarea' +import { CreateLinksButton } from 'web/components/manalinks/create-links-button' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' dayjs.extend(customParseFormat) -function getLinkUrl(slug: string) { +export function getManalinkUrl(slug: string) { return `${location.protocol}//${location.host}/link/${slug}` } -// TODO: incredibly gross, but the tab component is wrongly designed and -// keeps the tab state inside of itself, so this seems like the only -// way we can tell it to switch tabs from outside after initial render. -function setTabIndex(tabIndex: number) { - const tabHref = document.getElementById(`tab-${tabIndex}`) - if (tabHref) { - tabHref.click() - } -} - export default function LinkPage() { const user = useUser() const links = useUserManalinks(user?.id ?? '') @@ -58,166 +46,31 @@ export default function LinkPage() { <Page> <SEO title="Manalinks" - description="Send mana to anyone via link!" + description="Send M$ to others with a link, even if they don't have a Manifold account yet!" url="/send" /> <Col className="w-full px-8"> - <Title text="Manalinks" /> - <Tabs - labelClassName={'pb-2 pt-1 '} - defaultIndex={0} - tabs={[ - { - title: 'Create a link', - content: ( - <CreateManalinkForm - user={user} - onCreate={async (newManalink) => { - const slug = await createManalink({ - fromId: user.id, - amount: newManalink.amount, - expiresTime: newManalink.expiresTime, - maxUses: newManalink.maxUses, - message: newManalink.message, - }) - setTabIndex(1) - setHighlightedSlug(slug || '') - }} - /> - ), - }, - { - title: 'Unclaimed links', - content: ( - <LinksTable - links={unclaimedLinks} - highlightedSlug={highlightedSlug} - /> - ), - }, - // TODO: we have no use case for this atm and it's also really inefficient - // { - // title: 'Claimed', - // content: <ClaimsList txns={manalinkTxns} />, - // }, - ]} - /> + <Row className="items-center justify-between"> + <Title text="Manalinks" /> + {user && ( + <CreateLinksButton + user={user} + highlightedSlug={highlightedSlug} + setHighlightedSlug={setHighlightedSlug} + /> + )} + </Row> + <p> + You can use manalinks to send mana to other people, even if they + don't yet have a Manifold account. + </p> + <Subtitle text="Your Manalinks" /> + <LinksTable links={unclaimedLinks} highlightedSlug={highlightedSlug} /> </Col> </Page> ) } -function CreateManalinkForm(props: { - user: User - onCreate: (m: ManalinkInfo) => Promise<void> -}) { - const { user, onCreate } = props - const [isCreating, setIsCreating] = useState(false) - const [newManalink, setNewManalink] = useState<ManalinkInfo>({ - expiresTime: null, - amount: 100, - maxUses: 1, - uses: 0, - message: '', - }) - return ( - <> - <p> - You can use manalinks to send mana to other people, even if they - don't yet have a Manifold account. - </p> - <form - className="my-5" - onSubmit={(e) => { - e.preventDefault() - setIsCreating(true) - onCreate(newManalink).finally(() => setIsCreating(false)) - }} - > - <div className="flex flex-row flex-wrap gap-x-5 gap-y-2"> - <div className="form-control flex-auto"> - <label className="label">Amount</label> - <input - className="input" - type="number" - value={newManalink.amount} - onChange={(e) => - setNewManalink((m) => { - return { ...m, amount: parseInt(e.target.value) } - }) - } - ></input> - </div> - <div className="form-control flex-auto"> - <label className="label">Uses</label> - <input - className="input" - type="number" - value={newManalink.maxUses ?? ''} - onChange={(e) => - setNewManalink((m) => { - return { ...m, maxUses: parseInt(e.target.value) } - }) - } - ></input> - </div> - <div className="form-control flex-auto"> - <label className="label">Expires at</label> - <input - value={ - newManalink.expiresTime != null - ? dayjs(newManalink.expiresTime).format('YYYY-MM-DDTHH:mm') - : '' - } - className="input" - type="datetime-local" - onChange={(e) => { - setNewManalink((m) => { - return { - ...m, - expiresTime: e.target.value - ? dayjs(e.target.value, 'YYYY-MM-DDTHH:mm').valueOf() - : null, - } - }) - }} - ></input> - </div> - </div> - <div className="form-control w-full"> - <label className="label">Message</label> - <Textarea - placeholder={`From ${user.name}`} - className="input input-bordered resize-none" - autoFocus - value={newManalink.message} - onChange={(e) => - setNewManalink((m) => { - return { ...m, message: e.target.value } - }) - } - /> - </div> - <button - type="submit" - className={clsx('btn mt-5', isCreating ? 'loading disabled' : '')} - > - {isCreating ? '' : 'Create'} - </button> - </form> - - <Title text="Preview" /> - <p>This is what the person you send the link to will see:</p> - <ManalinkCard - className="my-5" - defaultMessage={`From ${user.name}`} - info={newManalink} - isClaiming={false} - /> - </> - ) -} - export function ClaimsList(props: { txns: ManalinkTxn[] }) { const { txns } = props return ( @@ -334,8 +187,8 @@ function LinkSummaryRow(props: { }) { const { link, highlight, expanded, onToggle } = props const className = clsx( - 'whitespace-nowrap text-sm hover:cursor-pointer', - highlight ? 'bg-primary' : 'text-gray-500 hover:bg-sky-50 bg-white' + 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white', + highlight ? 'bg-indigo-100 rounded-lg animate-pulse' : '' ) return ( <tr id={link.slug} key={link.slug} className={className}> @@ -350,7 +203,7 @@ function LinkSummaryRow(props: { <td className="px-5 py-4 font-medium text-gray-900"> {formatMoney(link.amount)} </td> - <td className="px-5 py-4">{getLinkUrl(link.slug)}</td> + <td className="px-5 py-4">{getManalinkUrl(link.slug)}</td> <td className="px-5 py-4">{link.claimedUserIds.length}</td> <td className="px-5 py-4">{link.maxUses == null ? '∞' : link.maxUses}</td> <td className="px-5 py-4"> @@ -365,22 +218,27 @@ function LinksTable(props: { links: Manalink[]; highlightedSlug?: string }) { return links.length == 0 ? ( <p>You don't currently have any outstanding manalinks.</p> ) : ( - <table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200"> - <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> - <tr> - <th></th> - <th className="px-5 py-3.5">Amount</th> - <th className="px-5 py-3.5">Link</th> - <th className="px-5 py-3.5">Uses</th> - <th className="px-5 py-3.5">Max Uses</th> - <th className="px-5 py-3.5">Expires</th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200 bg-white"> - {links.map((link) => ( - <LinkTableRow link={link} highlight={link.slug === highlightedSlug} /> - ))} - </tbody> - </table> + <div className="overflow-scroll"> + <table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200"> + <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> + <tr> + <th></th> + <th className="px-5 py-3.5">Amount</th> + <th className="px-5 py-3.5">Link</th> + <th className="px-5 py-3.5">Uses</th> + <th className="px-5 py-3.5">Max Uses</th> + <th className="px-5 py-3.5">Expires</th> + </tr> + </thead> + <tbody className="divide-y divide-gray-200 bg-white"> + {links.map((link) => ( + <LinkTableRow + link={link} + highlight={link.slug === highlightedSlug} + /> + ))} + </tbody> + </table> + </div> ) } From f08d6bda930bae2f7071edec4d9d57a8a723f64a Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 15:14:06 -0700 Subject: [PATCH 150/519] when adding package, don't put ^ before version (#645) --- common/.yarnrc | 1 + functions/.yarnrc | 1 + web/.yarnrc | 1 + 3 files changed, 3 insertions(+) create mode 100644 common/.yarnrc create mode 100644 functions/.yarnrc create mode 100644 web/.yarnrc diff --git a/common/.yarnrc b/common/.yarnrc new file mode 100644 index 00000000..fdd705c6 --- /dev/null +++ b/common/.yarnrc @@ -0,0 +1 @@ +save-prefix "" diff --git a/functions/.yarnrc b/functions/.yarnrc new file mode 100644 index 00000000..fdd705c6 --- /dev/null +++ b/functions/.yarnrc @@ -0,0 +1 @@ +save-prefix "" diff --git a/web/.yarnrc b/web/.yarnrc new file mode 100644 index 00000000..fdd705c6 --- /dev/null +++ b/web/.yarnrc @@ -0,0 +1 @@ +save-prefix "" From 7a49549389ed36b19dc81a8fde3495009efeb36d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 16:20:56 -0600 Subject: [PATCH 151/519] Ignore rankings/members for huge groups for now --- web/components/contract-search.tsx | 5 +- web/hooks/use-group.ts | 12 +++-- web/pages/group/[...slugs]/index.tsx | 81 +++++++++------------------- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 834a4db1..952d4034 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -57,6 +57,7 @@ export function ContractSearch(props: { creatorId?: string tag?: string excludeContractIds?: string[] + groupSlug?: string } onContractClick?: (contract: Contract) => void showPlaceHolder?: boolean @@ -79,7 +80,6 @@ export function ContractSearch(props: { ?.map((g) => g.slug) .filter((s) => !NEW_USER_GROUP_SLUGS.includes(s)) const follows = useFollows(user?.id) - console.log(memberGroupSlugs, follows) const { initialSort } = useInitialQueryAndSort(querySortOptions) const sort = sortIndexes @@ -112,6 +112,9 @@ export function ContractSearch(props: { additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` : '', + additionalFilter?.groupSlug + ? `groupSlugs:${additionalFilter.groupSlug}` + : '', ].filter((f) => f) // Hack to make Algolia work. filters = ['', ...filters] diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 1fde9f4e..8af2183e 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -75,19 +75,21 @@ export const useMemberGroupIds = (user: User | null | undefined) => { return memberGroupIds } -export function useMembers(group: Group) { +export function useMembers(group: Group, max?: number) { const [members, setMembers] = useState<User[]>([]) useEffect(() => { const { memberIds } = group if (memberIds.length > 0) { - listMembers(group).then((members) => setMembers(members)) + listMembers(group, max).then((members) => setMembers(members)) } - }, [group]) + }, [group, max]) return members } -export async function listMembers(group: Group) { - return await Promise.all(group.memberIds.map(getUser)) +export async function listMembers(group: Group, max?: number) { + return await Promise.all( + group.memberIds.slice(0, max ? max : group.memberIds.length).map(getUser) + ) } export const useGroupsWithContract = (contractId: string | undefined) => { diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index fc76df48..91e6f998 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -7,10 +7,10 @@ import { Contract } from 'web/lib/firebase/contracts' import { groupPath, getGroupBySlug, - getGroupContracts, updateGroup, addUserToGroup, addContractToGroup, + getGroupContracts, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' @@ -33,7 +33,6 @@ import { SEO } from 'web/components/SEO' import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' -import { ContractsGrid } from 'web/components/contract/contracts-list' import { createButtonStyle, CreateQuestionButton, @@ -42,13 +41,15 @@ import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' -import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' +import { + checkAgainstQuery, + getSavedSort, +} from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' import { ShareIconButton } from 'web/components/share-icon-button' import { REFERRAL_AMOUNT } from 'common/user' -import { SiteLink } from 'web/components/site-link' import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' @@ -61,10 +62,14 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const { slugs } = props.params const group = await getGroupBySlug(slugs[0]) - const members = group ? await listMembers(group) : [] + const members = + group && group.memberIds.length < 100 ? await listMembers(group) : [] const creatorPromise = group ? getUser(group.creatorId) : null - const contracts = group ? await getGroupContracts(group).catch((_) => []) : [] + const contracts = + group && group.contractIds.length < 100 + ? await getGroupContracts(group).catch((_) => []) + : [] const bets = await Promise.all( contracts.map((contract: Contract) => listAllBets(contract.id)) @@ -149,26 +154,9 @@ export default function GroupPage(props: { const page = slugs?.[1] as typeof groupSubpages[number] const group = useGroup(props.group?.id) ?? props.group - const [contracts, setContracts] = useState<Contract[] | undefined>(undefined) - const [query, setQuery] = useState('') const tips = useTipTxns({ groupId: group?.id }) const messages = useCommentsOnGroup(group?.id) - const debouncedQuery = debounce(setQuery, 50) - const filteredContracts = - query != '' && contracts - ? contracts.filter( - (c) => - checkAgainstQuery(query, c.question) || - checkAgainstQuery(query, c.creatorName) || - checkAgainstQuery(query, c.creatorUsername) - ) - : [] - - useEffect(() => { - if (group) - getGroupContracts(group).then((contracts) => setContracts(contracts)) - }, [group]) const user = useUser() useEffect(() => { @@ -237,37 +225,14 @@ export default function GroupPage(props: { { title: 'Questions', content: ( - <div className={'mt-2 px-1'}> - {contracts ? ( - contracts.length > 0 ? ( - <> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search the group's questions" - className="input input-bordered mb-4 w-full" - /> - <ContractsGrid - contracts={query != '' ? filteredContracts : contracts} - hasMore={false} - loadMore={() => {}} - /> - </> - ) : ( - <div className="p-2 text-gray-500"> - No questions yet. Why not{' '} - <SiteLink - href={`/create/?groupId=${group.id}`} - className={'font-bold text-gray-700'} - > - add one? - </SiteLink> - </div> - ) - ) : ( - <LoadingIndicator /> - )} - </div> + <ContractSearch + querySortOptions={{ + shouldLoadFromStorage: true, + defaultSort: getSavedSort() ?? 'newest', + defaultFilter: 'open', + }} + additionalFilter={{ groupSlug: group.slug }} + /> ), href: groupPath(group.slug, 'questions'), }, @@ -491,8 +456,10 @@ function GroupMemberSearch(props: { group: Group }) { export function GroupMembersList(props: { group: Group }) { const { group } = props - const members = useMembers(group).filter((m) => m.id !== group.creatorId) const maxMembersToShow = 3 + const members = useMembers(group, maxMembersToShow).filter( + (m) => m.id !== group.creatorId + ) if (group.memberIds.length === 1) return <div /> return ( <div className="text-neutral flex flex-wrap gap-1"> @@ -503,8 +470,8 @@ export function GroupMembersList(props: { group: Group }) { {members.length > 1 && i !== members.length - 1 && <span>,</span>} </div> ))} - {members.length > maxMembersToShow && ( - <span> & {members.length - maxMembersToShow} more</span> + {group.memberIds.length > maxMembersToShow && ( + <span> & {group.memberIds.length - maxMembersToShow} more</span> )} </div> ) From e1b6619e9c0a593db8ec0f06d6027b172fb4c9cc Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 13 Jul 2022 17:22:22 -0500 Subject: [PATCH 152/519] embeds: don't show bet button after resolution --- web/pages/embed/[username]/[contractSlug].tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index dc8cb51d..57189c0c 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -22,6 +22,7 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz' import { useWindowSize } from 'web/hooks/use-window-size' import { listAllBets } from 'web/lib/firebase/bets' import { contractPath, getContractFromSlug } from 'web/lib/firebase/contracts' +import { tradingAllowed } from 'web/lib/firebase/contracts' import Custom404 from '../../404' export const getStaticProps = fromPropz(getStaticPropz) @@ -112,17 +113,21 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { {isBinary && ( <Row className="items-center gap-4"> - <BetRow - contract={contract as CPMMBinaryContract} - betPanelClassName="scale-75" - /> + {tradingAllowed(contract) && ( + <BetRow + contract={contract as CPMMBinaryContract} + betPanelClassName="scale-75" + /> + )} <BinaryResolutionOrChance contract={contract} /> </Row> )} {isPseudoNumeric && ( <Row className="items-center gap-4"> - <BetRow contract={contract} betPanelClassName="scale-75" /> + {tradingAllowed(contract) && ( + <BetRow contract={contract} betPanelClassName="scale-75" /> + )} <PseudoNumericResolutionOrExpectation contract={contract} /> </Row> )} From 45fb3803c1c31ed45724fb52d14e4b67111deede Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 13 Jul 2022 16:24:35 -0600 Subject: [PATCH 153/519] Limit member search to 100 --- web/pages/group/[...slugs]/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 91e6f998..43647cdb 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -428,7 +428,8 @@ function SearchBar(props: { setQuery: (query: string) => void }) { function GroupMemberSearch(props: { group: Group }) { const [query, setQuery] = useState('') - const members = useMembers(props.group) + const { group } = props + const members = useMembers(group, 100) // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( From 664e55a40bc3369445ff2753ce82228e219c980a Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 15:56:15 -0700 Subject: [PATCH 154/519] Add typing, pasting links (#646) --- common/package.json | 1 + common/util/parse.ts | 2 ++ functions/package.json | 1 + web/components/editor.tsx | 10 ++++++++-- web/package.json | 1 + yarn.lock | 14 ++++++++++++++ 6 files changed, 27 insertions(+), 2 deletions(-) diff --git a/common/package.json b/common/package.json index 25992cb6..6f0f5b29 100644 --- a/common/package.json +++ b/common/package.json @@ -9,6 +9,7 @@ "sideEffects": false, "dependencies": { "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/starter-kit": "2.0.0-beta.190", "lodash": "4.17.21" }, diff --git a/common/util/parse.ts b/common/util/parse.ts index 48b68fdd..94b5ab7f 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -19,6 +19,7 @@ import { Strike } from '@tiptap/extension-strike' import { Text } from '@tiptap/extension-text' // other tiptap extensions import { Image } from '@tiptap/extension-image' +import { Link } from '@tiptap/extension-link' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -68,6 +69,7 @@ export const exhibitExts = [ Text, Image, + Link, ] // export const exhibitExts = [StarterKit as unknown as Extension, Image] diff --git a/functions/package.json b/functions/package.json index d7ebb663..f8657516 100644 --- a/functions/package.json +++ b/functions/package.json @@ -26,6 +26,7 @@ "@google-cloud/functions-framework": "3.1.2", "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/starter-kit": "2.0.0-beta.190", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", diff --git a/web/components/editor.tsx b/web/components/editor.tsx index bd4d97c0..3ec0663b 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -10,6 +10,7 @@ import { } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' +import { Link } from '@tiptap/extension-link' import clsx from 'clsx' import { useEffect } from 'react' import { Linkify } from './linkify' @@ -18,8 +19,12 @@ import { useMutation } from 'react-query' import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' -const proseClass = - 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' +const proseClass = clsx( + 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none', + // link styles mostly copied from site-link.ts + 'prose-a:no-underline prose-a:!text-indigo-700', + 'prose-a:z-10 prose-a:break-words hover:prose-a:underline hover:prose-a:decoration-indigo-400 prose-a:hover:decoration-2' +) export function useTextEditor(props: { placeholder?: string @@ -47,6 +52,7 @@ export function useTextEditor(props: { }), CharacterCount.configure({ limit: max }), Image, + Link, ], content: defaultValue, }) diff --git a/web/package.json b/web/package.json index f81950bf..f8e1881b 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,7 @@ "@react-query-firebase/firestore": "0.4.2", "@tiptap/extension-character-count": "2.0.0-beta.31", "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-placeholder": "2.0.0-beta.53", "@tiptap/react": "2.0.0-beta.114", "@tiptap/starter-kit": "2.0.0-beta.190", diff --git a/yarn.lock b/yarn.lock index e84f3616..6fcdf53a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2984,6 +2984,15 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.28.tgz#bf88ecae64c8f2f69f1f508b802c1efd7454a84e" integrity sha512-/pKRiCfewh7nqiXRD3N4hQHfGrGNOiWPFYZfY35bSpvTms7PDb/MF7xT1CWW23hSpY31BBS+R/a66vlR/gqu7Q== +"@tiptap/extension-link@2.0.0-beta.43": + version "2.0.0-beta.43" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.43.tgz#c123a2170dd50d075b9fe7fb91d86d23f778ffb0" + integrity sha512-AYueqfTW713KGVfWSWhVbj4ObeWudgawikm3m0uYcKSdsAz/CfEvOD2/NA0uyQzlxmYLA6Pf8HMxoKGN+O4Cmg== + dependencies: + linkifyjs "^3.0.5" + prosemirror-model "1.18.1" + prosemirror-state "1.4.1" + "@tiptap/extension-list-item@^2.0.0-beta.23": version "2.0.0-beta.23" resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843" @@ -7769,6 +7778,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkifyjs@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.5.tgz#99e51a3a0c0e232fcb63ebb89eea3ff923378f34" + integrity sha512-1Y9XQH65eQKA9p2xtk+zxvnTeQBG7rdAXSkUG97DmuI/Xhji9uaUzaWxRj6rf9YC0v8KKHkxav7tnLX82Sz5Fg== + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" From 98192ee58069ed0bbd823753e4c503da82910fef Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 16:12:19 -0700 Subject: [PATCH 155/519] simplify rich text link styles --- web/components/editor.tsx | 6 ++---- web/components/site-link.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 3ec0663b..41c2b80a 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -18,12 +18,10 @@ import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' +import { linkClass } from './site-link' const proseClass = clsx( 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none', - // link styles mostly copied from site-link.ts - 'prose-a:no-underline prose-a:!text-indigo-700', - 'prose-a:z-10 prose-a:break-words hover:prose-a:underline hover:prose-a:decoration-indigo-400 prose-a:hover:decoration-2' ) export function useTextEditor(props: { @@ -52,7 +50,7 @@ export function useTextEditor(props: { }), CharacterCount.configure({ limit: max }), Image, - Link, + Link.configure({ HTMLAttributes: { class: clsx('no-underline !text-indigo-700', linkClass)}}), ], content: defaultValue, }) diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index 8137eb08..ee12d519 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -2,6 +2,9 @@ import clsx from 'clsx' import { ReactNode } from 'react' import Link from 'next/link' +export const linkClass = + 'z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2' + export const SiteLink = (props: { href: string children?: ReactNode @@ -13,10 +16,7 @@ export const SiteLink = (props: { return ( <MaybeLink href={href}> <a - className={clsx( - 'z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2', - className - )} + className={clsx(linkClass, className)} href={href} target={href.startsWith('http') ? '_blank' : undefined} style={{ /* For iOS safari */ wordBreak: 'break-word' }} From 9240cd3d1c533402f0431d7ca5fb78bc8781c6d0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 18:23:03 -0500 Subject: [PATCH 156/519] Bet panel: Quick vs Limit pill buttons. Also, pill buttons for Yes vs No. --- web/components/bet-panel.tsx | 67 +++++++++++++++++--------- web/components/buttons/pill-button.tsx | 27 +++++++++++ web/components/yes-no-selector.tsx | 62 ------------------------ 3 files changed, 71 insertions(+), 85 deletions(-) create mode 100644 web/components/buttons/pill-button.tsx diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index c8356a06..bd2bfebb 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -8,7 +8,6 @@ import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' -import { YesNoSelector } from './yes-no-selector' import { formatMoney, formatMoneyWithDecimals, @@ -41,6 +40,7 @@ import { removeUndefinedProps } from 'common/util/object' import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBets } from './limit-bets' import { BucketInput } from './bucket-input' +import { PillButton } from './buttons/pill-button' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -54,10 +54,6 @@ export function BetPanel(props: { const { sharesOutcome } = useSaveBinaryShares(contract, userBets) const [isLimitOrder, setIsLimitOrder] = useState(false) - const toggleLimitOrder = () => { - setIsLimitOrder(!isLimitOrder) - track('toggle limit order') - } const showLimitOrders = (isLimitOrder && unfilledBets.length > 0) || yourUnfilledBets.length > 0 @@ -71,21 +67,33 @@ export function BetPanel(props: { /> <Col className={clsx( - 'relative rounded-b-md bg-white px-8 py-6', + 'relative rounded-b-md bg-white px-6 py-6', !sharesOutcome && 'rounded-t-md', className )} > - <Row className="align-center justify-between"> - <div className="mb-6 text-2xl"> - {isLimitOrder ? <>Limit order</> : <>Place your bet</>} - </div> - <button - className="btn btn-ghost btn-sm text-sm normal-case" - onClick={toggleLimitOrder} - > - <SwitchHorizontalIcon className="inline h-6 w-6" /> - </button> + <Row className="align-center mb-4 justify-between"> + <div className="text-4xl">Bet</div> + <Row className="mt-2 items-center gap-2"> + <PillButton + selected={!isLimitOrder} + onSelect={() => { + setIsLimitOrder(false) + track('select quick order') + }} + > + Quick + </PillButton> + <PillButton + selected={isLimitOrder} + onSelect={() => { + setIsLimitOrder(true) + track('select limit order') + }} + > + Limit + </PillButton> + </Row> </Row> <BuyPanel @@ -287,13 +295,26 @@ function BuyPanel(props: { return ( <> - <YesNoSelector - className="mb-4" - btnClassName="flex-1" - selected={betChoice} - onSelect={(choice) => onBetChoice(choice)} - isPseudoNumeric={isPseudoNumeric} - /> + <div className="my-3 text-left text-sm text-gray-500">Direction</div> + <Row className="mb-4 items-center gap-2"> + <PillButton + selected={betChoice === 'YES'} + onSelect={() => onBetChoice('YES')} + big + color="bg-primary" + > + {isPseudoNumeric ? 'Higher' : 'Yes'} + </PillButton> + <PillButton + selected={betChoice === 'NO'} + onSelect={() => onBetChoice('NO')} + big + color="bg-red-400" + > + {isPseudoNumeric ? 'Lower' : 'No'} + </PillButton> + </Row> + <div className="my-3 text-left text-sm text-gray-500">Amount</div> <BuyAmountInput inputClassName="w-full max-w-none" diff --git a/web/components/buttons/pill-button.tsx b/web/components/buttons/pill-button.tsx new file mode 100644 index 00000000..796036d1 --- /dev/null +++ b/web/components/buttons/pill-button.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx' +import { ReactNode } from 'react' + +export function PillButton(props: { + selected: boolean + onSelect: () => void + color?: string + big?: boolean + children: ReactNode +}) { + const { children, selected, onSelect, color, big } = props + + return ( + <button + className={clsx( + 'cursor-pointer select-none rounded-full', + selected + ? ['text-white', color ?? 'bg-gray-700'] + : 'bg-gray-100 hover:bg-gray-200', + big ? 'px-8 py-2' : 'px-3 py-1.5 text-sm' + )} + onClick={onSelect} + > + {children} + </button> + ) +} diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index 3b3cc21d..dda97c0c 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -5,68 +5,6 @@ import { Col } from './layout/col' import { Row } from './layout/row' import { resolution } from 'common/contract' -export function YesNoSelector(props: { - selected?: 'YES' | 'NO' - onSelect: (selected: 'YES' | 'NO') => void - className?: string - btnClassName?: string - replaceYesButton?: React.ReactNode - replaceNoButton?: React.ReactNode - isPseudoNumeric?: boolean -}) { - const { - selected, - onSelect, - className, - btnClassName, - replaceNoButton, - replaceYesButton, - isPseudoNumeric, - } = props - - const commonClassNames = - 'inline-flex items-center justify-center rounded-3xl border-2 p-2' - - return ( - <Row className={clsx('space-x-3', className)}> - {replaceYesButton ? ( - replaceYesButton - ) : ( - <button - className={clsx( - commonClassNames, - 'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white', - selected == 'YES' - ? 'bg-primary text-white' - : 'text-primary bg-transparent', - btnClassName - )} - onClick={() => onSelect('YES')} - > - {isPseudoNumeric ? 'HIGHER' : 'YES'} - </button> - )} - {replaceNoButton ? ( - replaceNoButton - ) : ( - <button - className={clsx( - commonClassNames, - 'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white', - selected == 'NO' - ? 'bg-red-400 text-white' - : 'bg-transparent text-red-400', - btnClassName - )} - onClick={() => onSelect('NO')} - > - {isPseudoNumeric ? 'LOWER' : 'NO'} - </button> - )} - </Row> - ) -} - export function YesNoCancelSelector(props: { selected: resolution | undefined onSelect: (selected: resolution) => void From 67b345092400005784180c5f43dd5a82c51621a2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 18:28:33 -0500 Subject: [PATCH 157/519] Use quick vs limit bet in mobile dialog --- web/components/bet-panel.tsx | 79 +++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index bd2bfebb..26d2d974 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,7 +1,6 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' import { partition, sum, sumBy } from 'lodash' -import { SwitchHorizontalIcon } from '@heroicons/react/solid' import { useUser } from 'web/hooks/use-user' import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' @@ -72,30 +71,10 @@ export function BetPanel(props: { className )} > - <Row className="align-center mb-4 justify-between"> - <div className="text-4xl">Bet</div> - <Row className="mt-2 items-center gap-2"> - <PillButton - selected={!isLimitOrder} - onSelect={() => { - setIsLimitOrder(false) - track('select quick order') - }} - > - Quick - </PillButton> - <PillButton - selected={isLimitOrder} - onSelect={() => { - setIsLimitOrder(true) - track('select limit order') - }} - > - Limit - </PillButton> - </Row> - </Row> - + <QuickOrLimitBet + isLimitOrder={isLimitOrder} + setIsLimitOrder={setIsLimitOrder} + /> <BuyPanel contract={contract} user={user} @@ -142,19 +121,10 @@ export function SimpleBetPanel(props: { 'rounded-b-md bg-white px-8 py-6' )} > - <Row className="justify-between"> - <div className="mb-6 text-2xl"> - {isLimitOrder ? <>Limit order</> : <>Place your bet</>} - </div> - - <button - className="btn btn-ghost btn-sm text-sm normal-case" - onClick={() => setIsLimitOrder(!isLimitOrder)} - > - <SwitchHorizontalIcon className="inline h-6 w-6" /> - </button> - </Row> - + <QuickOrLimitBet + isLimitOrder={isLimitOrder} + setIsLimitOrder={setIsLimitOrder} + /> <BuyPanel contract={contract} user={user} @@ -427,6 +397,39 @@ function BuyPanel(props: { ) } +function QuickOrLimitBet(props: { + isLimitOrder: boolean + setIsLimitOrder: (isLimitOrder: boolean) => void +}) { + const { isLimitOrder, setIsLimitOrder } = props + + return ( + <Row className="align-center mb-4 justify-between"> + <div className="text-4xl">Bet</div> + <Row className="mt-2 items-center gap-2"> + <PillButton + selected={!isLimitOrder} + onSelect={() => { + setIsLimitOrder(false) + track('select quick order') + }} + > + Quick + </PillButton> + <PillButton + selected={isLimitOrder} + onSelect={() => { + setIsLimitOrder(true) + track('select limit order') + }} + > + Limit + </PillButton> + </Row> + </Row> + ) +} + export function SellPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract userBets: Bet[] From f4b7b9efd0901ea6fe45e565855d43be0c0f7f61 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 13 Jul 2022 18:39:32 -0500 Subject: [PATCH 158/519] Only show probabilty update with arrow if probability changes --- web/components/bet-panel.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 26d2d974..8343d696 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -250,6 +250,9 @@ function BuyPanel(props: { ) const resultProb = getCpmmProbability(newPool, newP) + const probStayedSame = + formatPercent(resultProb) === formatPercent(initialProb) + const remainingMatched = isLimitOrder ? ((newBet.orderAmount ?? 0) - newBet.amount) / (betChoice === 'YES' ? limitProbFrac : 1 - limitProbFrac) @@ -339,11 +342,15 @@ function BuyPanel(props: { <div className="text-gray-500"> {isPseudoNumeric ? 'Estimated value' : 'Probability'} </div> - <div> - {format(initialProb)} - <span className="mx-2">→</span> - {format(resultProb)} - </div> + {probStayedSame ? ( + <div>{format(initialProb)}</div> + ) : ( + <div> + {format(initialProb)} + <span className="mx-2">→</span> + {format(resultProb)} + </div> + )} </Row> )} From 095af10d4fb4bc607a7a37e04cd3faf6dacdeeb7 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 16:50:08 -0700 Subject: [PATCH 159/519] replace raw checkbox w/ Checkbox component also run prettier --- web/components/checkbox.tsx | 6 ++++-- web/components/editor.tsx | 8 ++++++-- web/pages/create.tsx | 17 ++++++++--------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/web/components/checkbox.tsx b/web/components/checkbox.tsx index 9cc7cae9..22a867b2 100644 --- a/web/components/checkbox.tsx +++ b/web/components/checkbox.tsx @@ -5,12 +5,13 @@ export function Checkbox(props: { checked: boolean toggle: (checked: boolean) => void className?: string + disabled?: boolean }) { - const { label, checked, toggle, className } = props + const { label, checked, toggle, className, disabled } = props return ( <div className={clsx(className, 'space-y-5')}> - <div className="relative flex items-start"> + <div className="relative flex items-center"> <div className="flex h-6 items-center"> <input id={label} @@ -18,6 +19,7 @@ export function Checkbox(props: { className="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" checked={checked} onChange={(e) => toggle(!e.target.checked)} + disabled={disabled} /> </div> <div className="ml-3"> diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 41c2b80a..4b3e2cce 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,7 +21,7 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' const proseClass = clsx( - 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none', + 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' ) export function useTextEditor(props: { @@ -50,7 +50,11 @@ export function useTextEditor(props: { }), CharacterCount.configure({ limit: max }), Image, - Link.configure({ HTMLAttributes: { class: clsx('no-underline !text-indigo-700', linkClass)}}), + Link.configure({ + HTMLAttributes: { + class: clsx('no-underline !text-indigo-700', linkClass), + }, + }), ], content: defaultValue, }) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 7040dff0..8d4c3662 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -27,6 +27,7 @@ import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' import { User } from 'common/user' import { TextEditor, useTextEditor } from 'web/components/editor' +import { Checkbox } from 'web/components/checkbox' type NewQuestionParams = { groupId?: string @@ -294,15 +295,13 @@ export function NewContract(props: { </Row> {!(min !== undefined && min < 0) && ( - <Row className="mt-1 ml-2 mb-2 items-center"> - <span className="mr-2 text-sm">Log scale</span>{' '} - <input - type="checkbox" - checked={isLogScale} - onChange={() => setIsLogScale(!isLogScale)} - disabled={isSubmitting} - /> - </Row> + <Checkbox + className="my-2 text-sm" + label="Log scale" + checked={isLogScale} + toggle={() => setIsLogScale(!isLogScale)} + disabled={isSubmitting} + /> )} {min !== undefined && max !== undefined && min >= max && ( From 5ebd4498a0780f17a9d6f39ed174abf85c7e72c7 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 13 Jul 2022 17:43:20 -0700 Subject: [PATCH 160/519] Remove deprecated useUserById implementation (#571) * Remove duplicate useUserById implementation * fix bug: firebase doesn't accept empty paths --- web/components/charity/feed-items.tsx | 2 +- web/hooks/use-users.ts | 13 ------------- web/pages/[username]/[contractSlug].tsx | 2 +- web/pages/link/[slug].tsx | 4 ++-- web/pages/links.tsx | 2 +- 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/web/components/charity/feed-items.tsx b/web/components/charity/feed-items.tsx index 6e6def00..365aa606 100644 --- a/web/components/charity/feed-items.tsx +++ b/web/components/charity/feed-items.tsx @@ -1,6 +1,6 @@ import { DonationTxn } from 'common/txn' import { Avatar } from '../avatar' -import { useUserById } from 'web/hooks/use-users' +import { useUserById } from 'web/hooks/use-user' import { UserLink } from '../user-page' import { manaToUSD } from '../../../common/util/format' import { RelativeTimestamp } from '../relative-timestamp' diff --git a/web/hooks/use-users.ts b/web/hooks/use-users.ts index 1a527659..1312444e 100644 --- a/web/hooks/use-users.ts +++ b/web/hooks/use-users.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react' import { PrivateUser, User } from 'common/user' import { - getUser, listenForAllUsers, listenForPrivateUsers, } from 'web/lib/firebase/users' @@ -20,18 +19,6 @@ export const useUsers = () => { return users } -export const useUserById = (userId?: string) => { - const [user, setUser] = useState<User | undefined>(undefined) - - useEffect(() => { - if (userId) { - getUser(userId).then(setUser) - } - }, [userId]) - - return user -} - export const usePrivateUsers = () => { const [users, setUsers] = useState<PrivateUser[]>([]) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index bfe13837..0cfbc99f 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -27,7 +27,7 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Leaderboard } from 'web/components/leaderboard' import { resolvedPayout } from 'common/calculate' import { formatMoney } from 'common/util/format' -import { useUserById } from 'web/hooks/use-users' +import { useUserById } from 'web/hooks/use-user' import { ContractTabs } from 'web/components/contract/contract-tabs' import { contractTextDetails } from 'web/components/contract/contract-details' import { useWindowSize } from 'web/hooks/use-window-size' diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 01597a15..8093969b 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -6,7 +6,7 @@ import { claimManalink } from 'web/lib/firebase/api' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' -import { useUserById } from 'web/hooks/use-users' +import { useUserById } from 'web/hooks/use-user' import { firebaseLogin } from 'web/lib/firebase/users' export default function ClaimPage() { @@ -17,7 +17,7 @@ export default function ClaimPage() { const [claiming, setClaiming] = useState(false) const [error, setError] = useState<string | undefined>(undefined) - const fromUser = useUserById(manalink?.fromId) + const fromUser = useUserById(manalink?.fromId ?? '_loading') if (!manalink) { return <></> } diff --git a/web/pages/links.tsx b/web/pages/links.tsx index ede997df..76c62978 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -12,7 +12,7 @@ import { Subtitle } from 'web/components/subtitle' import { useUser } from 'web/hooks/use-user' import { useUserManalinks } from 'web/lib/firebase/manalinks' import { fromNow } from 'web/lib/util/time' -import { useUserById } from 'web/hooks/use-users' +import { useUserById } from 'web/hooks/use-user' import { ManalinkTxn } from 'common/txn' import { Avatar } from 'web/components/avatar' import { RelativeTimestamp } from 'web/components/relative-timestamp' From ee01328553414c889b62f7db98d4ddd9f0ce93cf Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 07:53:41 -0600 Subject: [PATCH 161/519] Remove group slugs from contracts on delete group --- functions/src/index.ts | 1 + functions/src/on-group-delete.ts | 31 +++++++++++++++++++++++++++++++ web/hooks/use-group.ts | 2 -- web/lib/firebase/groups.ts | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 functions/src/on-group-delete.ts diff --git a/functions/src/index.ts b/functions/src/index.ts index cf75802e..b3f65a4f 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -21,6 +21,7 @@ export * from './on-create-group' export * from './on-update-user' export * from './on-create-comment-on-group' export * from './on-create-txn' +export * from './on-group-delete' // v2 export * from './health' diff --git a/functions/src/on-group-delete.ts b/functions/src/on-group-delete.ts new file mode 100644 index 00000000..e078bbcd --- /dev/null +++ b/functions/src/on-group-delete.ts @@ -0,0 +1,31 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { Group } from 'common/group' +import { Contract } from 'common/contract' +const firestore = admin.firestore() + +exports.onGroupDelete = functions.firestore + .document('groups/{groupId}') + .onDelete(async (change) => { + const group = change.data() as Group + + // get all contracts with this group's slug + const contracts = await firestore + .collection('contracts') + .where('groupSlugs', 'array-contains', group.slug) + .get() + + for (const doc of contracts.docs) { + const contract = doc.data() as Contract + // remove the group from the contract + await firestore + .collection('contracts') + .doc(contract.id) + .update({ + groupSlugs: (contract.groupSlugs ?? []).filter( + (groupSlug) => groupSlug !== group.slug + ), + }) + } + }) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 8af2183e..be0c92f6 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -2,14 +2,12 @@ import { useEffect, useState } from 'react' import { Group } from 'common/group' import { User } from 'common/user' import { - getGroupBySlug, getGroupsWithContractId, listenForGroup, listenForGroups, listenForMemberGroups, } from 'web/lib/firebase/groups' import { getUser } from 'web/lib/firebase/users' -import { CATEGORIES, CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' import { filterDefined } from 'common/util/array' export const useGroup = (groupId: string | undefined) => { diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 708096b3..ec152792 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -119,6 +119,7 @@ export async function addUserToGroup( await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) return newGroup } + export async function leaveGroup(group: Group, userId: string): Promise<Group> { const { memberIds } = group if (!memberIds.includes(userId)) { From 709ce5377aa4017a7d352cf76e04ae83f5bd5287 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 07:57:33 -0600 Subject: [PATCH 162/519] Remove extra key assignment --- web/pages/groups.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 8f2fe424..9192a094 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -171,10 +171,7 @@ export default function Groups(props: { export function GroupCard(props: { group: Group; creator: User | undefined }) { const { group, creator } = props return ( - <Col - key={group.id} - className="relative min-w-[20rem] max-w-xs gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100" - > + <Col className="relative min-w-[20rem] max-w-xs gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100"> <Link href={groupPath(group.slug)}> <a className="absolute left-0 right-0 top-0 bottom-0 z-0" /> </Link> From eb6b1b9f89e20ac0642bee354ec5070a5331f140 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 08:02:54 -0600 Subject: [PATCH 163/519] Rename on-delete-group --- functions/src/index.ts | 2 +- functions/src/{on-group-delete.ts => on-delete-group.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename functions/src/{on-group-delete.ts => on-delete-group.ts} (94%) diff --git a/functions/src/index.ts b/functions/src/index.ts index b3f65a4f..3055f8dc 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -21,7 +21,7 @@ export * from './on-create-group' export * from './on-update-user' export * from './on-create-comment-on-group' export * from './on-create-txn' -export * from './on-group-delete' +export * from './on-delete-group' // v2 export * from './health' diff --git a/functions/src/on-group-delete.ts b/functions/src/on-delete-group.ts similarity index 94% rename from functions/src/on-group-delete.ts rename to functions/src/on-delete-group.ts index e078bbcd..ca833254 100644 --- a/functions/src/on-group-delete.ts +++ b/functions/src/on-delete-group.ts @@ -5,7 +5,7 @@ import { Group } from 'common/group' import { Contract } from 'common/contract' const firestore = admin.firestore() -exports.onGroupDelete = functions.firestore +export const onDeleteGroup = functions.firestore .document('groups/{groupId}') .onDelete(async (change) => { const group = change.data() as Group From 4eba3c812404c72a537c4c09c17e0e82627e2aed Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 09:09:12 -0600 Subject: [PATCH 164/519] Try new way of calculating rankings for large groups --- web/hooks/use-group.ts | 13 ++++-- web/lib/firebase/contracts.ts | 8 ++++ web/lib/firebase/groups.ts | 18 +------- web/pages/group/[...slugs]/index.tsx | 62 +++++++++------------------- web/pages/groups.tsx | 29 +++++++++++-- 5 files changed, 63 insertions(+), 67 deletions(-) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index be0c92f6..c3098ba4 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -7,7 +7,7 @@ import { listenForGroups, listenForMemberGroups, } from 'web/lib/firebase/groups' -import { getUser } from 'web/lib/firebase/users' +import { getUser, getUsers } from 'web/lib/firebase/users' import { filterDefined } from 'common/util/array' export const useGroup = (groupId: string | undefined) => { @@ -85,9 +85,14 @@ export function useMembers(group: Group, max?: number) { } export async function listMembers(group: Group, max?: number) { - return await Promise.all( - group.memberIds.slice(0, max ? max : group.memberIds.length).map(getUser) - ) + const { memberIds } = group + const numToRetrieve = max ?? memberIds.length + if (memberIds.length === 0) return [] + if (numToRetrieve) + return (await getUsers()).filter((user) => + group.memberIds.includes(user.id) + ) + return await Promise.all(group.memberIds.slice(0, numToRetrieve).map(getUser)) } export const useGroupsWithContract = (contractId: string | undefined) => { diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index d5fb85cb..63efa53b 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -124,6 +124,14 @@ export async function listContracts(creatorId: string): Promise<Contract[]> { return snapshot.docs.map((doc) => doc.data()) } +export async function listContractsByGroupSlug( + slug: string +): Promise<Contract[]> { + const q = query(contracts, where('groupSlugs', 'array-contains', slug)) + const snapshot = await getDocs(q) + return snapshot.docs.map((doc) => doc.data()) +} + export async function listTaggedContractsCaseInsensitive( tag: string ): Promise<Contract[]> { diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index ec152792..762bfab1 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -8,7 +8,7 @@ import { } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' import { Group } from 'common/group' -import { getContractFromId, updateContract } from './contracts' +import { updateContract } from './contracts' import { coll, getValue, @@ -16,7 +16,6 @@ import { listenForValue, listenForValues, } from './utils' -import { filterDefined } from 'common/util/array' import { Contract } from 'common/contract' export const groups = coll<Group>('groups') @@ -54,21 +53,6 @@ export async function getGroupBySlug(slug: string) { return docs.length === 0 ? null : docs[0].data() } -export async function getGroupContracts(group: Group) { - const { contractIds } = group - - const contracts = - filterDefined( - await Promise.all( - contractIds.map(async (contractId) => { - return await getContractFromId(contractId) - }) - ) - ) ?? [] - - return [...contracts] -} - export function listenForGroup( groupId: string, setGroup: (group: Group | null) => void diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 43647cdb..c00a8e29 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -3,14 +3,13 @@ import { take, sortBy, debounce } from 'lodash' import { Group } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' -import { Contract } from 'web/lib/firebase/contracts' +import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' import { groupPath, getGroupBySlug, updateGroup, addUserToGroup, addContractToGroup, - getGroupContracts, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' @@ -22,7 +21,7 @@ import { } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' -import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' +import { listMembers, useGroup } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' @@ -62,14 +61,11 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const { slugs } = props.params const group = await getGroupBySlug(slugs[0]) - const members = - group && group.memberIds.length < 100 ? await listMembers(group) : [] + const members = group && (await listMembers(group)) const creatorPromise = group ? getUser(group.creatorId) : null const contracts = - group && group.contractIds.length < 100 - ? await getGroupContracts(group).catch((_) => []) - : [] + (group && (await listContractsByGroupSlug(group.slug))) ?? [] const bets = await Promise.all( contracts.map((contract: Contract) => listAllBets(contract.id)) @@ -77,10 +73,12 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const creatorScores = scoreCreators(contracts) const traderScores = scoreTraders(contracts, bets) - const [topCreators, topTraders] = await Promise.all([ - toTopUsers(creatorScores), - toTopUsers(traderScores), - ]) + const [topCreators, topTraders] = + (members && [ + toTopUsers(creatorScores, members), + toTopUsers(traderScores, members), + ]) ?? + [] const creator = await creatorPromise @@ -99,14 +97,14 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { } } -async function toTopUsers(userScores: { [userId: string]: number }) { +function toTopUsers(userScores: { [userId: string]: number }, users: User[]) { const topUserPairs = take( sortBy(Object.entries(userScores), ([_, score]) => -1 * score), 10 ).filter(([_, score]) => score >= 0.5) - const topUsers = await Promise.all( - topUserPairs.map(([userId]) => getUser(userId)) + const topUsers = topUserPairs.map( + ([userId]) => users.filter((user) => user.id === userId)[0] ) return topUsers.filter((user) => user) } @@ -199,6 +197,7 @@ export default function GroupPage(props: { creator={creator} isCreator={!!isCreator} user={user} + members={members} /> </Col> ) @@ -327,8 +326,9 @@ function GroupOverview(props: { creator: User user: User | null | undefined isCreator: boolean + members: User[] }) { - const { group, creator, isCreator, user } = props + const { group, creator, isCreator, user, members } = props const anyoneCanJoinChoices: { [key: string]: string } = { Closed: 'false', Open: 'true', @@ -403,7 +403,7 @@ function GroupOverview(props: { </Row> )} <Col className={'mt-2'}> - <GroupMemberSearch group={group} /> + <GroupMemberSearch members={members} /> </Col> </Col> </> @@ -426,10 +426,9 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { group: Group }) { +function GroupMemberSearch(props: { members: User[] }) { const [query, setQuery] = useState('') - const { group } = props - const members = useMembers(group, 100) + const { members } = props // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( @@ -455,29 +454,6 @@ function GroupMemberSearch(props: { group: Group }) { ) } -export function GroupMembersList(props: { group: Group }) { - const { group } = props - const maxMembersToShow = 3 - const members = useMembers(group, maxMembersToShow).filter( - (m) => m.id !== group.creatorId - ) - if (group.memberIds.length === 1) return <div /> - return ( - <div className="text-neutral flex flex-wrap gap-1"> - <span className={'text-gray-500'}>Other members</span> - {members.slice(0, maxMembersToShow).map((member, i) => ( - <div key={member.id} className={'flex-shrink'}> - <UserLink name={member.name} username={member.username} /> - {members.length > 1 && i !== members.length - 1 && <span>,</span>} - </div> - ))} - {group.memberIds.length > maxMembersToShow && ( - <span> & {group.memberIds.length - maxMembersToShow} more</span> - )} - </div> - ) -} - function SortedLeaderboard(props: { users: User[] scoreFunction: (user: User) => number diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 9192a094..2523b789 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -1,23 +1,23 @@ import { sortBy, debounce } from 'lodash' import Link from 'next/link' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { Group } from 'common/group' import { CreateGroupButton } from 'web/components/groups/create-group-button' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' import { Title } from 'web/components/title' -import { useGroups, useMemberGroupIds } from 'web/hooks/use-group' +import { useGroups, useMemberGroupIds, useMembers } from 'web/hooks/use-group' import { useUser } from 'web/hooks/use-user' import { groupPath, listAllGroups } from 'web/lib/firebase/groups' import { getUser, User } from 'web/lib/firebase/users' import { Tabs } from 'web/components/layout/tabs' -import { GroupMembersList } from 'web/pages/group/[...slugs]' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { SiteLink } from 'web/components/site-link' import clsx from 'clsx' import { Avatar } from 'web/components/avatar' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' +import { UserLink } from 'web/components/user-page' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -201,6 +201,29 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) { ) } +function GroupMembersList(props: { group: Group }) { + const { group } = props + const maxMembersToShow = 3 + const members = useMembers(group, maxMembersToShow).filter( + (m) => m.id !== group.creatorId + ) + if (group.memberIds.length === 1) return <div /> + return ( + <div className="text-neutral flex flex-wrap gap-1"> + <span className={'text-gray-500'}>Other members</span> + {members.slice(0, maxMembersToShow).map((member, i) => ( + <div key={member.id} className={'flex-shrink'}> + <UserLink name={member.name} username={member.username} /> + {members.length > 1 && i !== members.length - 1 && <span>,</span>} + </div> + ))} + {group.memberIds.length > maxMembersToShow && ( + <span> & {group.memberIds.length - maxMembersToShow} more</span> + )} + </div> + ) +} + export function GroupLink(props: { group: Group; className?: string }) { const { group, className } = props From deaa595f0735c90554db3a8160c2bbc3bdeed33b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 09:32:50 -0600 Subject: [PATCH 165/519] Exclude contract creator in both places --- functions/src/on-create-bet.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index adf22d56..fc2e0053 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -82,7 +82,9 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( ) } - const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId) + const isNewUniqueBettor = + !previousUniqueBettorIds.includes(bettorId) && + bettorId !== contract.creatorId const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId]) // Update contract unique bettors From 0c328bc39809f1ab947066952f9611d283f54465 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 11:44:52 -0500 Subject: [PATCH 166/519] Move getStorage() into init.ts after initializeApp() is called. --- web/lib/firebase/init.ts | 2 ++ web/lib/firebase/storage.ts | 10 ++-------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/web/lib/firebase/init.ts b/web/lib/firebase/init.ts index 12f3d832..bf712a8f 100644 --- a/web/lib/firebase/init.ts +++ b/web/lib/firebase/init.ts @@ -1,5 +1,6 @@ import { getFirestore } from '@firebase/firestore' import { initializeApp, getApps, getApp } from 'firebase/app' +import { getStorage } from 'firebase/storage' import { FIREBASE_CONFIG } from 'common/envs/constants' import { connectFirestoreEmulator } from 'firebase/firestore' import { connectFunctionsEmulator, getFunctions } from 'firebase/functions' @@ -8,6 +9,7 @@ import { connectFunctionsEmulator, getFunctions } from 'firebase/functions' export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG) export const db = getFirestore() export const functions = getFunctions() +export const storage = getStorage() declare global { /* eslint-disable-next-line no-var */ diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index 5293a6bc..2fc2ccc7 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -1,11 +1,5 @@ -import { - getStorage, - ref, - uploadBytesResumable, - getDownloadURL, -} from 'firebase/storage' - -const storage = getStorage() +import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage' +import { storage } from './init' // TODO: compress large images export const uploadImage = async ( From a93e64c830e3d404936dcd25ed69b65e98e11297 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 14 Jul 2022 10:02:46 -0700 Subject: [PATCH 167/519] fix: let useUserById accept undefined userId (#648) --- web/hooks/use-user.ts | 2 +- web/pages/link/[slug].tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index 158235ca..df4dc8f7 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -46,7 +46,7 @@ export const usePrivateUser = (userId?: string) => { return privateUser } -export const useUserById = (userId: string) => { +export const useUserById = (userId = '_') => { const result = useFirestoreDocumentData<DocumentData, User>( ['users', userId], doc(users, userId), diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 8093969b..67e7b695 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -17,7 +17,7 @@ export default function ClaimPage() { const [claiming, setClaiming] = useState(false) const [error, setError] = useState<string | undefined>(undefined) - const fromUser = useUserById(manalink?.fromId ?? '_loading') + const fromUser = useUserById(manalink?.fromId) if (!manalink) { return <></> } From 8daf1b2ba894212dec28e5f69b000e9d5e020f13 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 12:03:29 -0500 Subject: [PATCH 168/519] Return undefined instead of null for useUserById(undefined) --- web/hooks/use-user.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index df4dc8f7..e04a69ca 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -53,6 +53,8 @@ export const useUserById = (userId = '_') => { { subscribe: true, includeMetadataChanges: true } ) + if (userId === '_') return undefined + return result.isLoading ? undefined : result.data } From 27a544205f3f03077c27ea70527a35085c852405 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 11:09:28 -0600 Subject: [PATCH 169/519] Optimistically join groups --- web/components/groups/groups-button.tsx | 31 ++++++++++++++++++------- web/lib/firebase/groups.ts | 27 +++++++-------------- web/pages/group/[...slugs]/index.tsx | 10 ++++---- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index b81155d1..f3ae77a2 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import { User } from 'common/user' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useUser } from 'web/hooks/use-user' import { withTracking } from 'web/lib/service/analytics' import { Row } from 'web/components/layout/row' @@ -9,9 +9,10 @@ import { TextButton } from 'web/components/text-button' import { Group } from 'common/group' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' -import { addUserToGroup, leaveGroup } from 'web/lib/firebase/groups' +import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { firebaseLogin } from 'web/lib/firebase/users' import { GroupLink } from 'web/pages/groups' +import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { const { user } = props @@ -88,22 +89,34 @@ export function JoinOrLeaveGroupButton(props: { }) { const { group, small, className } = props const currentUser = useUser() - const isFollowing = currentUser - ? group.memberIds.includes(currentUser.id) - : false + const [isMember, setIsMember] = useState<boolean>(false) + useEffect(() => { + if (currentUser && group.memberIds.includes(currentUser.id)) { + setIsMember(group.memberIds.includes(currentUser.id)) + } + }, [currentUser, group]) + const onJoinGroup = () => { if (!currentUser) return - addUserToGroup(group, currentUser.id) + setIsMember(true) + joinGroup(group, currentUser.id).catch(() => { + setIsMember(false) + toast.error('Failed to join group') + }) } const onLeaveGroup = () => { if (!currentUser) return - leaveGroup(group, currentUser.id) + setIsMember(false) + leaveGroup(group, currentUser.id).catch(() => { + setIsMember(true) + toast.error('Failed to leave group') + }) } const smallStyle = 'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500' - if (!currentUser || isFollowing === undefined) { + if (!currentUser || isMember === undefined) { if (!group.anyoneCanJoin) return <div className={clsx(className, 'text-gray-500')}>Closed</div> return ( @@ -116,7 +129,7 @@ export function JoinOrLeaveGroupButton(props: { ) } - if (isFollowing) { + if (isMember) { return ( <button className={clsx( diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 762bfab1..6d695b7f 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -87,32 +87,23 @@ export async function addUserToGroupViaSlug(groupSlug: string, userId: string) { console.error(`Group not found: ${groupSlug}`) return } - return await addUserToGroup(group, userId) + return await joinGroup(group, userId) } -export async function addUserToGroup( - group: Group, - userId: string -): Promise<Group> { +export async function joinGroup(group: Group, userId: string): Promise<void> { const { memberIds } = group - if (memberIds.includes(userId)) { - return group - } + if (memberIds.includes(userId)) return // already a member + const newMemberIds = [...memberIds, userId] - const newGroup = { ...group, memberIds: newMemberIds } - await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) - return newGroup + return await updateGroup(group, { memberIds: uniq(newMemberIds) }) } -export async function leaveGroup(group: Group, userId: string): Promise<Group> { +export async function leaveGroup(group: Group, userId: string): Promise<void> { const { memberIds } = group - if (!memberIds.includes(userId)) { - return group - } + if (!memberIds.includes(userId)) return // not a member + const newMemberIds = memberIds.filter((id) => id !== userId) - const newGroup = { ...group, memberIds: newMemberIds } - await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) - return newGroup + return await updateGroup(group, { memberIds: uniq(newMemberIds) }) } export async function addContractToGroup(group: Group, contract: Contract) { diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c00a8e29..a364de43 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -8,7 +8,7 @@ import { groupPath, getGroupBySlug, updateGroup, - addUserToGroup, + joinGroup, addContractToGroup, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' @@ -604,19 +604,19 @@ function JoinGroupButton(props: { user: User | null | undefined }) { const { group, user } = props - function joinGroup() { + function addUserToGroup() { if (user && !group.memberIds.includes(user.id)) { - toast.promise(addUserToGroup(group, user.id), { + toast.promise(joinGroup(group, user.id), { loading: 'Joining group...', success: 'Joined group!', - error: "Couldn't join group", + error: "Couldn't join group, try again?", }) } } return ( <div> <button - onClick={user ? joinGroup : firebaseLogin} + onClick={user ? addUserToGroup : firebaseLogin} className={'btn-md btn-outline btn whitespace-nowrap normal-case'} > {user ? 'Join group' : 'Login to join group'} From 6a286432153c1b48b69e7d19ceaef52380129e5c Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 11:48:04 -0600 Subject: [PATCH 170/519] Notifications ux --- web/pages/notifications.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2001c557..250fae37 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -36,6 +36,7 @@ import { groupBy, sum, uniq } from 'lodash' import Custom404 from 'web/pages/404' import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' +import { useWindowSize } from 'web/hooks/use-window-size' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -183,7 +184,7 @@ function IncomeNotificationGroupItem(props: { const groupedNotificationsBySourceTitle = groupBy( groupedNotificationsBySourceType[sourceType], (notification) => { - return notification.sourceTitle + return notification.sourceTitle ?? notification.sourceContractTitle } ) for (const contractId in groupedNotificationsBySourceTitle) { @@ -314,7 +315,8 @@ function IncomeNotificationItem(props: { const { notification, justSummary } = props const { sourceType, sourceUserName, sourceUserUsername } = notification const [highlighted] = useState(!notification.isSeen) - + const { width } = useWindowSize() + const isMobile = (width && width < 768) || false useEffect(() => { setNotificationsAsSeen([notification]) }, [notification]) @@ -351,7 +353,7 @@ function IncomeNotificationItem(props: { {getReasonForShowingIncomeNotification(true)} <QuestionOrGroupLink notification={notification} - ignoreClick={true} + ignoreClick={isMobile} /> </span> </div> @@ -406,6 +408,8 @@ function NotificationGroupItem(props: { const { notificationGroup, className } = props const { notifications } = notificationGroup const { sourceContractTitle } = notifications[0] + const { width } = useWindowSize() + const isMobile = (width && width < 768) || false const numSummaryLines = 3 const [expanded, setExpanded] = useState(false) @@ -445,7 +449,10 @@ function NotificationGroupItem(props: { <div className={'flex w-full flex-row justify-between'}> <div className={'ml-2'}> Activity on - <QuestionOrGroupLink notification={notifications[0]} /> + <QuestionOrGroupLink + notification={notifications[0]} + ignoreClick={!expanded && isMobile} + /> </div> <div className={'hidden sm:inline-block'}> <RelativeTimestamp time={notifications[0].createdTime} /> From d9279e42ccf38d9839b5e293f94846d6dd31aa54 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 11:56:40 -0600 Subject: [PATCH 171/519] Don't collapse/expand notifs with ctrl/cmd click --- web/pages/notifications.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 250fae37..f86c4fef 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -157,6 +157,11 @@ function IncomeNotificationGroupItem(props: { notifications.some((n) => !n.isSeen) ) + const onClickHandler = (event: React.MouseEvent<HTMLDivElement>) => { + if (event.ctrlKey || event.metaKey) return + setExpanded(!expanded) + } + useEffect(() => { setNotificationsAsSeen(notifications) }, [notifications]) @@ -231,7 +236,7 @@ function IncomeNotificationGroupItem(props: { !expanded ? 'hover:bg-gray-100' : '', highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' )} - onClick={() => setExpanded(!expanded)} + onClick={onClickHandler} > {expanded && ( <span @@ -245,7 +250,7 @@ function IncomeNotificationGroupItem(props: { /> <div className={'ml-2 flex w-full flex-row flex-wrap truncate'} - onClick={() => setExpanded(!expanded)} + onClick={onClickHandler} > <div className={'flex w-full flex-row justify-between'}> <div> @@ -416,6 +421,12 @@ function NotificationGroupItem(props: { const [highlighted, setHighlighted] = useState( notifications.some((n) => !n.isSeen) ) + + const onClickHandler = (event: React.MouseEvent<HTMLDivElement>) => { + if (event.ctrlKey || event.metaKey) return + setExpanded(!expanded) + } + useEffect(() => { setNotificationsAsSeen(notifications) }, [notifications]) @@ -432,7 +443,7 @@ function NotificationGroupItem(props: { !expanded ? 'hover:bg-gray-100' : '', highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' )} - onClick={() => setExpanded(!expanded)} + onClick={onClickHandler} > {expanded && ( <span From be64bf71a784e0588f60c647f808ac50dbfc174e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 14:57:17 -0500 Subject: [PATCH 172/519] Limit the amount of bets and comments sent to the client through getStaticProps --- web/pages/[username]/[contractSlug].tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 0cfbc99f..17453770 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -65,8 +65,9 @@ export async function getStaticPropz(props: { contract, username, slug: contractSlug, - bets, - comments, + // Limit the data sent to the client. Client will still load all bets and comments directly. + bets: bets.slice(0, 5000), + comments: comments.slice(0, 1000), }, revalidate: 60, // regenerate after a minute From 6e1aa4b0f458deac09e56eaaebcc30a5c80bce62 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 16:46:45 -0600 Subject: [PATCH 173/519] Order groups by most recent chat activity (#650) * Order groups by most recent chat activity * Use group chat slug constant * Match source slug and isSeenOnHref * Listen for group member changes --- common/group.ts | 3 ++ functions/src/create-notification.ts | 49 ++++++++++++++------- functions/src/on-create-comment-on-group.ts | 18 +++----- functions/src/on-update-group.ts | 10 ++++- web/components/groups/groups-button.tsx | 4 +- web/components/nav/sidebar.tsx | 20 +++++++-- web/components/user-page.tsx | 5 ++- web/hooks/use-group.ts | 27 +++++++----- web/lib/firebase/groups.ts | 19 +++++--- web/pages/group/[...slugs]/index.tsx | 23 ++++++---- web/pages/groups.tsx | 6 ++- 11 files changed, 125 insertions(+), 59 deletions(-) diff --git a/common/group.ts b/common/group.ts index 15348d5a..e367ded7 100644 --- a/common/group.ts +++ b/common/group.ts @@ -11,8 +11,11 @@ export type Group = { contractIds: string[] chatDisabled?: boolean + mostRecentChatActivityTime?: number + mostRecentContractAddedTime?: number } export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] +export const GROUP_CHAT_SLUG = 'chat' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 1fb6c3af..4c42b00e 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -15,11 +15,11 @@ import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' -import { Group } from '../../common/group' +import { Group, GROUP_CHAT_SLUG } from '../../common/group' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string } + [userId: string]: { reason: notification_reason_types } } export const createNotification = async ( @@ -72,7 +72,6 @@ export const createNotification = async ( sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, - isSeenOnHref: userToReasonTexts[userId].isSeeOnHref, } await notificationRef.set(removeUndefinedProps(notification)) }) @@ -277,17 +276,6 @@ export const createNotification = async ( } } - const notifyOtherGroupMembersOfComment = async ( - userToReasons: user_to_reason_texts, - userId: string - ) => { - if (shouldGetNotification(userId, userToReasons)) - userToReasons[userId] = { - reason: 'on_group_you_are_member_of', - isSeeOnHref: sourceSlug, - } - } - const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -298,8 +286,6 @@ export const createNotification = async ( await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) } else if (sourceType === 'user' && relatedUserId) { await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) - } else if (sourceType === 'comment' && !sourceContract && relatedUserId) { - await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. @@ -417,3 +403,34 @@ export const createBetFillNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) } + +export const createGroupCommentNotification = async ( + fromUser: User, + toUserId: string, + comment: Comment, + group: Group, + idempotencyKey: string +) => { + const notificationRef = firestore + .collection(`/users/${toUserId}/notifications`) + .doc(idempotencyKey) + const sourceSlug = `/group/${group.slug}/${GROUP_CHAT_SLUG}` + const notification: Notification = { + id: idempotencyKey, + userId: toUserId, + reason: 'on_group_you_are_member_of', + createdTime: Date.now(), + isSeen: false, + sourceId: comment.id, + sourceType: 'comment', + sourceUpdateType: 'created', + sourceUserName: fromUser.name, + sourceUserUsername: fromUser.username, + sourceUserAvatarUrl: fromUser.avatarUrl, + sourceText: comment.text, + sourceSlug, + sourceTitle: `${group.name}`, + isSeenOnHref: sourceSlug, + } + await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts index 7217e602..0064480f 100644 --- a/functions/src/on-create-comment-on-group.ts +++ b/functions/src/on-create-comment-on-group.ts @@ -3,7 +3,7 @@ import { Comment } from '../../common/comment' import * as admin from 'firebase-admin' import { Group } from '../../common/group' import { User } from '../../common/user' -import { createNotification } from './create-notification' +import { createGroupCommentNotification } from './create-notification' const firestore = admin.firestore() export const onCreateCommentOnGroup = functions.firestore @@ -29,23 +29,17 @@ export const onCreateCommentOnGroup = functions.firestore const group = groupSnapshot.data() as Group await firestore.collection('groups').doc(groupId).update({ - mostRecentActivityTime: comment.createdTime, + mostRecentChatActivityTime: comment.createdTime, }) await Promise.all( group.memberIds.map(async (memberId) => { - return await createNotification( - comment.id, - 'comment', - 'created', + return await createGroupCommentNotification( creatorSnapshot.data() as User, - eventId, - comment.text, - undefined, - undefined, memberId, - `/group/${group.slug}`, - `${group.name}` + comment, + group, + eventId ) }) ) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index feaa6443..3ab2a249 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,7 +12,15 @@ export const onUpdateGroup = functions.firestore // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return - // TODO: create notification with isSeeOnHref set to the group's /group/questions url + + if (prevGroup.contractIds.length < group.contractIds.length) { + await firestore + .collection('groups') + .doc(group.id) + .update({ mostRecentContractAddedTime: Date.now() }) + //TODO: create notification with isSeeOnHref set to the group's /group/slug/questions url + // but first, let the new /group/slug/chat notification permeate so that we can differentiate between the two + } await firestore .collection('groups') diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index f3ae77a2..b510f44d 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -17,7 +17,9 @@ import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { const { user } = props const [isOpen, setIsOpen] = useState(false) - const groups = useMemberGroups(user.id) + const groups = useMemberGroups(user.id, undefined, { + by: 'mostRecentChatActivityTime', + }) return ( <> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 784eb63a..b260553b 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -24,7 +24,7 @@ import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -194,10 +194,14 @@ export default function Sidebar(props: { className?: string }) { ? signedOutMobileNavigation : signedInMobileNavigation const memberItems = ( - useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] + useMemberGroups( + user?.id, + { withChatEnabled: true }, + { by: 'mostRecentChatActivityTime' } + ) ?? [] ).map((group: Group) => ({ name: group.name, - href: groupPath(group.slug), + href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, })) return ( @@ -278,8 +282,16 @@ function GroupsList(props: { // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { + const currentPageGroupSlug = currentPage.split('/')[2] preferredNotifications.forEach((notification) => { - if (notification.isSeenOnHref === currentPage) { + if ( + notification.isSeenOnHref === currentPage || + // Old chat style group chat notif ended just with the group slug + notification.isSeenOnHref?.endsWith(currentPageGroupSlug) || + // They're on the home page, so if they've a chat notif, they're seeing the chat + (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && + currentPage.endsWith(currentPageGroupSlug)) + ) { setNotificationsAsSeen([notification]) } }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index be3f3ac4..85d70e86 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,6 +38,7 @@ import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' +import { ReferralsButton } from 'web/components/referrals-button' export function UserLink(props: { name: string @@ -202,7 +203,9 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - {/* <ReferralsButton user={user} currentUser={currentUser} /> */} + {currentUser?.username === 'Ian' && ( + <ReferralsButton user={user} currentUser={currentUser} /> + )} <GroupsButton user={user} /> </Row> diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index c3098ba4..4f968005 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -32,19 +32,26 @@ export const useGroups = () => { export const useMemberGroups = ( userId: string | null | undefined, - options?: { withChatEnabled: boolean } + options?: { withChatEnabled: boolean }, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) => { const [memberGroups, setMemberGroups] = useState<Group[] | undefined>() useEffect(() => { if (userId) - return listenForMemberGroups(userId, (groups) => { - if (options?.withChatEnabled) - return setMemberGroups( - filterDefined(groups.filter((group) => group.chatDisabled !== true)) - ) - return setMemberGroups(groups) - }) - }, [options?.withChatEnabled, userId]) + return listenForMemberGroups( + userId, + (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined( + groups.filter((group) => group.chatDisabled !== true) + ) + ) + return setMemberGroups(groups) + }, + sort + ) + }, [options?.withChatEnabled, sort, userId]) return memberGroups } @@ -88,7 +95,7 @@ export async function listMembers(group: Group, max?: number) { const { memberIds } = group const numToRetrieve = max ?? memberIds.length if (memberIds.length === 0) return [] - if (numToRetrieve) + if (numToRetrieve > 100) return (await getUsers()).filter((user) => group.memberIds.includes(user.id) ) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 6d695b7f..e49b012a 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { updateContract } from './contracts' import { coll, @@ -22,7 +22,7 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } @@ -62,12 +62,21 @@ export function listenForGroup( export function listenForMemberGroups( userId: string, - setGroups: (groups: Group[]) => void + setGroups: (groups: Group[]) => void, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) { const q = query(groups, where('memberIds', 'array-contains', userId)) - + const sorter = (group: Group) => { + if (sort?.by === 'mostRecentChatActivityTime') { + return group.mostRecentChatActivityTime ?? group.mostRecentActivityTime + } + if (sort?.by === 'mostRecentContractAddedTime') { + return group.mostRecentContractAddedTime ?? group.mostRecentActivityTime + } + return group.mostRecentActivityTime + } return listenForValues<Group>(q, (groups) => { - const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) + const sorted = sortBy(groups, [(group) => -sorter(group)]) setGroups(sorted) }) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a364de43..3fa64964 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,6 +1,6 @@ import { take, sortBy, debounce } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' @@ -21,7 +21,7 @@ import { } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' -import { listMembers, useGroup } from 'web/hooks/use-group' +import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' @@ -114,7 +114,7 @@ export async function getStaticPaths() { } const groupSubpages = [ undefined, - 'chat', + GROUP_CHAT_SLUG, 'questions', 'rankings', 'about', @@ -218,7 +218,7 @@ export default function GroupPage(props: { ) : ( <LoadingIndicator /> ), - href: groupPath(group.slug, 'chat'), + href: groupPath(group.slug, GROUP_CHAT_SLUG), }, ]), { @@ -246,7 +246,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) return ( <Page rightSidebar={rightSidebar} className="!pb-0"> <SEO @@ -403,7 +403,7 @@ function GroupOverview(props: { </Row> )} <Col className={'mt-2'}> - <GroupMemberSearch members={members} /> + <GroupMemberSearch members={members} group={group} /> </Col> </Col> </> @@ -426,9 +426,16 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { members: User[] }) { +function GroupMemberSearch(props: { members: User[]; group: Group }) { const [query, setQuery] = useState('') - const { members } = props + const { group } = props + let { members } = props + + // Use static members on load, but also listen to member changes: + const listenToMembers = useMembers(group) + if (listenToMembers) { + members = listenToMembers + } // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 2523b789..87ac1501 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -79,7 +79,11 @@ export default function Groups(props: { ) const matchesOrderedByRecentActivity = sortBy(groups, [ - (group) => -1 * group.mostRecentActivityTime, + (group) => + -1 * + (group.mostRecentChatActivityTime ?? + group.mostRecentContractAddedTime ?? + group.mostRecentActivityTime), ]).filter( (g) => checkAgainstQuery(query, g.name) || From a9018d77c77067417a6c31f0aaa97670baf98b5f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 18:01:35 -0500 Subject: [PATCH 174/519] If a limit bet doesn't match any orders, don't update the contract, don't redeem shares. Perf win! --- functions/src/place-bet.ts | 50 ++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 73c60187..b8c0ca0e 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -128,34 +128,38 @@ export const placebet = newEndpoint({}, async (req, auth) => { updateMakers(makers, betDoc.id, contractDoc, trans) } - trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) }) - log('Updated user balance.') - trans.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - p: newP, - totalShares: newTotalShares, - totalBets: newTotalBets, - totalLiquidity: newTotalLiquidity, - collectedFees: addObjects(newBet.fees, collectedFees), - volume: volume + newBet.amount, - }) - ) - log('Updated contract properties.') + if (newBet.amount !== 0) { + trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) }) + log('Updated user balance.') - return { betId: betDoc.id, makers } + trans.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + p: newP, + totalShares: newTotalShares, + totalBets: newTotalBets, + totalLiquidity: newTotalLiquidity, + collectedFees: addObjects(newBet.fees, collectedFees), + volume: volume + newBet.amount, + }) + ) + log('Updated contract properties.') + } + + return { betId: betDoc.id, makers, newBet } }) log('Main transaction finished.') - await redeemShares(auth.uid, contractId) - const userIds = [ - auth.uid, - ...(result.makers ?? []).map((maker) => maker.bet.userId), - ] - await Promise.all(userIds.map((userId) => redeemShares(userId, contractId))) - log('Share redemption transaction finished.') + if (result.newBet.amount !== 0) { + const userIds = [ + auth.uid, + ...(result.makers ?? []).map((maker) => maker.bet.userId), + ] + await Promise.all(userIds.map((userId) => redeemShares(userId, contractId))) + log('Share redemption transaction finished.') + } return { betId: result.betId } }) From 44d993a5884dd5cb83941e6d04eb1d7f4109b429 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 14 Jul 2022 17:03:08 -0600 Subject: [PATCH 175/519] Bold group for old chat notif --- web/components/nav/sidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index b260553b..8553b506 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -321,7 +321,10 @@ function GroupsList(props: { className={clsx( 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', preferredNotifications.some( - (n) => !n.isSeen && n.isSeenOnHref === item.href + (n) => + !n.isSeen && + (n.isSeenOnHref === item.href || + n.isSeenOnHref === item.href.replace('/chat', '')) ) && 'font-bold' )} > From 2f02e4d3e050da56d362af7ebb045496ae13f533 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Thu, 14 Jul 2022 17:43:06 -0700 Subject: [PATCH 176/519] minor tweaks of manalink form (#647) * minor tweaks of manalink form, adding M$ in front of amount and changing expire time to dropdown instead of calendar selection * made minimum for uses and amount 1, it seems otherwise it does not generate a link at all --- .../manalinks/create-links-button.tsx | 87 ++++++++++++------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index d74980cf..12ab8c87 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -63,15 +63,39 @@ function CreateManalinkForm(props: { const [finishedCreating, setFinishedCreating] = useState(false) const [copyPressed, setCopyPressed] = useState(false) setTimeout(() => setCopyPressed(false), 300) + const defaultExpire = 'week' + const [expiresIn, setExpiresIn] = useState(defaultExpire) const [newManalink, setNewManalink] = useState<ManalinkInfo>({ - expiresTime: null, + expiresTime: dayjs().add(1, defaultExpire).valueOf(), amount: 100, maxUses: 1, uses: 0, message: '', }) + const EXPIRE_OPTIONS = { + day: '1 Day', + week: '1 Week', + month: '1 Month', + never: 'Never', + } + + const expireOptions = Object.entries(EXPIRE_OPTIONS).map(([key, value]) => { + return <option value={key}>{value}</option> + }) + + function setExpireTime(timeDelta: string) { + const expiresTime = + timeDelta === 'never' ? null : dayjs().add(1, timeDelta).valueOf() + setNewManalink((m) => { + return { + ...m, + expiresTime, + } + }) + } + return ( <> {!finishedCreating && ( @@ -87,23 +111,30 @@ function CreateManalinkForm(props: { <div className="flex flex-col flex-wrap gap-x-5 gap-y-2"> <div className="form-control flex-auto"> <label className="label">Amount</label> - <input - className="input input-bordered" - type="number" - value={newManalink.amount} - onChange={(e) => - setNewManalink((m) => { - return { ...m, amount: parseInt(e.target.value) } - }) - } - ></input> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-full pl-10" + type="number" + min="1" + value={newManalink.amount} + onChange={(e) => + setNewManalink((m) => { + return { ...m, amount: parseInt(e.target.value) } + }) + } + /> + </div> </div> <div className="flex flex-col gap-2 md:flex-row"> - <div className="form-control"> + <div className="form-control w-full md:w-1/2"> <label className="label">Uses</label> <input - className="input input-bordered w-full" + className="input input-bordered" type="number" + min="1" value={newManalink.maxUses ?? ''} onChange={(e) => setNewManalink((m) => { @@ -112,29 +143,19 @@ function CreateManalinkForm(props: { } ></input> </div> - <div className="form-control"> + <div className="form-control w-full md:w-1/2"> <label className="label">Expires in</label> - <input - value={ - newManalink.expiresTime != null - ? dayjs(newManalink.expiresTime).format( - 'YYYY-MM-DDTHH:mm' - ) - : '' - } - className="input input-bordered" - type="datetime-local" + <select + className="!select !select-bordered" + value={expiresIn} + defaultValue={defaultExpire} onChange={(e) => { - setNewManalink((m) => { - return { - ...m, - expiresTime: e.target.value - ? dayjs(e.target.value, 'YYYY-MM-DDTHH:mm').valueOf() - : null, - } - }) + setExpiresIn(e.target.value) + setExpireTime(e.target.value) }} - ></input> + > + {expireOptions} + </select> </div> </div> <div className="form-control w-full"> From 17c9beca2846b358505b3048a1c7b731eb931dfc Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 20:51:38 -0500 Subject: [PATCH 177/519] Revert "Order groups by most recent chat activity (#650)" This reverts commit 6e1aa4b0f458deac09e56eaaebcc30a5c80bce62. --- common/group.ts | 3 -- functions/src/create-notification.ts | 49 +++++++-------------- functions/src/on-create-comment-on-group.ts | 18 +++++--- functions/src/on-update-group.ts | 10 +---- web/components/groups/groups-button.tsx | 4 +- web/components/nav/sidebar.tsx | 20 ++------- web/components/user-page.tsx | 5 +-- web/hooks/use-group.ts | 27 +++++------- web/lib/firebase/groups.ts | 19 +++----- web/pages/group/[...slugs]/index.tsx | 23 ++++------ web/pages/groups.tsx | 6 +-- 11 files changed, 59 insertions(+), 125 deletions(-) diff --git a/common/group.ts b/common/group.ts index e367ded7..15348d5a 100644 --- a/common/group.ts +++ b/common/group.ts @@ -11,11 +11,8 @@ export type Group = { contractIds: string[] chatDisabled?: boolean - mostRecentChatActivityTime?: number - mostRecentContractAddedTime?: number } export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] -export const GROUP_CHAT_SLUG = 'chat' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 4c42b00e..1fb6c3af 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -15,11 +15,11 @@ import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' -import { Group, GROUP_CHAT_SLUG } from '../../common/group' +import { Group } from '../../common/group' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types } + [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string } } export const createNotification = async ( @@ -72,6 +72,7 @@ export const createNotification = async ( sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, + isSeenOnHref: userToReasonTexts[userId].isSeeOnHref, } await notificationRef.set(removeUndefinedProps(notification)) }) @@ -276,6 +277,17 @@ export const createNotification = async ( } } + const notifyOtherGroupMembersOfComment = async ( + userToReasons: user_to_reason_texts, + userId: string + ) => { + if (shouldGetNotification(userId, userToReasons)) + userToReasons[userId] = { + reason: 'on_group_you_are_member_of', + isSeeOnHref: sourceSlug, + } + } + const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -286,6 +298,8 @@ export const createNotification = async ( await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) } else if (sourceType === 'user' && relatedUserId) { await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) + } else if (sourceType === 'comment' && !sourceContract && relatedUserId) { + await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. @@ -403,34 +417,3 @@ export const createBetFillNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) } - -export const createGroupCommentNotification = async ( - fromUser: User, - toUserId: string, - comment: Comment, - group: Group, - idempotencyKey: string -) => { - const notificationRef = firestore - .collection(`/users/${toUserId}/notifications`) - .doc(idempotencyKey) - const sourceSlug = `/group/${group.slug}/${GROUP_CHAT_SLUG}` - const notification: Notification = { - id: idempotencyKey, - userId: toUserId, - reason: 'on_group_you_are_member_of', - createdTime: Date.now(), - isSeen: false, - sourceId: comment.id, - sourceType: 'comment', - sourceUpdateType: 'created', - sourceUserName: fromUser.name, - sourceUserUsername: fromUser.username, - sourceUserAvatarUrl: fromUser.avatarUrl, - sourceText: comment.text, - sourceSlug, - sourceTitle: `${group.name}`, - isSeenOnHref: sourceSlug, - } - await notificationRef.set(removeUndefinedProps(notification)) -} diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts index 0064480f..7217e602 100644 --- a/functions/src/on-create-comment-on-group.ts +++ b/functions/src/on-create-comment-on-group.ts @@ -3,7 +3,7 @@ import { Comment } from '../../common/comment' import * as admin from 'firebase-admin' import { Group } from '../../common/group' import { User } from '../../common/user' -import { createGroupCommentNotification } from './create-notification' +import { createNotification } from './create-notification' const firestore = admin.firestore() export const onCreateCommentOnGroup = functions.firestore @@ -29,17 +29,23 @@ export const onCreateCommentOnGroup = functions.firestore const group = groupSnapshot.data() as Group await firestore.collection('groups').doc(groupId).update({ - mostRecentChatActivityTime: comment.createdTime, + mostRecentActivityTime: comment.createdTime, }) await Promise.all( group.memberIds.map(async (memberId) => { - return await createGroupCommentNotification( + return await createNotification( + comment.id, + 'comment', + 'created', creatorSnapshot.data() as User, + eventId, + comment.text, + undefined, + undefined, memberId, - comment, - group, - eventId + `/group/${group.slug}`, + `${group.name}` ) }) ) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index 3ab2a249..feaa6443 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,15 +12,7 @@ export const onUpdateGroup = functions.firestore // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return - - if (prevGroup.contractIds.length < group.contractIds.length) { - await firestore - .collection('groups') - .doc(group.id) - .update({ mostRecentContractAddedTime: Date.now() }) - //TODO: create notification with isSeeOnHref set to the group's /group/slug/questions url - // but first, let the new /group/slug/chat notification permeate so that we can differentiate between the two - } + // TODO: create notification with isSeeOnHref set to the group's /group/questions url await firestore .collection('groups') diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index b510f44d..f3ae77a2 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -17,9 +17,7 @@ import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { const { user } = props const [isOpen, setIsOpen] = useState(false) - const groups = useMemberGroups(user.id, undefined, { - by: 'mostRecentChatActivityTime', - }) + const groups = useMemberGroups(user.id) return ( <> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 8553b506..f4abc6c7 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -24,7 +24,7 @@ import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' -import { Group, GROUP_CHAT_SLUG } from 'common/group' +import { Group } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -194,14 +194,10 @@ export default function Sidebar(props: { className?: string }) { ? signedOutMobileNavigation : signedInMobileNavigation const memberItems = ( - useMemberGroups( - user?.id, - { withChatEnabled: true }, - { by: 'mostRecentChatActivityTime' } - ) ?? [] + useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] ).map((group: Group) => ({ name: group.name, - href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, + href: groupPath(group.slug), })) return ( @@ -282,16 +278,8 @@ function GroupsList(props: { // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { - const currentPageGroupSlug = currentPage.split('/')[2] preferredNotifications.forEach((notification) => { - if ( - notification.isSeenOnHref === currentPage || - // Old chat style group chat notif ended just with the group slug - notification.isSeenOnHref?.endsWith(currentPageGroupSlug) || - // They're on the home page, so if they've a chat notif, they're seeing the chat - (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && - currentPage.endsWith(currentPageGroupSlug)) - ) { + if (notification.isSeenOnHref === currentPage) { setNotificationsAsSeen([notification]) } }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 85d70e86..be3f3ac4 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,7 +38,6 @@ import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' -import { ReferralsButton } from 'web/components/referrals-button' export function UserLink(props: { name: string @@ -203,9 +202,7 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - {currentUser?.username === 'Ian' && ( - <ReferralsButton user={user} currentUser={currentUser} /> - )} + {/* <ReferralsButton user={user} currentUser={currentUser} /> */} <GroupsButton user={user} /> </Row> diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 4f968005..c3098ba4 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -32,26 +32,19 @@ export const useGroups = () => { export const useMemberGroups = ( userId: string | null | undefined, - options?: { withChatEnabled: boolean }, - sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } + options?: { withChatEnabled: boolean } ) => { const [memberGroups, setMemberGroups] = useState<Group[] | undefined>() useEffect(() => { if (userId) - return listenForMemberGroups( - userId, - (groups) => { - if (options?.withChatEnabled) - return setMemberGroups( - filterDefined( - groups.filter((group) => group.chatDisabled !== true) - ) - ) - return setMemberGroups(groups) - }, - sort - ) - }, [options?.withChatEnabled, sort, userId]) + return listenForMemberGroups(userId, (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined(groups.filter((group) => group.chatDisabled !== true)) + ) + return setMemberGroups(groups) + }) + }, [options?.withChatEnabled, userId]) return memberGroups } @@ -95,7 +88,7 @@ export async function listMembers(group: Group, max?: number) { const { memberIds } = group const numToRetrieve = max ?? memberIds.length if (memberIds.length === 0) return [] - if (numToRetrieve > 100) + if (numToRetrieve) return (await getUsers()).filter((user) => group.memberIds.includes(user.id) ) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index e49b012a..6d695b7f 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' -import { Group, GROUP_CHAT_SLUG } from 'common/group' +import { Group } from 'common/group' import { updateContract } from './contracts' import { coll, @@ -22,7 +22,7 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } @@ -62,21 +62,12 @@ export function listenForGroup( export function listenForMemberGroups( userId: string, - setGroups: (groups: Group[]) => void, - sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } + setGroups: (groups: Group[]) => void ) { const q = query(groups, where('memberIds', 'array-contains', userId)) - const sorter = (group: Group) => { - if (sort?.by === 'mostRecentChatActivityTime') { - return group.mostRecentChatActivityTime ?? group.mostRecentActivityTime - } - if (sort?.by === 'mostRecentContractAddedTime') { - return group.mostRecentContractAddedTime ?? group.mostRecentActivityTime - } - return group.mostRecentActivityTime - } + return listenForValues<Group>(q, (groups) => { - const sorted = sortBy(groups, [(group) => -sorter(group)]) + const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) setGroups(sorted) }) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3fa64964..a364de43 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,6 +1,6 @@ import { take, sortBy, debounce } from 'lodash' -import { Group, GROUP_CHAT_SLUG } from 'common/group' +import { Group } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' @@ -21,7 +21,7 @@ import { } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' -import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' +import { listMembers, useGroup } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' @@ -114,7 +114,7 @@ export async function getStaticPaths() { } const groupSubpages = [ undefined, - GROUP_CHAT_SLUG, + 'chat', 'questions', 'rankings', 'about', @@ -218,7 +218,7 @@ export default function GroupPage(props: { ) : ( <LoadingIndicator /> ), - href: groupPath(group.slug, GROUP_CHAT_SLUG), + href: groupPath(group.slug, 'chat'), }, ]), { @@ -246,7 +246,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') return ( <Page rightSidebar={rightSidebar} className="!pb-0"> <SEO @@ -403,7 +403,7 @@ function GroupOverview(props: { </Row> )} <Col className={'mt-2'}> - <GroupMemberSearch members={members} group={group} /> + <GroupMemberSearch members={members} /> </Col> </Col> </> @@ -426,16 +426,9 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { members: User[]; group: Group }) { +function GroupMemberSearch(props: { members: User[] }) { const [query, setQuery] = useState('') - const { group } = props - let { members } = props - - // Use static members on load, but also listen to member changes: - const listenToMembers = useMembers(group) - if (listenToMembers) { - members = listenToMembers - } + const { members } = props // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 87ac1501..2523b789 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -79,11 +79,7 @@ export default function Groups(props: { ) const matchesOrderedByRecentActivity = sortBy(groups, [ - (group) => - -1 * - (group.mostRecentChatActivityTime ?? - group.mostRecentContractAddedTime ?? - group.mostRecentActivityTime), + (group) => -1 * group.mostRecentActivityTime, ]).filter( (g) => checkAgainstQuery(query, g.name) || From 590c63e911340f088907124369868aad6e340410 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 21:27:00 -0500 Subject: [PATCH 178/519] Small fixes for limit order table --- web/components/bets-list.tsx | 18 ++++++++++-------- web/components/limit-bets.tsx | 8 ++++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index e57b0c38..db6b0d05 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -45,7 +45,7 @@ import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBet } from 'common/bet' import { floatingEqual } from 'common/util/math' import { Pagination } from './pagination' -import { LimitBets } from './limit-bets' +import { LimitOrderTable } from './limit-bets' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' @@ -364,18 +364,20 @@ function ContractBets(props: { {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( <> - <div className="bg-gray-50 px-4 py-2">Your limit orders</div> - <LimitBets - className="max-w-md px-2 py-0 sm:px-4" - contract={contract} - bets={limitBets} - hideLabel - /> + <div className="max-w-md"> + <div className="bg-gray-50 px-4 py-2">Limit orders</div> + <LimitOrderTable + contract={contract} + limitBets={limitBets} + isYou={true} + /> + </div> </> )} <Spacer h={4} /> + <div className="bg-gray-50 px-4 py-2">Bets</div> <ContractBetsTable contract={contract} bets={bets} diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index f81d4294..93647a5e 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -16,7 +16,6 @@ import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' export function LimitBets(props: { contract: CPMMBinaryContract | PseudoNumericContract bets: LimitBet[] - hideLabel?: boolean className?: string }) { const { contract, bets, className } = props @@ -67,20 +66,21 @@ export function LimitBets(props: { ) } -function LimitOrderTable(props: { +export function LimitOrderTable(props: { limitBets: LimitBet[] contract: CPMMBinaryContract | PseudoNumericContract isYou: boolean }) { const { limitBets, contract, isYou } = props + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' return ( <table className="table-compact table w-full rounded text-gray-500"> <thead> - {!isYou && <th>User</th>} + {!isYou && <th></th>} <th>Outcome</th> <th>Amount</th> - <th>Prob</th> + <th>{isPseudoNumeric ? 'Value' : 'Prob'}</th> {isYou && <th></th>} </thead> <tbody> From 64c83c4ef021c269a90f6ee2b51faef22ca534c3 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 14 Jul 2022 23:56:30 -0500 Subject: [PATCH 179/519] Don't show portfolio no history message --- web/components/portfolio/portfolio-value-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 903b3f3d..22f1478f 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -16,7 +16,7 @@ export const PortfolioValueSection = memo( const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime') if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { - return <div> No portfolio history data yet </div> + return <></> } return ( From 36851ae9f9d5035d6c509c113122b5fb485e6cf5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 15 Jul 2022 00:45:50 -0500 Subject: [PATCH 180/519] Exclude more mobile options from private instances --- web/components/nav/sidebar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index f4abc6c7..1ff59275 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -110,9 +110,13 @@ const signedInMobileNavigation = [ function getMoreMobileNav() { return [ - { name: 'Send M$', href: '/links' }, - { name: 'Charity', href: '/charity' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ...(IS_PRIVATE_MANIFOLD + ? [] + : [ + { name: 'Send M$', href: '/links' }, + { name: 'Charity', href: '/charity' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ]), { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Sign out', From 9c49f2e2d729f4526630b2e4b0a7c6a3538df54a Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 06:52:08 -0600 Subject: [PATCH 181/519] Revert "Revert "Order groups by most recent chat activity (#650)"" This reverts commit 17c9beca2846b358505b3048a1c7b731eb931dfc. --- common/group.ts | 3 ++ functions/src/create-notification.ts | 49 ++++++++++++++------- functions/src/on-create-comment-on-group.ts | 18 +++----- functions/src/on-update-group.ts | 10 ++++- web/components/groups/groups-button.tsx | 4 +- web/components/nav/sidebar.tsx | 20 +++++++-- web/components/user-page.tsx | 5 ++- web/hooks/use-group.ts | 27 +++++++----- web/lib/firebase/groups.ts | 19 +++++--- web/pages/group/[...slugs]/index.tsx | 23 ++++++---- web/pages/groups.tsx | 6 ++- 11 files changed, 125 insertions(+), 59 deletions(-) diff --git a/common/group.ts b/common/group.ts index 15348d5a..e367ded7 100644 --- a/common/group.ts +++ b/common/group.ts @@ -11,8 +11,11 @@ export type Group = { contractIds: string[] chatDisabled?: boolean + mostRecentChatActivityTime?: number + mostRecentContractAddedTime?: number } export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] +export const GROUP_CHAT_SLUG = 'chat' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 1fb6c3af..4c42b00e 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -15,11 +15,11 @@ import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' -import { Group } from '../../common/group' +import { Group, GROUP_CHAT_SLUG } from '../../common/group' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string } + [userId: string]: { reason: notification_reason_types } } export const createNotification = async ( @@ -72,7 +72,6 @@ export const createNotification = async ( sourceContractSlug: sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, - isSeenOnHref: userToReasonTexts[userId].isSeeOnHref, } await notificationRef.set(removeUndefinedProps(notification)) }) @@ -277,17 +276,6 @@ export const createNotification = async ( } } - const notifyOtherGroupMembersOfComment = async ( - userToReasons: user_to_reason_texts, - userId: string - ) => { - if (shouldGetNotification(userId, userToReasons)) - userToReasons[userId] = { - reason: 'on_group_you_are_member_of', - isSeeOnHref: sourceSlug, - } - } - const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -298,8 +286,6 @@ export const createNotification = async ( await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) } else if (sourceType === 'user' && relatedUserId) { await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) - } else if (sourceType === 'comment' && !sourceContract && relatedUserId) { - await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. @@ -417,3 +403,34 @@ export const createBetFillNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) } + +export const createGroupCommentNotification = async ( + fromUser: User, + toUserId: string, + comment: Comment, + group: Group, + idempotencyKey: string +) => { + const notificationRef = firestore + .collection(`/users/${toUserId}/notifications`) + .doc(idempotencyKey) + const sourceSlug = `/group/${group.slug}/${GROUP_CHAT_SLUG}` + const notification: Notification = { + id: idempotencyKey, + userId: toUserId, + reason: 'on_group_you_are_member_of', + createdTime: Date.now(), + isSeen: false, + sourceId: comment.id, + sourceType: 'comment', + sourceUpdateType: 'created', + sourceUserName: fromUser.name, + sourceUserUsername: fromUser.username, + sourceUserAvatarUrl: fromUser.avatarUrl, + sourceText: comment.text, + sourceSlug, + sourceTitle: `${group.name}`, + isSeenOnHref: sourceSlug, + } + await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts index 7217e602..0064480f 100644 --- a/functions/src/on-create-comment-on-group.ts +++ b/functions/src/on-create-comment-on-group.ts @@ -3,7 +3,7 @@ import { Comment } from '../../common/comment' import * as admin from 'firebase-admin' import { Group } from '../../common/group' import { User } from '../../common/user' -import { createNotification } from './create-notification' +import { createGroupCommentNotification } from './create-notification' const firestore = admin.firestore() export const onCreateCommentOnGroup = functions.firestore @@ -29,23 +29,17 @@ export const onCreateCommentOnGroup = functions.firestore const group = groupSnapshot.data() as Group await firestore.collection('groups').doc(groupId).update({ - mostRecentActivityTime: comment.createdTime, + mostRecentChatActivityTime: comment.createdTime, }) await Promise.all( group.memberIds.map(async (memberId) => { - return await createNotification( - comment.id, - 'comment', - 'created', + return await createGroupCommentNotification( creatorSnapshot.data() as User, - eventId, - comment.text, - undefined, - undefined, memberId, - `/group/${group.slug}`, - `${group.name}` + comment, + group, + eventId ) }) ) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index feaa6443..3ab2a249 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,7 +12,15 @@ export const onUpdateGroup = functions.firestore // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return - // TODO: create notification with isSeeOnHref set to the group's /group/questions url + + if (prevGroup.contractIds.length < group.contractIds.length) { + await firestore + .collection('groups') + .doc(group.id) + .update({ mostRecentContractAddedTime: Date.now() }) + //TODO: create notification with isSeeOnHref set to the group's /group/slug/questions url + // but first, let the new /group/slug/chat notification permeate so that we can differentiate between the two + } await firestore .collection('groups') diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index f3ae77a2..b510f44d 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -17,7 +17,9 @@ import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { const { user } = props const [isOpen, setIsOpen] = useState(false) - const groups = useMemberGroups(user.id) + const groups = useMemberGroups(user.id, undefined, { + by: 'mostRecentChatActivityTime', + }) return ( <> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 1ff59275..baa60719 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -24,7 +24,7 @@ import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -198,10 +198,14 @@ export default function Sidebar(props: { className?: string }) { ? signedOutMobileNavigation : signedInMobileNavigation const memberItems = ( - useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] + useMemberGroups( + user?.id, + { withChatEnabled: true }, + { by: 'mostRecentChatActivityTime' } + ) ?? [] ).map((group: Group) => ({ name: group.name, - href: groupPath(group.slug), + href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, })) return ( @@ -282,8 +286,16 @@ function GroupsList(props: { // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { + const currentPageGroupSlug = currentPage.split('/')[2] preferredNotifications.forEach((notification) => { - if (notification.isSeenOnHref === currentPage) { + if ( + notification.isSeenOnHref === currentPage || + // Old chat style group chat notif ended just with the group slug + notification.isSeenOnHref?.endsWith(currentPageGroupSlug) || + // They're on the home page, so if they've a chat notif, they're seeing the chat + (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && + currentPage.endsWith(currentPageGroupSlug)) + ) { setNotificationsAsSeen([notification]) } }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index be3f3ac4..85d70e86 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,6 +38,7 @@ import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' +import { ReferralsButton } from 'web/components/referrals-button' export function UserLink(props: { name: string @@ -202,7 +203,9 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - {/* <ReferralsButton user={user} currentUser={currentUser} /> */} + {currentUser?.username === 'Ian' && ( + <ReferralsButton user={user} currentUser={currentUser} /> + )} <GroupsButton user={user} /> </Row> diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index c3098ba4..4f968005 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -32,19 +32,26 @@ export const useGroups = () => { export const useMemberGroups = ( userId: string | null | undefined, - options?: { withChatEnabled: boolean } + options?: { withChatEnabled: boolean }, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) => { const [memberGroups, setMemberGroups] = useState<Group[] | undefined>() useEffect(() => { if (userId) - return listenForMemberGroups(userId, (groups) => { - if (options?.withChatEnabled) - return setMemberGroups( - filterDefined(groups.filter((group) => group.chatDisabled !== true)) - ) - return setMemberGroups(groups) - }) - }, [options?.withChatEnabled, userId]) + return listenForMemberGroups( + userId, + (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined( + groups.filter((group) => group.chatDisabled !== true) + ) + ) + return setMemberGroups(groups) + }, + sort + ) + }, [options?.withChatEnabled, sort, userId]) return memberGroups } @@ -88,7 +95,7 @@ export async function listMembers(group: Group, max?: number) { const { memberIds } = group const numToRetrieve = max ?? memberIds.length if (memberIds.length === 0) return [] - if (numToRetrieve) + if (numToRetrieve > 100) return (await getUsers()).filter((user) => group.memberIds.includes(user.id) ) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 6d695b7f..e49b012a 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { updateContract } from './contracts' import { coll, @@ -22,7 +22,7 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } @@ -62,12 +62,21 @@ export function listenForGroup( export function listenForMemberGroups( userId: string, - setGroups: (groups: Group[]) => void + setGroups: (groups: Group[]) => void, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) { const q = query(groups, where('memberIds', 'array-contains', userId)) - + const sorter = (group: Group) => { + if (sort?.by === 'mostRecentChatActivityTime') { + return group.mostRecentChatActivityTime ?? group.mostRecentActivityTime + } + if (sort?.by === 'mostRecentContractAddedTime') { + return group.mostRecentContractAddedTime ?? group.mostRecentActivityTime + } + return group.mostRecentActivityTime + } return listenForValues<Group>(q, (groups) => { - const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) + const sorted = sortBy(groups, [(group) => -sorter(group)]) setGroups(sorted) }) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a364de43..3fa64964 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,6 +1,6 @@ import { take, sortBy, debounce } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' @@ -21,7 +21,7 @@ import { } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' -import { listMembers, useGroup } from 'web/hooks/use-group' +import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' @@ -114,7 +114,7 @@ export async function getStaticPaths() { } const groupSubpages = [ undefined, - 'chat', + GROUP_CHAT_SLUG, 'questions', 'rankings', 'about', @@ -218,7 +218,7 @@ export default function GroupPage(props: { ) : ( <LoadingIndicator /> ), - href: groupPath(group.slug, 'chat'), + href: groupPath(group.slug, GROUP_CHAT_SLUG), }, ]), { @@ -246,7 +246,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) return ( <Page rightSidebar={rightSidebar} className="!pb-0"> <SEO @@ -403,7 +403,7 @@ function GroupOverview(props: { </Row> )} <Col className={'mt-2'}> - <GroupMemberSearch members={members} /> + <GroupMemberSearch members={members} group={group} /> </Col> </Col> </> @@ -426,9 +426,16 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { members: User[] }) { +function GroupMemberSearch(props: { members: User[]; group: Group }) { const [query, setQuery] = useState('') - const { members } = props + const { group } = props + let { members } = props + + // Use static members on load, but also listen to member changes: + const listenToMembers = useMembers(group) + if (listenToMembers) { + members = listenToMembers + } // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 2523b789..87ac1501 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -79,7 +79,11 @@ export default function Groups(props: { ) const matchesOrderedByRecentActivity = sortBy(groups, [ - (group) => -1 * group.mostRecentActivityTime, + (group) => + -1 * + (group.mostRecentChatActivityTime ?? + group.mostRecentContractAddedTime ?? + group.mostRecentActivityTime), ]).filter( (g) => checkAgainstQuery(query, g.name) || From 47579e850908f94be7b62bdd68050e19f8c2a131 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 07:28:04 -0600 Subject: [PATCH 182/519] Fix network spam with modified deps array --- web/hooks/use-group.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 4f968005..892efda0 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -51,7 +51,8 @@ export const useMemberGroups = ( }, sort ) - }, [options?.withChatEnabled, sort, userId]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options?.withChatEnabled, sort?.by, userId]) return memberGroups } From 2610f325212bc5c42bd63aac4d0e65e6d43a7aa2 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 07:35:17 -0600 Subject: [PATCH 183/519] Correct my username --- web/components/user-page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 85d70e86..38efe345 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -203,7 +203,7 @@ export function UserPage(props: { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - {currentUser?.username === 'Ian' && ( + {currentUser?.username === 'ian' && ( <ReferralsButton user={user} currentUser={currentUser} /> )} <GroupsButton user={user} /> From dd9d24e657682f366e324a2dbc8b2074f4b21eb6 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 08:45:52 -0600 Subject: [PATCH 184/519] Show online users on desktop --- common/user.ts | 2 +- firestore.rules | 21 ++++++----- web/components/follow-list.tsx | 6 +-- web/components/nav/sidebar.tsx | 13 ++++++- web/components/online-user-list.tsx | 56 ++++++++++++++++++++++++++++ web/pages/group/[...slugs]/index.tsx | 20 +++++++--- 6 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 web/components/online-user-list.tsx diff --git a/common/user.ts b/common/user.ts index 477139fd..2960bda0 100644 --- a/common/user.ts +++ b/common/user.ts @@ -38,6 +38,7 @@ export type User = { referredByUserId?: string referredByContractId?: string + lastPingTime?: number } export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000 @@ -57,7 +58,6 @@ export type PrivateUser = { initialIpAddress?: string apiKey?: string notificationPreferences?: notification_subscribe_types - lastTimeCheckedBonuses?: number } export type notification_subscribe_types = 'all' | 'less' | 'none' diff --git a/firestore.rules b/firestore.rules index 918448d6..196c5992 100644 --- a/firestore.rules +++ b/firestore.rules @@ -20,16 +20,17 @@ service cloud.firestore { allow read; allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); - allow update: if resource.data.id == request.auth.uid - && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['referredByUserId']) - // only one referral allowed per user - && !("referredByUserId" in resource.data) - // user can't refer themselves - && !(resource.data.id == request.resource.data.referredByUserId); - // quid pro quos enabled (only once though so nbd) - bc I can't make this work: - // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId', 'lastPingTime']); + // User referral rules + allow update: if resource.data.id == request.auth.uid + && request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['referredByUserId']) + // only one referral allowed per user + && !("referredByUserId" in resource.data) + // user can't refer themselves + && !(resource.data.id == request.resource.data.referredByUserId); + // quid pro quos enabled (only once though so nbd) - bc I can't make this work: + // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); } match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { diff --git a/web/components/follow-list.tsx b/web/components/follow-list.tsx index c935f73d..391ce4d3 100644 --- a/web/components/follow-list.tsx +++ b/web/components/follow-list.tsx @@ -7,6 +7,7 @@ import { FollowButton } from './follow-button' import { Col } from './layout/col' import { Row } from './layout/row' import { UserLink } from './user-page' +import { OnlineUserAvatar } from 'web/components/online-user-list' export function FollowList(props: { userIds: string[] }) { const { userIds } = props @@ -63,10 +64,7 @@ function UserFollowItem(props: { return ( <Row className={clsx('items-center justify-between gap-2 p-2', className)}> - <Row className="items-center gap-2"> - <Avatar username={user?.username} avatarUrl={user?.avatarUrl} /> - {user && <UserLink name={user.name} username={user.username} />} - </Row> + <OnlineUserAvatar user={user} /> {!hideFollowButton && ( <FollowButton isFollowing={isFollowing} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index baa60719..c2a7bcde 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -13,7 +13,7 @@ import clsx from 'clsx' import Link from 'next/link' import { useRouter } from 'next/router' import { usePrivateUser, useUser } from 'web/hooks/use-user' -import { firebaseLogout, User } from 'web/lib/firebase/users' +import { firebaseLogout, updateUser, User } from 'web/lib/firebase/users' import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' import { ProfileSummary } from './profile-menu' @@ -208,6 +208,17 @@ export default function Sidebar(props: { className?: string }) { href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, })) + useEffect(() => { + if (!user) return + // set ping time to now every 60 seconds to indicate that the user is active + const pingInterval = setInterval(() => { + updateUser(user.id, { + lastPingTime: Date.now(), + }) + }, 1000 * 30) + return () => clearInterval(pingInterval) + }, [user]) + return ( <nav aria-label="Sidebar" className={className}> <ManifoldLogo className="py-6" twoLine /> diff --git a/web/components/online-user-list.tsx b/web/components/online-user-list.tsx new file mode 100644 index 00000000..7156ad91 --- /dev/null +++ b/web/components/online-user-list.tsx @@ -0,0 +1,56 @@ +import clsx from 'clsx' +import { Avatar } from './avatar' +import { Col } from './layout/col' +import { Row } from './layout/row' +import { UserLink } from './user-page' +import { User } from 'common/user' +import { UserCircleIcon } from '@heroicons/react/solid' +import { useUsers } from 'web/hooks/use-users' +import { partition } from 'lodash' + +const isOnline = (user?: User) => + user && user.lastPingTime && user.lastPingTime > Date.now() - 5 * 60 * 1000 + +export function OnlineUserList(props: { users: User[] }) { + let { users } = props + const liveUsers = useUsers().filter((user) => + users.map((u) => u.id).includes(user.id) + ) + if (liveUsers) users = liveUsers + const [onlineUsers, offlineUsers] = partition(users, (user) => isOnline(user)) + return ( + <Col className="mt-4 gap-1"> + {onlineUsers + .concat(offlineUsers) + .slice(0, 15) + .map((user) => ( + <Row + key={user.id} + className={clsx('items-center justify-between gap-2 p-2')} + > + <OnlineUserAvatar key={user.id} user={user} /> + </Row> + ))} + </Col> + ) +} + +export function OnlineUserAvatar(props: { user?: User; className?: string }) { + const { user, className } = props + + return ( + <Row className={clsx('relative items-center gap-2', className)}> + <Avatar + username={user?.username} + avatarUrl={user?.avatarUrl} + className={className} + /> + {user && <UserLink name={user.name} username={user.username} />} + {isOnline(user) && ( + <div className="absolute left-0 top-0 "> + <UserCircleIcon className="text-primary bg-primary h-3 w-3 rounded-full border-2 border-white" /> + </div> + )} + </Row> + ) +} diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3fa64964..9620894f 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -55,6 +55,7 @@ import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' +import { OnlineUserList } from 'web/components/online-user-list' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -174,7 +175,12 @@ export default function GroupPage(props: { const rightSidebar = ( <Col className="mt-6 hidden xl:block"> - <JoinOrCreateButton group={group} user={user} isMember={!!isMember} /> + <JoinOrAddQuestionsButtons + group={group} + user={user} + isMember={!!isMember} + /> + <OnlineUserList users={members} /> </Col> ) const leaderboard = ( @@ -254,7 +260,6 @@ export default function GroupPage(props: { description={`Created by ${creator.name}. ${group.about}`} url={groupPath(group.slug)} /> - <Col className="px-3"> <Row className={'items-center justify-between gap-4'}> <div className={'sm:mb-1'}> @@ -270,7 +275,7 @@ export default function GroupPage(props: { </div> </div> <div className="hidden sm:block xl:hidden"> - <JoinOrCreateButton + <JoinOrAddQuestionsButtons group={group} user={user} isMember={!!isMember} @@ -278,10 +283,13 @@ export default function GroupPage(props: { </div> </Row> <div className="block sm:hidden"> - <JoinOrCreateButton group={group} user={user} isMember={!!isMember} /> + <JoinOrAddQuestionsButtons + group={group} + user={user} + isMember={!!isMember} + /> </div> </Col> - <Tabs currentPageForAnalytics={groupPath(group.slug)} className={'mb-0 sm:mb-2'} @@ -292,7 +300,7 @@ export default function GroupPage(props: { ) } -function JoinOrCreateButton(props: { +function JoinOrAddQuestionsButtons(props: { group: Group user: User | null | undefined isMember: boolean From d54a72c4312a35f2bce249600550e78da17bf07e Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 08:47:19 -0600 Subject: [PATCH 185/519] Remove extra comment --- web/components/nav/sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index c2a7bcde..55f11b16 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -210,7 +210,6 @@ export default function Sidebar(props: { className?: string }) { useEffect(() => { if (!user) return - // set ping time to now every 60 seconds to indicate that the user is active const pingInterval = setInterval(() => { updateUser(user.id, { lastPingTime: Date.now(), From 50447cf8d3972cb841944e01ac2a8a9b86f6e3c3 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 08:48:35 -0600 Subject: [PATCH 186/519] Unused vars --- web/components/follow-list.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/components/follow-list.tsx b/web/components/follow-list.tsx index 391ce4d3..670eba09 100644 --- a/web/components/follow-list.tsx +++ b/web/components/follow-list.tsx @@ -2,11 +2,9 @@ import clsx from 'clsx' import { useFollows } from 'web/hooks/use-follows' import { useUser, useUserById } from 'web/hooks/use-user' import { follow, unfollow } from 'web/lib/firebase/users' -import { Avatar } from './avatar' import { FollowButton } from './follow-button' import { Col } from './layout/col' import { Row } from './layout/row' -import { UserLink } from './user-page' import { OnlineUserAvatar } from 'web/components/online-user-list' export function FollowList(props: { userIds: string[] }) { From 0be38c4e0921aa686a3f14f139885ad28bdfe0b3 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 09:32:03 -0600 Subject: [PATCH 187/519] Online users list ui, remove from followers list --- web/components/follow-list.tsx | 8 ++++-- web/components/online-user-list.tsx | 41 ++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/web/components/follow-list.tsx b/web/components/follow-list.tsx index 670eba09..c935f73d 100644 --- a/web/components/follow-list.tsx +++ b/web/components/follow-list.tsx @@ -2,10 +2,11 @@ import clsx from 'clsx' import { useFollows } from 'web/hooks/use-follows' import { useUser, useUserById } from 'web/hooks/use-user' import { follow, unfollow } from 'web/lib/firebase/users' +import { Avatar } from './avatar' import { FollowButton } from './follow-button' import { Col } from './layout/col' import { Row } from './layout/row' -import { OnlineUserAvatar } from 'web/components/online-user-list' +import { UserLink } from './user-page' export function FollowList(props: { userIds: string[] }) { const { userIds } = props @@ -62,7 +63,10 @@ function UserFollowItem(props: { return ( <Row className={clsx('items-center justify-between gap-2 p-2', className)}> - <OnlineUserAvatar user={user} /> + <Row className="items-center gap-2"> + <Avatar username={user?.username} avatarUrl={user?.avatarUrl} /> + {user && <UserLink name={user.name} username={user.username} />} + </Row> {!hideFollowButton && ( <FollowButton isFollowing={isFollowing} diff --git a/web/components/online-user-list.tsx b/web/components/online-user-list.tsx index 7156ad91..d7f52d56 100644 --- a/web/components/online-user-list.tsx +++ b/web/components/online-user-list.tsx @@ -7,6 +7,8 @@ import { User } from 'common/user' import { UserCircleIcon } from '@heroicons/react/solid' import { useUsers } from 'web/hooks/use-users' import { partition } from 'lodash' +import { useWindowSize } from 'web/hooks/use-window-size' +import { useState } from 'react' const isOnline = (user?: User) => user && user.lastPingTime && user.lastPingTime > Date.now() - 5 * 60 * 1000 @@ -18,34 +20,59 @@ export function OnlineUserList(props: { users: User[] }) { ) if (liveUsers) users = liveUsers const [onlineUsers, offlineUsers] = partition(users, (user) => isOnline(user)) + const { width, height } = useWindowSize() + const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) + // Subtract bottom bar when it's showing (less than lg screen) + const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 + const remainingHeight = + (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight return ( - <Col className="mt-4 gap-1"> + <Col + className="mt-4 flex-1 gap-1 hover:overflow-auto" + ref={setContainerRef} + style={{ height: remainingHeight }} + > {onlineUsers - .concat(offlineUsers) + .concat( + offlineUsers.sort( + (a, b) => (b.lastPingTime ?? 0) - (a.lastPingTime ?? 0) + ) + ) .slice(0, 15) .map((user) => ( <Row key={user.id} className={clsx('items-center justify-between gap-2 p-2')} > - <OnlineUserAvatar key={user.id} user={user} /> + <OnlineUserAvatar key={user.id} user={user} size={'sm'} /> </Row> ))} </Col> ) } -export function OnlineUserAvatar(props: { user?: User; className?: string }) { - const { user, className } = props +export function OnlineUserAvatar(props: { + user?: User + className?: string + size?: 'sm' | 'xs' | number +}) { + const { user, className, size } = props return ( <Row className={clsx('relative items-center gap-2', className)}> <Avatar username={user?.username} avatarUrl={user?.avatarUrl} - className={className} + size={size} + className={!isOnline(user) ? 'opacity-50' : ''} /> - {user && <UserLink name={user.name} username={user.username} />} + {user && ( + <UserLink + name={user.name} + username={user.username} + className={!isOnline(user) ? 'text-gray-500' : ''} + /> + )} {isOnline(user) && ( <div className="absolute left-0 top-0 "> <UserCircleIcon className="text-primary bg-primary h-3 w-3 rounded-full border-2 border-white" /> From ec682788e0118478a15476d50e282d3ef068c939 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 15 Jul 2022 11:03:42 -0500 Subject: [PATCH 188/519] Put back old Yes/No bet buttons --- web/components/bet-panel.tsx | 30 ++++++--------- web/components/yes-no-selector.tsx | 62 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 8343d696..7188c19a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -40,6 +40,7 @@ import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBets } from './limit-bets' import { BucketInput } from './bucket-input' import { PillButton } from './buttons/pill-button' +import { YesNoSelector } from './yes-no-selector' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -268,25 +269,16 @@ function BuyPanel(props: { return ( <> - <div className="my-3 text-left text-sm text-gray-500">Direction</div> - <Row className="mb-4 items-center gap-2"> - <PillButton - selected={betChoice === 'YES'} - onSelect={() => onBetChoice('YES')} - big - color="bg-primary" - > - {isPseudoNumeric ? 'Higher' : 'Yes'} - </PillButton> - <PillButton - selected={betChoice === 'NO'} - onSelect={() => onBetChoice('NO')} - big - color="bg-red-400" - > - {isPseudoNumeric ? 'Lower' : 'No'} - </PillButton> - </Row> + <div className="my-3 text-left text-sm text-gray-500"> + {isPseudoNumeric ? 'Direction' : 'Outcome'} + </div> + <YesNoSelector + className="mb-4" + btnClassName="flex-1" + selected={betChoice} + onSelect={(choice) => onBetChoice(choice)} + isPseudoNumeric={isPseudoNumeric} + /> <div className="my-3 text-left text-sm text-gray-500">Amount</div> <BuyAmountInput diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index dda97c0c..3b3cc21d 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -5,6 +5,68 @@ import { Col } from './layout/col' import { Row } from './layout/row' import { resolution } from 'common/contract' +export function YesNoSelector(props: { + selected?: 'YES' | 'NO' + onSelect: (selected: 'YES' | 'NO') => void + className?: string + btnClassName?: string + replaceYesButton?: React.ReactNode + replaceNoButton?: React.ReactNode + isPseudoNumeric?: boolean +}) { + const { + selected, + onSelect, + className, + btnClassName, + replaceNoButton, + replaceYesButton, + isPseudoNumeric, + } = props + + const commonClassNames = + 'inline-flex items-center justify-center rounded-3xl border-2 p-2' + + return ( + <Row className={clsx('space-x-3', className)}> + {replaceYesButton ? ( + replaceYesButton + ) : ( + <button + className={clsx( + commonClassNames, + 'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white', + selected == 'YES' + ? 'bg-primary text-white' + : 'text-primary bg-transparent', + btnClassName + )} + onClick={() => onSelect('YES')} + > + {isPseudoNumeric ? 'HIGHER' : 'YES'} + </button> + )} + {replaceNoButton ? ( + replaceNoButton + ) : ( + <button + className={clsx( + commonClassNames, + 'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white', + selected == 'NO' + ? 'bg-red-400 text-white' + : 'bg-transparent text-red-400', + btnClassName + )} + onClick={() => onSelect('NO')} + > + {isPseudoNumeric ? 'LOWER' : 'NO'} + </button> + )} + </Row> + ) +} + export function YesNoCancelSelector(props: { selected: resolution | undefined onSelect: (selected: resolution) => void From 1ca73ecd4de663bf1435a22bf61f2d4e45b52cfc Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 15 Jul 2022 12:24:07 -0500 Subject: [PATCH 189/519] Add size prop to button --- web/components/button.tsx | 21 ++++++++++++++----- .../manalinks/create-links-button.tsx | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/web/components/button.tsx b/web/components/button.tsx index 3b59581b..d050b81e 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -1,26 +1,37 @@ import { ReactNode } from 'react' import clsx from 'clsx' -export default function Button(props: { +export function Button(props: { + children: ReactNode className?: string onClick?: () => void - color: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' - children?: ReactNode + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' + color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' type?: 'button' | 'reset' | 'submit' }) { const { + children, className, onClick, - children, + size = 'md', color = 'indigo', type = 'button', } = props + const sizeClasses = { + xs: 'px-2.5 py-1.5 text-sm', + sm: 'px-3 py-2 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-4 py-2 text-base', + xl: 'px-6 py-3 text-base', + }[size] + return ( <button type={type} className={clsx( - 'font-md items-center justify-center rounded-md border border-transparent px-4 py-2 shadow-sm hover:transition-colors', + 'font-md items-center justify-center rounded-md border border-transparent shadow-sm hover:transition-colors', + sizeClasses, color === 'green' && 'btn-primary text-white', color === 'red' && 'bg-red-400 text-white hover:bg-red-500', color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500', diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index 12ab8c87..538e794e 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -9,7 +9,7 @@ import { createManalink } from 'web/lib/firebase/manalinks' import { Modal } from 'web/components/layout/modal' import Textarea from 'react-expanding-textarea' import dayjs from 'dayjs' -import Button from '../button' +import { Button } from '../button' import { getManalinkUrl } from 'web/pages/links' import { DuplicateIcon } from '@heroicons/react/outline' From a6cbb6b759168187aebd68d8cf38291669f5368d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 11:53:30 -0600 Subject: [PATCH 190/519] Small notifications ux improvements --- common/user.ts | 3 ++ functions/src/create-user.ts | 6 ++- web/pages/notifications.tsx | 74 ++++++++++++++++++++---------------- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/common/user.ts b/common/user.ts index 2960bda0..6eed3bdb 100644 --- a/common/user.ts +++ b/common/user.ts @@ -69,3 +69,6 @@ export type PortfolioMetrics = { timestamp: number userId: string } + +export const MANIFOLD_USERNAME = 'ManifoldMarkets' +export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 332c1872..1fd23894 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -1,6 +1,8 @@ import * as admin from 'firebase-admin' import { z } from 'zod' import { + MANIFOLD_AVATAR_URL, + MANIFOLD_USERNAME, PrivateUser, STARTING_BALANCE, SUS_STARTING_BALANCE, @@ -160,8 +162,8 @@ const addUserToDefaultGroups = async (user: User) => { text: `Welcome, ${user.name} (@${user.username})!`, createdTime: Date.now(), userName: 'Manifold Markets', - userUsername: 'ManifoldMarkets', - userAvatarUrl: 'https://manifold.markets/logo-bg-white.png', + userUsername: MANIFOLD_USERNAME, + userAvatarUrl: MANIFOLD_AVATAR_URL, }) } } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index f86c4fef..94c18d64 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -9,7 +9,12 @@ import { Title } from 'web/components/title' import { doc, updateDoc } from 'firebase/firestore' import { db } from 'web/lib/firebase/init' import { UserLink } from 'web/components/user-page' -import { notification_subscribe_types, PrivateUser } from 'common/user' +import { + MANIFOLD_AVATAR_URL, + MANIFOLD_USERNAME, + notification_subscribe_types, + PrivateUser, +} from 'common/user' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' import { LoadingIndicator } from 'web/components/loading-indicator' @@ -37,6 +42,7 @@ import Custom404 from 'web/pages/404' import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' +import Router from 'next/router' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -550,6 +556,8 @@ function NotificationItem(props: { setNotificationsAsSeen([notification]) }, [notification]) + const questionNeedsResolution = sourceUpdateType == 'closed' + if (justSummary) { return ( <Row className={'items-center text-sm text-gray-500 sm:justify-start'}> @@ -565,7 +573,7 @@ function NotificationItem(props: { <span className={'flex-shrink-0'}> {sourceType && reason && - getReasonForShowingNotification(notification, true, true)} + getReasonForShowingNotification(notification, true)} </span> <div className={'ml-1 text-black'}> <NotificationTextLabel @@ -588,9 +596,11 @@ function NotificationItem(props: { highlighted && 'bg-indigo-200 hover:bg-indigo-100' )} > - <a - href={getSourceUrl(notification)} - onClick={() => + <div + className={'cursor-pointer'} + onClick={(event) => { + event.stopPropagation() + Router.push(getSourceUrl(notification) ?? '') track('Notification Clicked', { type: 'notification item', sourceType, @@ -602,14 +612,20 @@ function NotificationItem(props: { sourceUserUsername, sourceText, }) - } + }} > <Row className={'items-center text-gray-500 sm:justify-start'}> <Avatar - avatarUrl={sourceUserAvatarUrl} + avatarUrl={ + questionNeedsResolution + ? MANIFOLD_AVATAR_URL + : sourceUserAvatarUrl + } size={'sm'} className={'mr-2'} - username={sourceUserName} + username={ + questionNeedsResolution ? MANIFOLD_USERNAME : sourceUserUsername + } /> <div className={'flex w-full flex-row pl-1 sm:pl-0'}> <div @@ -618,7 +634,7 @@ function NotificationItem(props: { } > <div> - {sourceUpdateType != 'closed' && ( + {!questionNeedsResolution && ( <UserLink name={sourceUserName || ''} username={sourceUserUsername || ''} @@ -628,8 +644,7 @@ function NotificationItem(props: { )} {getReasonForShowingNotification( notification, - false, - isChildOfGroup + isChildOfGroup ?? false )} {isChildOfGroup ? ( <RelativeTimestamp time={notification.createdTime} /> @@ -650,7 +665,7 @@ function NotificationItem(props: { </div> <div className={'mt-6 border-b border-gray-300'} /> - </a> + </div> </div> ) } @@ -769,17 +784,10 @@ function NotificationTextLabel(props: { justSummary?: boolean }) { const { className, notification, justSummary } = props - const { - sourceUpdateType, - sourceType, - sourceText, - sourceContractTitle, - reasonText, - } = notification + const { sourceUpdateType, sourceType, sourceText, reasonText } = notification const defaultText = sourceText ?? reasonText ?? '' if (sourceType === 'contract') { - if (justSummary) return <span>{sourceContractTitle}</span> - if (!sourceText) return <div /> + if (justSummary || !sourceText) return <div /> // Resolved contracts if (sourceType === 'contract' && sourceUpdateType === 'resolved') { { @@ -857,27 +865,27 @@ function NotificationTextLabel(props: { function getReasonForShowingNotification( notification: Notification, - simple?: boolean, - replaceOn?: boolean + justSummary: boolean ) { const { sourceType, sourceUpdateType, reason, sourceSlug } = notification let reasonText: string switch (sourceType) { case 'comment': if (reason === 'reply_to_users_answer') - reasonText = !simple ? 'replied to you on' : 'replied' + reasonText = justSummary ? 'replied' : 'replied to you on' else if (reason === 'tagged_user') - reasonText = !simple ? 'tagged you on' : 'tagged you' + reasonText = justSummary ? 'tagged you' : 'tagged you on' else if (reason === 'reply_to_users_comment') - reasonText = !simple ? 'replied to you on' : 'replied' - else reasonText = `commented on` + reasonText = justSummary ? 'replied' : 'replied to you on' + else reasonText = justSummary ? `commented` : `commented on` break case 'contract': - if (reason === 'you_follow_user') reasonText = 'asked' - else if (sourceUpdateType === 'resolved') reasonText = `resolved` - else if (sourceUpdateType === 'closed') - reasonText = `Please resolve your question` - else reasonText = `updated` + if (reason === 'you_follow_user') + reasonText = justSummary ? 'asked the question' : 'asked' + else if (sourceUpdateType === 'resolved') + reasonText = justSummary ? `resolved the question` : `resolved` + else if (sourceUpdateType === 'closed') reasonText = `Please resolve` + else reasonText = justSummary ? 'updated the question' : `updated` break case 'answer': if (reason === 'on_users_contract') reasonText = `answered your question ` @@ -904,7 +912,7 @@ function getReasonForShowingNotification( default: reasonText = '' } - return replaceOn ? reasonText.replace(' on', '') : reasonText + return reasonText } // TODO: where should we put referral bonus notifications? From feba0b58eef524ea40f452c0a0f3503eb92330a0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 15 Jul 2022 15:06:33 -0500 Subject: [PATCH 191/519] Turn search filters into pills --- web/components/contract-search.tsx | 33 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 952d4034..f7972a3d 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -26,6 +26,8 @@ import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { NEW_USER_GROUP_SLUGS } from 'common/group' +import { PillButton } from './buttons/pill-button' +import { toPairs } from 'lodash' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -46,6 +48,13 @@ const sortIndexes = [ ] type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' +const filterOptions: { [label: string]: filter } = { + All: 'all', + Open: 'open', + Closed: 'closed', + Resolved: 'resolved', + 'For you': 'personal', +} export function ContractSearch(props: { querySortOptions?: { @@ -156,18 +165,6 @@ export function ContractSearch(props: { }} /> {/*// TODO track WHICH filter users are using*/} - <select - className="!select !select-bordered" - value={filter} - onChange={(e) => setFilter(e.target.value as filter)} - onBlur={trackCallback('select search filter')} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="personal">For you</option> - <option value="all">All</option> - </select> {!hideOrderSelector && ( <SortBy items={sortIndexes} @@ -187,7 +184,17 @@ export function ContractSearch(props: { <Spacer h={3} /> - {/*<Spacer h={4} />*/} + <Row className="gap-2"> + {toPairs<filter>(filterOptions).map(([label, f]) => { + return ( + <PillButton selected={filter === f} onSelect={() => setFilter(f)}> + {label} + </PillButton> + ) + })} + </Row> + + <Spacer h={3} /> {filter === 'personal' && (follows ?? []).length === 0 && From 38c26f8b5cf4fbfd665f7780b56867b66b82e119 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 15 Jul 2022 14:03:34 -0700 Subject: [PATCH 192/519] Add API endpoints for fetching user info by username and ID (#652) * Add an API endpoint for fetching user info by username * Add endpoint for querying users by ID, too * Add very simple docs about user APIs --- docs/docs/api.md | 13 +++++++++++++ web/pages/api/v0/user/[username]/index.ts | 19 +++++++++++++++++++ web/pages/api/v0/user/by-id/[id].ts | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 web/pages/api/v0/user/[username]/index.ts create mode 100644 web/pages/api/v0/user/by-id/[id].ts diff --git a/docs/docs/api.md b/docs/docs/api.md index 1cea6027..667c68b8 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -34,6 +34,18 @@ response was a 4xx or 5xx.) ## Endpoints +### `GET /v0/user/[username]` + +Gets a user by their username. Remember that usernames may change. + +Requires no authorization. + +### `GET /v0/user/by-id/[id]` + +Gets a user by their unique ID. Many other API endpoints return this as the `userId`. + +Requires no authorization. + ### `GET /v0/markets` Lists all markets, ordered by creation date descending. @@ -627,6 +639,7 @@ Requires no authorization. ## Changelog +- 2022-07-15: Add user by username and user by ID APIs - 2022-06-08: Add paging to markets endpoint - 2022-06-05: Add new authorized write endpoints - 2022-02-28: Add `resolutionTime` to markets, change `closeTime` definition diff --git a/web/pages/api/v0/user/[username]/index.ts b/web/pages/api/v0/user/[username]/index.ts new file mode 100644 index 00000000..58daffcd --- /dev/null +++ b/web/pages/api/v0/user/[username]/index.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getUserByUsername } from 'web/lib/firebase/users' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { LiteUser, ApiError, toLiteUser } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<LiteUser | ApiError> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { username } = req.query + const user = await getUserByUsername(username as string) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(toLiteUser(user)) +} diff --git a/web/pages/api/v0/user/by-id/[id].ts b/web/pages/api/v0/user/by-id/[id].ts new file mode 100644 index 00000000..6ed67d1c --- /dev/null +++ b/web/pages/api/v0/user/by-id/[id].ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getUser } from 'web/lib/firebase/users' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { LiteUser, ApiError, toLiteUser } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<LiteUser | ApiError> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const user = await getUser(id as string) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(toLiteUser(user)) +} From 2543bdcdfc0f21098b4f886fd1eec3a534446718 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 15 Jul 2022 14:16:00 -0700 Subject: [PATCH 193/519] refactor string matching (#649) --- common/util/parse.ts | 10 +++++++++ web/components/filter-select-users.tsx | 4 ++-- web/components/groups/group-selector.tsx | 13 +++++------- web/hooks/use-sort-and-query-params.tsx | 5 ----- web/pages/charity/index.tsx | 11 ++++++---- web/pages/contract-search-firestore.tsx | 25 ++++++++--------------- web/pages/group/[...slugs]/index.tsx | 11 ++++------ web/pages/groups.tsx | 26 ++++++++++++++---------- 8 files changed, 52 insertions(+), 53 deletions(-) diff --git a/common/util/parse.ts b/common/util/parse.ts index 94b5ab7f..30dcb952 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -49,6 +49,16 @@ export function parseWordsAsTags(text: string) { return parseTags(taggedText) } +// TODO: fuzzy matching +export const wordIn = (word: string, corpus: string) => + corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase()) + +const checkAgainstQuery = (query: string, corpus: string) => + query.split(' ').every((word) => wordIn(word, corpus)) + +export const searchInAny = (query: string, ...fields: string[]) => + fields.some((field) => checkAgainstQuery(query, field)) + // can't just do [StarterKit, Image...] because it doesn't work with cjs imports export const exhibitExts = [ Blockquote, diff --git a/web/components/filter-select-users.tsx b/web/components/filter-select-users.tsx index 7ce73cf8..a19ab6af 100644 --- a/web/components/filter-select-users.tsx +++ b/web/components/filter-select-users.tsx @@ -7,6 +7,7 @@ import { Menu, Transition } from '@headlessui/react' import { Avatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' +import { searchInAny } from 'common/util/parse' export function FilterSelectUsers(props: { setSelectedUsers: (users: User[]) => void @@ -35,8 +36,7 @@ export function FilterSelectUsers(props: { return ( !selectedUsers.map((user) => user.name).includes(user.name) && !ignoreUserIds.includes(user.id) && - (user.name.toLowerCase().includes(query.toLowerCase()) || - user.username.toLowerCase().includes(query.toLowerCase())) + searchInAny(query, user.name, user.username) ) }) ) diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index ea1597f2..2417403a 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -11,6 +11,7 @@ import { CreateGroupButton } from 'web/components/groups/create-group-button' import { useState } from 'react' import { useMemberGroups } from 'web/hooks/use-group' import { User } from 'common/user' +import { searchInAny } from 'common/util/parse' export function GroupSelector(props: { selectedGroup?: Group @@ -22,14 +23,10 @@ export function GroupSelector(props: { const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const [query, setQuery] = useState('') - const memberGroups = useMemberGroups(creator?.id) - const filteredGroups = memberGroups - ? query === '' - ? memberGroups - : memberGroups.filter((group) => { - return group.name.toLowerCase().includes(query.toLowerCase()) - }) - : [] + const memberGroups = useMemberGroups(creator?.id) ?? [] + const filteredGroups = memberGroups.filter((group) => + searchInAny(query, group.name) + ) if (!showSelector || !creator) { return ( diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index a2590249..a2248c2e 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -16,11 +16,6 @@ export type Sort = | 'resolve-date' | 'last-updated' -export function checkAgainstQuery(query: string, corpus: string) { - const queryWords = query.toLowerCase().split(' ') - return queryWords.every((word) => corpus.toLowerCase().includes(word)) -} - export function getSavedSort() { // TODO: this obviously doesn't work with SSR, common sense would suggest // that we should save things like this in cookies so the server has them diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index 46201c3d..92e6b69f 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -20,6 +20,7 @@ import { manaToUSD } from 'common/util/format' import { quadraticMatches } from 'common/quadratic-funding' import { Txn } from 'common/txn' import { useTracking } from 'web/hooks/use-tracking' +import { searchInAny } from 'common/util/parse' export async function getStaticProps() { const txns = await getAllCharityTxns() @@ -88,10 +89,12 @@ export default function Charity(props: { () => charities.filter( (charity) => - charity.name.toLowerCase().includes(query.toLowerCase()) || - charity.preview.toLowerCase().includes(query.toLowerCase()) || - charity.description.toLowerCase().includes(query.toLowerCase()) || - (charity.tags as string[])?.includes(query.toLowerCase()) + searchInAny( + query, + charity.name, + charity.preview, + charity.description + ) || (charity.tags as string[])?.includes(query.toLowerCase()) ), [charities, query] ) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 3ac11993..0ef8cdfe 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -1,4 +1,5 @@ import { Answer } from 'common/answer' +import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { useState } from 'react' import { ContractsGrid } from 'web/components/contract/contracts-list' @@ -28,22 +29,14 @@ export default function ContractSearchFirestore(props: { const [sort, setSort] = useState(initialSort || 'newest') const [query, setQuery] = useState(initialQuery) - const queryWords = query.toLowerCase().split(' ') - function check(corpus: string) { - return queryWords.every((word) => corpus.toLowerCase().includes(word)) - } - - let matches = (contracts ?? []).filter( - (c) => - check(c.question) || - check(c.creatorName) || - check(c.creatorUsername) || - check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) || - check( - ((c as any).answers ?? []) - .map((answer: Answer) => answer.text) - .join(' ') - ) + let matches = (contracts ?? []).filter((c) => + searchInAny( + query, + c.question, + c.creatorName, + c.lowercaseTags.map((tag) => `#${tag}`).join(' '), + ((c as any).answers ?? []).map((answer: Answer) => answer.text).join(' ') + ) ) if (sort === 'newest') { diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 9620894f..5fd564ea 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -40,10 +40,7 @@ import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' -import { - checkAgainstQuery, - getSavedSort, -} from 'web/hooks/use-sort-and-query-params' +import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' @@ -56,6 +53,7 @@ import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { OnlineUserList } from 'web/components/online-user-list' +import { searchInAny } from 'common/util/parse' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -446,9 +444,8 @@ function GroupMemberSearch(props: { members: User[]; group: Group }) { } // TODO use find-active-contracts to sort by? - const matches = sortBy(members, [(member) => member.name]).filter( - (m) => - checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) + const matches = sortBy(members, [(member) => member.name]).filter((m) => + searchInAny(query, m.name, m.username) ) const matchLimit = 25 diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 87ac1501..9e21c346 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -12,12 +12,12 @@ import { useUser } from 'web/hooks/use-user' import { groupPath, listAllGroups } from 'web/lib/firebase/groups' import { getUser, User } from 'web/lib/firebase/users' import { Tabs } from 'web/components/layout/tabs' -import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { SiteLink } from 'web/components/site-link' import clsx from 'clsx' import { Avatar } from 'web/components/avatar' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { UserLink } from 'web/components/user-page' +import { searchInAny } from 'common/util/parse' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -71,11 +71,13 @@ export default function Groups(props: { const matches = sortBy(groups, [ (group) => -1 * group.contractIds.length, (group) => -1 * group.memberIds.length, - ]).filter( - (g) => - checkAgainstQuery(query, g.name) || - checkAgainstQuery(query, g.about || '') || - checkAgainstQuery(query, creatorsDict[g.creatorId].username) + ]).filter((g) => + searchInAny( + query, + g.name, + g.about || '', + creatorsDict[g.creatorId].username + ) ) const matchesOrderedByRecentActivity = sortBy(groups, [ @@ -84,11 +86,13 @@ export default function Groups(props: { (group.mostRecentChatActivityTime ?? group.mostRecentContractAddedTime ?? group.mostRecentActivityTime), - ]).filter( - (g) => - checkAgainstQuery(query, g.name) || - checkAgainstQuery(query, g.about || '') || - checkAgainstQuery(query, creatorsDict[g.creatorId].username) + ]).filter((g) => + searchInAny( + query, + g.name, + g.about || '', + creatorsDict[g.creatorId].username + ) ) // Not strictly necessary, but makes the "hold delete" experience less laggy From eed7990c3c9e0459fafbda29046a124401a15e53 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 15 Jul 2022 16:57:58 -0600 Subject: [PATCH 194/519] Lighten unseen notifs --- web/pages/notifications.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 94c18d64..2d4d2f2b 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -46,6 +46,7 @@ import Router from 'next/router' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' +const HIGHLIGHT_CLASS = 'bg-indigo-50' export default function Notifications() { const user = useUser() @@ -240,7 +241,7 @@ function IncomeNotificationGroupItem(props: { 'relative cursor-pointer bg-white px-2 pt-6 text-sm', className, !expanded ? 'hover:bg-gray-100' : '', - highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' + highlighted && !expanded ? HIGHLIGHT_CLASS : '' )} onClick={onClickHandler} > @@ -378,7 +379,7 @@ function IncomeNotificationItem(props: { <div className={clsx( 'bg-white px-2 pt-6 text-sm sm:px-4', - highlighted && 'bg-indigo-200 hover:bg-indigo-100' + highlighted && HIGHLIGHT_CLASS )} > <a href={getSourceUrl(notification)}> @@ -447,7 +448,7 @@ function NotificationGroupItem(props: { 'relative cursor-pointer bg-white px-2 pt-6 text-sm', className, !expanded ? 'hover:bg-gray-100' : '', - highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : '' + highlighted && !expanded ? HIGHLIGHT_CLASS : '' )} onClick={onClickHandler} > @@ -593,7 +594,7 @@ function NotificationItem(props: { <div className={clsx( 'bg-white px-2 pt-6 text-sm sm:px-4', - highlighted && 'bg-indigo-200 hover:bg-indigo-100' + highlighted && HIGHLIGHT_CLASS )} > <div From 7d24a3e4a237a3e5c8f7dafd2392b78c02acf8be Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Fri, 15 Jul 2022 18:42:37 -0700 Subject: [PATCH 195/519] Inga/manalink bug fixes (#653) * fixed manalinks bug of claiming own manalink, and also rerouting to home upon claiming if not logged in * no more multiple hardcoded manalink messages --- functions/README.md | 6 ++++- functions/src/claim-manalink.ts | 3 +++ web/components/button.tsx | 7 ++++-- web/components/manalink-card.tsx | 22 +++++++++---------- .../manalinks/create-links-button.tsx | 12 +++++----- web/pages/link/[slug].tsx | 9 +++++--- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/functions/README.md b/functions/README.md index 8013fb20..97a7a33b 100644 --- a/functions/README.md +++ b/functions/README.md @@ -27,6 +27,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started 1. `$ brew install java` 2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk` + 2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud 3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options) 4. `$ mkdir firestore_export` to create a folder to store the exported database @@ -53,7 +54,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started ## Deploying -0. `$ firebase use prod` to switch to prod +0. After merging, you need to manually deploy to backend: +1. `git checkout main` +1. `git pull origin main` +1. `$ firebase use prod` to switch to prod 1. `$ firebase deploy --only functions` to push your changes live! (Future TODO: auto-deploy functions on Git push) diff --git a/functions/src/claim-manalink.ts b/functions/src/claim-manalink.ts index 3822bbf7..b534f0a3 100644 --- a/functions/src/claim-manalink.ts +++ b/functions/src/claim-manalink.ts @@ -28,6 +28,9 @@ export const claimmanalink = newEndpoint({}, async (req, auth) => { if (amount <= 0 || isNaN(amount) || !isFinite(amount)) throw new APIError(500, 'Invalid amount') + if (auth.uid === fromId) + throw new APIError(400, `You can't claim your own manalink`) + const fromDoc = firestore.doc(`users/${fromId}`) const fromSnap = await transaction.get(fromDoc) if (!fromSnap.exists) { diff --git a/web/components/button.tsx b/web/components/button.tsx index d050b81e..8cdeacdd 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -2,12 +2,13 @@ import { ReactNode } from 'react' import clsx from 'clsx' export function Button(props: { - children: ReactNode className?: string onClick?: () => void + children?: ReactNode size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' type?: 'button' | 'reset' | 'submit' + disabled?: boolean }) { const { children, @@ -16,6 +17,7 @@ export function Button(props: { size = 'md', color = 'indigo', type = 'button', + disabled = false, } = props const sizeClasses = { @@ -30,7 +32,7 @@ export function Button(props: { <button type={type} className={clsx( - 'font-md items-center justify-center rounded-md border border-transparent shadow-sm hover:transition-colors', + 'font-md items-center justify-center rounded-md border border-transparent shadow-sm hover:transition-colors disabled:cursor-not-allowed disabled:opacity-50', sizeClasses, color === 'green' && 'btn-primary text-white', color === 'red' && 'bg-red-400 text-white hover:bg-red-500', @@ -40,6 +42,7 @@ export function Button(props: { color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300', className )} + disabled={disabled} onClick={onClick} > {children} diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index fec05919..b5a79091 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -3,6 +3,8 @@ import { formatMoney } from 'common/util/format' import { fromNow } from 'web/lib/util/time' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' +import { User } from 'web/lib/firebase/users' +import { Button } from './button' export type ManalinkInfo = { expiresTime: number | null @@ -13,13 +15,13 @@ export type ManalinkInfo = { } export function ManalinkCard(props: { + user: User | null | undefined className?: string info: ManalinkInfo - defaultMessage: string isClaiming: boolean onClaim?: () => void }) { - const { className, defaultMessage, isClaiming, info, onClaim } = props + const { user, className, isClaiming, info, onClaim } = props const { expiresTime, maxUses, uses, amount, message } = info return ( <div @@ -52,16 +54,13 @@ export function ManalinkCard(props: { <div className="mb-1 text-xl text-indigo-500"> {formatMoney(amount)} </div> - <div>{message || defaultMessage}</div> + <div>{message}</div> </Col> <div className="ml-auto"> - <button - className={clsx('btn', isClaiming ? 'loading disabled' : '')} - onClick={onClaim} - > - {isClaiming ? '' : 'Claim'} - </button> + <Button onClick={onClaim} disabled={isClaiming}> + {user ? 'Claim' : 'Login'} + </Button> </div> </Row> </div> @@ -71,9 +70,8 @@ export function ManalinkCard(props: { export function ManalinkCardPreview(props: { className?: string info: ManalinkInfo - defaultMessage: string }) { - const { className, defaultMessage, info } = props + const { className, info } = props const { expiresTime, maxUses, uses, amount, message } = info return ( <div @@ -102,7 +100,7 @@ export function ManalinkCardPreview(props: { <Row className="rounded-b-lg bg-white p-2"> <Col className="text-md"> <div className="mb-1 text-indigo-500">{formatMoney(amount)}</div> - <div className="text-xs">{message || defaultMessage}</div> + <div className="text-xs">{message}</div> </Col> </Row> </div> diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index 538e794e..0d1d603e 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -66,12 +66,14 @@ function CreateManalinkForm(props: { const defaultExpire = 'week' const [expiresIn, setExpiresIn] = useState(defaultExpire) + const defaultMessage = 'from ' + user.name + const [newManalink, setNewManalink] = useState<ManalinkInfo>({ expiresTime: dayjs().add(1, defaultExpire).valueOf(), amount: 100, maxUses: 1, uses: 0, - message: '', + message: defaultMessage, }) const EXPIRE_OPTIONS = { @@ -161,7 +163,7 @@ function CreateManalinkForm(props: { <div className="form-control w-full"> <label className="label">Message</label> <Textarea - placeholder={`From ${user.name}`} + placeholder={defaultMessage} className="input input-bordered resize-none" autoFocus value={newManalink.message} @@ -189,11 +191,7 @@ function CreateManalinkForm(props: { {finishedCreating && ( <> <Title className="!my-0" text="Manalink Created!" /> - <ManalinkCardPreview - className="my-4" - defaultMessage={`From ${user.name}`} - info={newManalink} - /> + <ManalinkCardPreview className="my-4" info={newManalink} /> <Row className={clsx( 'rounded border bg-gray-50 py-2 px-3 text-sm text-gray-500 transition-colors duration-700', diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 67e7b695..8ad9850f 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -6,7 +6,6 @@ import { claimManalink } from 'web/lib/firebase/api' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' -import { useUserById } from 'web/hooks/use-user' import { firebaseLogin } from 'web/lib/firebase/users' export default function ClaimPage() { @@ -17,7 +16,6 @@ export default function ClaimPage() { const [claiming, setClaiming] = useState(false) const [error, setError] = useState<string | undefined>(undefined) - const fromUser = useUserById(manalink?.fromId) if (!manalink) { return <></> } @@ -33,7 +31,7 @@ export default function ClaimPage() { <div className="mx-auto max-w-xl"> <Title text={`Claim M$${manalink.amount} mana`} /> <ManalinkCard - defaultMessage={fromUser?.name || 'Enjoy this mana!'} + user={user} info={info} isClaiming={claiming} onClaim={async () => { @@ -41,6 +39,11 @@ export default function ClaimPage() { try { if (user == null) { await firebaseLogin() + setClaiming(false) + return + } + if (user?.id == manalink.fromId) { + throw new Error("You can't claim your own manalink.") } await claimManalink({ slug: manalink.slug }) user && router.push(`/${user.username}?claimed-mana=yes`) From 6d8ad74b4d8fbf5116830088a2a4be49f379b881 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 16 Jul 2022 13:10:59 -0500 Subject: [PATCH 196/519] Redeem shares of makers after sellshares --- functions/src/place-bet.ts | 6 +++--- functions/src/sell-shares.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index b8c0ca0e..97ff9780 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -6,7 +6,7 @@ import { Query, Transaction, } from 'firebase-admin/firestore' -import { groupBy, mapValues, sumBy } from 'lodash' +import { groupBy, mapValues, sumBy, uniq } from 'lodash' import { APIError, newEndpoint, validate } from './api' import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' @@ -153,10 +153,10 @@ export const placebet = newEndpoint({}, async (req, auth) => { log('Main transaction finished.') if (result.newBet.amount !== 0) { - const userIds = [ + const userIds = uniq([ auth.uid, ...(result.makers ?? []).map((maker) => maker.bet.userId), - ] + ]) await Promise.all(userIds.map((userId) => redeemShares(userId, contractId))) log('Share redemption transaction finished.') } diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 3407760b..40ea0f4a 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -1,4 +1,4 @@ -import { sumBy } from 'lodash' +import { sumBy, uniq } from 'lodash' import * as admin from 'firebase-admin' import { z } from 'zod' @@ -7,11 +7,12 @@ import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' import { User } from '../../common/user' import { getCpmmSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' -import { getValues } from './utils' +import { getValues, log } from './utils' import { Bet } from '../../common/bet' import { floatingLesserEqual } from '../../common/util/math' import { getUnfilledBetsQuery, updateMakers } from './place-bet' import { FieldValue } from 'firebase-admin/firestore' +import { redeemShares } from './redeem-shares' const bodySchema = z.object({ contractId: z.string(), @@ -23,7 +24,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const { contractId, shares, outcome } = validate(bodySchema, req.body) // Run as transaction to prevent race conditions. - return await firestore.runTransaction(async (transaction) => { + const result = await firestore.runTransaction(async (transaction) => { const contractDoc = firestore.doc(`contracts/${contractId}`) const userDoc = firestore.doc(`users/${auth.uid}`) const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid) @@ -97,8 +98,14 @@ export const sellshares = newEndpoint({}, async (req, auth) => { }) ) - return { status: 'success' } + return { newBet, makers } }) + + const userIds = uniq(result.makers.map((maker) => maker.bet.userId)) + await Promise.all(userIds.map((userId) => redeemShares(userId, contractId))) + log('Share redemption transaction finished.') + + return { status: 'success' } }) const firestore = admin.firestore() From 916618be31b7240aa155d8cd0425b3e24883bb9e Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 09:30:23 -0700 Subject: [PATCH 197/519] Disable quotation marks in quotes --- web/components/editor.tsx | 4 ++-- web/tailwind.config.js | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 4b3e2cce..9adc0c9f 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,7 +21,7 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' const proseClass = clsx( - 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' + 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless' ) export function useTextEditor(props: { @@ -94,7 +94,7 @@ export function TextEditor(props: { return ( <> {/* hide placeholder when focused */} - <div className="w-full [&:focus-within_p.is-empty]:before:content-none"> + <div className="[&:focus-within_p.is-empty]:before:content-none w-full"> {editor && ( <FloatingMenu editor={editor} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 31c0c533..0a1616b6 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -17,6 +17,14 @@ module.exports = { backgroundImage: { 'world-trading': "url('/world-trading-background.webp')", }, + typography: { + quoteless: { + css: { + 'blockquote p:first-of-type::before': { content: 'none' }, + 'blockquote p:first-of-type::after': { content: 'none' }, + }, + }, + }, }, }, plugins: [ From 349772a2f9ddee8d6aa34a648c5889d41b40d3bb Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 10:16:18 -0700 Subject: [PATCH 198/519] Description typography: font-light, text-base --- web/components/editor.tsx | 2 +- web/pages/_document.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 9adc0c9f..06a3c2a3 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,7 +21,7 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' const proseClass = clsx( - 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless' + 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless font-light' ) export function useTextEditor(props: { diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index f1a7ccab..2ff0b494 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -14,7 +14,7 @@ export default function Document() { crossOrigin="true" /> <link - href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@400;600;700&display=swap" + href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@300;400;600;700&display=swap" rel="stylesheet" /> <link From 1bc49dc0a29cd39893cef441260555113f99e065 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 10:19:53 -0700 Subject: [PATCH 199/519] Tweak placeholder copy --- web/components/editor.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 06a3c2a3..bcd49c63 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -98,15 +98,16 @@ export function TextEditor(props: { {editor && ( <FloatingMenu editor={editor} - className="w-full text-sm text-slate-300" + className="-ml-2 mr-2 w-full text-sm text-slate-300" > - Type <em>*anything*</em> or even paste or{' '} + Type <em>*markdown*</em>. Paste or{' '} <FileUploadButton className="link text-blue-300" onFiles={upload.mutate} > - upload an image - </FileUploadButton> + upload + </FileUploadButton>{' '} + images! </FloatingMenu> )} <EditorContent editor={editor} /> From 32cb19d01f1e521613a2966ea1c58dcf4a6e4c4b Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 11:37:28 -0700 Subject: [PATCH 200/519] Randomize image upload path to avoid collisions --- web/components/editor.tsx | 1 + web/lib/firebase/storage.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index bcd49c63..ec4a291c 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -123,6 +123,7 @@ export function TextEditor(props: { const useUploadMutation = (editor: Editor | null) => useMutation( (files: File[]) => + // TODO: Images should be uploaded under a particular username Promise.all(files.map((file) => uploadImage('default', file))), { onSuccess(urls) { diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index 2fc2ccc7..4918a99c 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -1,4 +1,5 @@ import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage' +import { nanoid } from 'nanoid' import { storage } from './init' // TODO: compress large images @@ -7,7 +8,10 @@ export const uploadImage = async ( file: File, onProgress?: (progress: number, isRunning: boolean) => void ) => { - const storageRef = ref(storage, `user-images/${username}/${file.name}`) + // Replace filename with a nanoid to avoid collisions + const [, ext] = file.name.split('.') + const filename = `${nanoid(10)}.${ext}` + const storageRef = ref(storage, `user-images/${username}/${filename}`) const uploadTask = uploadBytesResumable(storageRef, file) let resolvePromise: (url: string) => void From 7b6344d976989f272adf578847ee34c111e11809 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 16 Jul 2022 14:21:24 -0500 Subject: [PATCH 201/519] Order book button opens full table of limit orders in dialog --- web/components/bet-panel.tsx | 2 +- web/components/layout/modal.tsx | 13 +++- web/components/limit-bets.tsx | 112 ++++++++++++++++++++++---------- 3 files changed, 89 insertions(+), 38 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 7188c19a..8aa8b60a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -405,7 +405,7 @@ function QuickOrLimitBet(props: { return ( <Row className="align-center mb-4 justify-between"> <div className="text-4xl">Bet</div> - <Row className="mt-2 items-center gap-2"> + <Row className="mt-1 items-center gap-2"> <PillButton selected={!isLimitOrder} onSelect={() => { diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index 7a320f24..af2b66de 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -7,9 +7,17 @@ export function Modal(props: { children: ReactNode open: boolean setOpen: (open: boolean) => void + size?: 'sm' | 'md' | 'lg' | 'xl' className?: string }) { - const { children, open, setOpen, className } = props + const { children, open, setOpen, size = 'md', className } = props + + const sizeClass = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-2xl', + xl: 'max-w-5xl', + }[size] return ( <Transition.Root show={open} as={Fragment}> @@ -49,7 +57,8 @@ export function Modal(props: { > <div className={clsx( - 'inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle', + 'my-8 mx-6 inline-block w-full transform overflow-hidden text-left align-bottom transition-all sm:align-middle', + sizeClass, className )} > diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 93647a5e..29d010e2 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx' import { LimitBet } from 'common/bet' import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' import { getFormattedMappedValue } from 'common/pseudo-numeric' @@ -8,10 +7,14 @@ import { useState } from 'react' import { useUser, useUserById } from 'web/hooks/use-user' import { cancelBet } from 'web/lib/firebase/api' import { Avatar } from './avatar' +import { Button } from './button' import { Col } from './layout/col' -import { Tabs } from './layout/tabs' +import { Modal } from './layout/modal' +import { Row } from './layout/row' import { LoadingIndicator } from './loading-indicator' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' +import { Subtitle } from './subtitle' +import { Title } from './title' export function LimitBets(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -28,40 +31,36 @@ export function LimitBets(props: { const yourBets = sortedBets.filter((bet) => bet.userId === user?.id) return ( - <Col - className={clsx( - className, - 'gap-2 overflow-hidden rounded bg-white px-4 py-3' + <Col className={className}> + {yourBets.length === 0 && ( + <OrderBookButton + className="self-end" + limitBets={sortedBets} + contract={contract} + /> + )} + + {yourBets.length > 0 && ( + <Col + className={'mt-4 gap-2 overflow-hidden rounded bg-white px-4 py-3'} + > + <Row className="mt-2 mb-4 items-center justify-between"> + <Subtitle className="!mt-0 !mb-0" text="Your limit orders" /> + + <OrderBookButton + className="self-end" + limitBets={sortedBets} + contract={contract} + /> + </Row> + + <LimitOrderTable + limitBets={yourBets} + contract={contract} + isYou={true} + /> + </Col> )} - > - <Tabs - tabs={[ - ...(yourBets.length > 0 - ? [ - { - title: 'Your limit orders', - content: ( - <LimitOrderTable - limitBets={yourBets} - contract={contract} - isYou={true} - /> - ), - }, - ] - : []), - { - title: 'All limit orders', - content: ( - <LimitOrderTable - limitBets={sortedBets} - contract={contract} - isYou={false} - /> - ), - }, - ]} - /> </Col> ) } @@ -153,3 +152,46 @@ function LimitBet(props: { </tr> ) } + +export function OrderBookButton(props: { + limitBets: LimitBet[] + contract: CPMMBinaryContract | PseudoNumericContract + className?: string +}) { + const { limitBets, contract, className } = props + const [open, setOpen] = useState(false) + + const yesBets = limitBets.filter((bet) => bet.outcome === 'YES') + const noBets = limitBets.filter((bet) => bet.outcome === 'NO').reverse() + + return ( + <> + <Button + className={className} + onClick={() => setOpen(true)} + size="xs" + color="blue" + > + Order book + </Button> + + <Modal open={open} setOpen={setOpen} size="lg"> + <Col className="rounded bg-white p-4 py-6"> + <Title className="!mt-0" text="Order book" /> + <Col className="justify-start gap-2 lg:flex-row lg:items-start"> + <LimitOrderTable + limitBets={yesBets} + contract={contract} + isYou={false} + /> + <LimitOrderTable + limitBets={noBets} + contract={contract} + isYou={false} + /> + </Col> + </Col> + </Modal> + </> + ) +} From 7feacbd96136ecae475aa18c07fe7d1dcd0cec89 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 16 Jul 2022 14:37:03 -0500 Subject: [PATCH 202/519] Tweak wording --- web/components/bet-panel.tsx | 6 +++++- web/components/limit-bets.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 8aa8b60a..351b012e 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -385,7 +385,11 @@ function BuyPanel(props: { )} onClick={betDisabled ? undefined : submitBet} > - {isSubmitting ? 'Submitting...' : 'Submit bet'} + {isSubmitting + ? 'Submitting...' + : isLimitOrder + ? 'Submit order' + : 'Submit bet'} </button> )} diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 29d010e2..70e06d79 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -45,7 +45,7 @@ export function LimitBets(props: { className={'mt-4 gap-2 overflow-hidden rounded bg-white px-4 py-3'} > <Row className="mt-2 mb-4 items-center justify-between"> - <Subtitle className="!mt-0 !mb-0" text="Your limit orders" /> + <Subtitle className="!mt-0 !mb-0" text="Your orders" /> <OrderBookButton className="self-end" From a3975080a1421654c5514626ae5b9f57c41122fd Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sat, 16 Jul 2022 14:50:07 -0500 Subject: [PATCH 203/519] adjust sig figs --- common/util/format.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index decdd55d..7dc1a341 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -36,15 +36,17 @@ export function formatPercent(zeroToOne: number) { // Eg 1234567.89 => 1.23M; 5678 => 5.68K export function formatLargeNumber(num: number, sigfigs = 2): string { const absNum = Math.abs(num) - if (absNum < 1000) { - return '' + Number(num.toPrecision(sigfigs)) - } + if (absNum < 1) return num.toPrecision(sigfigs) + + if (absNum < 100) return num.toPrecision(2) + if (absNum < 1000) return num.toPrecision(3) + if (absNum < 10000) return num.toPrecision(4) const suffix = ['', 'K', 'M', 'B', 'T', 'Q'] - const suffixIdx = Math.floor(Math.log10(absNum) / 3) - const suffixStr = suffix[suffixIdx] - const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs) - return `${Number(numStr)}${suffixStr}` + const i = Math.floor(Math.log10(absNum) / 3) + + const numStr = (num / Math.pow(10, 3 * i)).toPrecision(sigfigs) + return `${numStr}${suffix[i]}` } export function toCamelCase(words: string) { From 60f4e43cf3d2bc8e628e1da0f33634fac4bd78d2 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 12:50:49 -0700 Subject: [PATCH 204/519] Prettier fix --- web/components/editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index ec4a291c..43d69c26 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -94,7 +94,7 @@ export function TextEditor(props: { return ( <> {/* hide placeholder when focused */} - <div className="[&:focus-within_p.is-empty]:before:content-none w-full"> + <div className="w-full [&:focus-within_p.is-empty]:before:content-none"> {editor && ( <FloatingMenu editor={editor} From bae55828a1017d4d6df2f7b9cf34329df5671228 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 16 Jul 2022 12:52:59 -0700 Subject: [PATCH 205/519] Simplify Firestore isAdmin rule --- firestore.rules | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/firestore.rules b/firestore.rules index 196c5992..84c3e990 100644 --- a/firestore.rules +++ b/firestore.rules @@ -6,10 +6,12 @@ service cloud.firestore { match /databases/{database}/documents { function isAdmin() { - return request.auth.uid == 'igi2zGXsfxYPgB0DJTXVJVmwCOr2' // Austin - || request.auth.uid == '5LZ4LgYuySdL1huCWe7bti02ghx2' // James - || request.auth.uid == 'tlmGNz9kjXc2EteizMORes4qvWl2' // Stephen - || request.auth.uid == 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // Manifold + return request.auth.token.email in [ + 'akrolsmir@gmail.com', + 'jahooma@gmail.com', + 'taowell@gmail.com', + 'manticmarkets@gmail.com' + ] } match /stats/stats { From 1edc1993e14a58449569dc39ef6987a1c64f362c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 16 Jul 2022 14:58:24 -0500 Subject: [PATCH 206/519] Cache follows in localstorage --- web/hooks/use-follows.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/hooks/use-follows.ts b/web/hooks/use-follows.ts index e5a074d6..2a8caaea 100644 --- a/web/hooks/use-follows.ts +++ b/web/hooks/use-follows.ts @@ -5,7 +5,16 @@ export const useFollows = (userId: string | null | undefined) => { const [followIds, setFollowIds] = useState<string[] | undefined>() useEffect(() => { - if (userId) return listenForFollows(userId, setFollowIds) + if (userId) { + const key = `follows:${userId}` + const follows = localStorage.getItem(key) + if (follows) setFollowIds(JSON.parse(follows)) + + return listenForFollows(userId, (follows) => { + setFollowIds(follows) + localStorage.setItem(key, JSON.stringify(follows)) + }) + } }, [userId]) return followIds From c1d77f48e36cfd4dd3f27f6e4f9a12198b7ebde4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 16 Jul 2022 18:56:21 -0500 Subject: [PATCH 207/519] Fix tag filter --- web/components/contract-search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index f7972a3d..78b28a94 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -121,6 +121,7 @@ export function ContractSearch(props: { additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` : '', + additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', additionalFilter?.groupSlug ? `groupSlugs:${additionalFilter.groupSlug}` : '', From 07bfdadd25fa1ea75a8c6f3f774c858acf39a9f9 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 17 Jul 2022 14:40:14 -0500 Subject: [PATCH 208/519] remove OnlineUserList b/c of responsiveness issues --- web/pages/group/[...slugs]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 5fd564ea..df1c7e2f 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -178,7 +178,7 @@ export default function GroupPage(props: { user={user} isMember={!!isMember} /> - <OnlineUserList users={members} /> + {/* <OnlineUserList users={members} /> */} </Col> ) const leaderboard = ( From b5f0b5889827fbb5f864418110d83532558199dd Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 17 Jul 2022 15:17:31 -0500 Subject: [PATCH 209/519] usePing --- web/components/nav/sidebar.tsx | 15 ++++----------- web/hooks/use-ping.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 web/hooks/use-ping.ts diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 55f11b16..5c3b9128 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -13,7 +13,7 @@ import clsx from 'clsx' import Link from 'next/link' import { useRouter } from 'next/router' import { usePrivateUser, useUser } from 'web/hooks/use-user' -import { firebaseLogout, updateUser, User } from 'web/lib/firebase/users' +import { firebaseLogout, User } from 'web/lib/firebase/users' import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' import { ProfileSummary } from './profile-menu' @@ -193,10 +193,13 @@ export default function Sidebar(props: { className?: string }) { const user = useUser() const privateUser = usePrivateUser(user?.id) + // usePing(user?.id) + const navigationOptions = !user ? signedOutNavigation : getNavigation() const mobileNavigationOptions = !user ? signedOutMobileNavigation : signedInMobileNavigation + const memberItems = ( useMemberGroups( user?.id, @@ -208,16 +211,6 @@ export default function Sidebar(props: { className?: string }) { href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, })) - useEffect(() => { - if (!user) return - const pingInterval = setInterval(() => { - updateUser(user.id, { - lastPingTime: Date.now(), - }) - }, 1000 * 30) - return () => clearInterval(pingInterval) - }, [user]) - return ( <nav aria-label="Sidebar" className={className}> <ManifoldLogo className="py-6" twoLine /> diff --git a/web/hooks/use-ping.ts b/web/hooks/use-ping.ts new file mode 100644 index 00000000..31daa770 --- /dev/null +++ b/web/hooks/use-ping.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { updateUser } from 'web/lib/firebase/users' + +export const usePing = (userId: string | undefined) => { + useEffect(() => { + if (!userId) return + + const pingInterval = setInterval(() => { + updateUser(userId, { + lastPingTime: Date.now(), + }) + }, 1000 * 30) + + return () => clearInterval(pingInterval) + }, [userId]) +} From 281b7122583cf96bdb487db783d0c36c28f308b1 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 17 Jul 2022 15:56:39 -0500 Subject: [PATCH 210/519] move group chat to sidebar on desktop --- web/components/page.tsx | 10 ++++-- web/pages/group/[...slugs]/index.tsx | 47 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/web/components/page.tsx b/web/components/page.tsx index e76a4dc2..40cbf7f7 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -8,9 +8,11 @@ export function Page(props: { rightSidebar?: ReactNode suspend?: boolean className?: string + rightSidebarClassName?: string children?: ReactNode }) { - const { children, rightSidebar, suspend, className } = props + const { children, rightSidebar, suspend, className, rightSidebarClassName } = + props const bottomBarPadding = 'pb-[58px] lg:pb-0 ' return ( @@ -37,7 +39,11 @@ export function Page(props: { <div className="block xl:hidden">{rightSidebar}</div> </main> <aside className="hidden xl:col-span-3 xl:block"> - <div className="sticky top-4 space-y-4">{rightSidebar}</div> + <div + className={clsx('sticky top-4 space-y-4', rightSidebarClassName)} + > + {rightSidebar} + </div> </aside> </div> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index df1c7e2f..c914b7f0 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -52,8 +52,8 @@ import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' -import { OnlineUserList } from 'web/components/online-user-list' import { searchInAny } from 'common/util/parse' +import { useWindowSize } from 'web/hooks/use-window-size' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -164,6 +164,9 @@ export default function GroupPage(props: { writeReferralInfo(creator.username, undefined, referrer, group?.slug) }, [user, creator, group, router]) + const { width } = useWindowSize() + const showChatSidebar = (width ?? 1280) >= 1280 + if (group === null || !groupSubpages.includes(page) || slugs[2]) { return <Custom404 /> } @@ -171,16 +174,6 @@ export default function GroupPage(props: { const isCreator = user && group && user.id === group.creatorId const isMember = user && memberIds.includes(user.id) - const rightSidebar = ( - <Col className="mt-6 hidden xl:block"> - <JoinOrAddQuestionsButtons - group={group} - user={user} - isMember={!!isMember} - /> - {/* <OnlineUserList users={members} /> */} - </Col> - ) const leaderboard = ( <Col> <GroupLeaderboards @@ -206,22 +199,25 @@ export default function GroupPage(props: { </Col> ) + const chatTab = group.chatDisabled ? ( + <></> + ) : ( + <Col className=""> + {messages ? ( + <GroupChat messages={messages} user={user} group={group} tips={tips} /> + ) : ( + <LoadingIndicator /> + )} + </Col> + ) + const tabs = [ - ...(group.chatDisabled + ...(group.chatDisabled || showChatSidebar ? [] : [ { title: 'Chat', - content: messages ? ( - <GroupChat - messages={messages} - user={user} - group={group} - tips={tips} - /> - ) : ( - <LoadingIndicator /> - ), + content: chatTab, href: groupPath(group.slug, GROUP_CHAT_SLUG), }, ]), @@ -251,8 +247,13 @@ export default function GroupPage(props: { }, ] const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) + return ( - <Page rightSidebar={rightSidebar} className="!pb-0"> + <Page + rightSidebar={showChatSidebar ? chatTab : undefined} + rightSidebarClassName="!top-0" + className="!max-w-none !pb-0" + > <SEO title={group.name} description={`Created by ${creator.name}. ${group.about}`} From f393246e4faae3508a85e7aaf25e09e8a5ddf52d Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 17 Jul 2022 22:22:44 -0700 Subject: [PATCH 211/519] Let users edit descriptions and questions (#654) * Use rich text editor on the description * Write a new line to description when the question is changed * Stop showing categories * Allow anyone to edit their own question --- .../contract/contract-description.tsx | 211 ++++++++++-------- web/components/editor.tsx | 6 +- 2 files changed, 119 insertions(+), 98 deletions(-) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index b2f839e9..d9864186 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -2,16 +2,17 @@ import clsx from 'clsx' import dayjs from 'dayjs' import { useState } from 'react' import Textarea from 'react-expanding-textarea' -import { CATEGORY_LIST } from '../../../common/categories' -import { Contract } from 'common/contract' -import { parseTags, exhibitExts } from 'common/util/parse' +import { Contract, MAX_DESCRIPTION_LENGTH } from 'common/contract' +import { exhibitExts, parseTags } from 'common/util/parse' import { useAdmin } from 'web/hooks/use-admin' import { updateContract } from 'web/lib/firebase/contracts' import { Row } from '../layout/row' -import { TagsList } from '../tags-list' import { Content } from '../editor' -import { Editor } from '@tiptap/react' +import { TextEditor, useTextEditor } from 'web/components/editor' +import { Button } from '../button' +import { Spacer } from '../layout/spacer' +import { Editor, Content as ContentType } from '@tiptap/react' export function ContractDescription(props: { contract: Contract @@ -19,20 +20,39 @@ export function ContractDescription(props: { className?: string }) { const { contract, isCreator, className } = props - const descriptionTimestamp = () => `${dayjs().format('MMM D, h:mma')}: ` const isAdmin = useAdmin() + return ( + <div className={clsx('mt-2 text-gray-700', className)}> + {isCreator || isAdmin ? ( + <RichEditContract contract={contract} /> + ) : ( + <Content content={contract.description} /> + )} + {isAdmin && !isCreator && ( + <div className="mt-2 text-red-400">(👆 admin powers)</div> + )} + </div> + ) +} - const desc = contract.description ?? '' +function editTimestamp() { + return `${dayjs().format('MMM D, h:mma')}: ` +} - // Append the new description (after a newline) - async function saveDescription(newText: string) { - const editor = new Editor({ content: desc, extensions: exhibitExts }) - editor - .chain() - .focus('end') - .insertContent('<br /><br />') - .insertContent(newText.trim()) - .run() +function RichEditContract(props: { contract: Contract }) { + const { contract } = props + const [editing, setEditing] = useState(false) + const [editingQ, setEditingQ] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const { editor, upload } = useTextEditor({ + max: MAX_DESCRIPTION_LENGTH, + defaultValue: contract.description, + disabled: isSubmitting, + }) + + async function saveDescription() { + if (!editor) return const tags = parseTags( `${editor.getText()} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` @@ -46,76 +66,92 @@ export function ContractDescription(props: { }) } - const { tags } = contract - const categories = tags.filter((tag) => - CATEGORY_LIST.includes(tag.toLowerCase()) - ) - - return ( - <div - className={clsx( - 'mt-2 whitespace-pre-line break-words text-gray-700', - className - )} - > - <Content content={desc} /> - - {categories.length > 0 && ( - <div className="mt-4"> - <TagsList tags={categories} noLabel /> - </div> - )} - - <br /> - - {isCreator && ( - <EditContract - // Note: Because descriptionTimestamp is called once, later edits use - // a stale timestamp. Ideally this is a function that gets called when - // isEditing is set to true. - text={descriptionTimestamp()} - onSave={saveDescription} - buttonText="Add to description" - /> - )} - {isAdmin && ( - <EditContract - text={contract.question} - onSave={(question) => updateContract(contract.id, { question })} - buttonText="ADMIN: Edit question" - /> - )} - {/* {isAdmin && ( - <EditContract - text={contract.createdTime.toString()} - onSave={(time) => - updateContract(contract.id, { createdTime: Number(time) }) - } - buttonText="ADMIN: Edit createdTime" - /> - )} */} - </div> + return editing ? ( + <> + <TextEditor editor={editor} upload={upload} /> + <Spacer h={2} /> + <Row className="gap-2"> + <Button + onClick={async () => { + setIsSubmitting(true) + await saveDescription() + setEditing(false) + setIsSubmitting(false) + }} + > + Save + </Button> + <Button color="gray" onClick={() => setEditing(false)}> + Cancel + </Button> + </Row> + </> + ) : ( + <> + <Content content={contract.description} /> + <Spacer h={2} /> + <Row className="gap-2"> + <Button + color="gray" + onClick={() => { + setEditing(true) + editor + ?.chain() + .setContent(contract.description) + .focus('end') + .insertContent(`<p>${editTimestamp()}</p>`) + .run() + }} + > + Edit description + </Button> + <Button color="gray" onClick={() => setEditingQ(true)}> + Edit question + </Button> + </Row> + <EditQuestion + contract={contract} + editing={editingQ} + setEditing={setEditingQ} + /> + </> ) } -function EditContract(props: { - text: string - onSave: (newText: string) => void - buttonText: string +function EditQuestion(props: { + contract: Contract + editing: boolean + setEditing: (editing: boolean) => void }) { - const [text, setText] = useState(props.text) - const [editing, setEditing] = useState(false) - const onSave = (newText: string) => { + const { contract, editing, setEditing } = props + const [text, setText] = useState(contract.question) + + function questionChanged(oldQ: string, newQ: string) { + return `<p>${editTimestamp()}<s>${oldQ}</s> → ${newQ}</p>` + } + + function joinContent(oldContent: ContentType, newContent: string) { + const editor = new Editor({ content: oldContent, extensions: exhibitExts }) + editor.chain().focus('end').insertContent(newContent).run() + return editor.getJSON() + } + + const onSave = async (newText: string) => { setEditing(false) - setText(props.text) // Reset to original text - props.onSave(newText) + await updateContract(contract.id, { + question: newText, + description: joinContent( + contract.description, + questionChanged(contract.question, newText) + ), + }) } return editing ? ( <div className="mt-4"> <Textarea className="textarea textarea-bordered mb-1 h-24 w-full resize-none" - rows={3} + rows={2} value={text} onChange={(e) => setText(e.target.value || '')} autoFocus @@ -130,28 +166,11 @@ function EditContract(props: { }} /> <Row className="gap-2"> - <button - className="btn btn-neutral btn-outline btn-sm" - onClick={() => onSave(text)} - > - Save - </button> - <button - className="btn btn-error btn-outline btn-sm" - onClick={() => setEditing(false)} - > + <Button onClick={() => onSave(text)}>Save</Button> + <Button color="gray" onClick={() => setEditing(false)}> Cancel - </button> + </Button> </Row> </div> - ) : ( - <Row> - <button - className="btn btn-neutral btn-outline btn-xs mt-4" - onClick={() => setEditing(true)} - > - {props.buttonText} - </button> - </Row> - ) + ) : null } diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 43d69c26..fabce934 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,7 +21,7 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' const proseClass = clsx( - 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless font-light' + 'prose prose-p:my-2 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless font-light' ) export function useTextEditor(props: { @@ -155,7 +155,9 @@ function RichContent(props: { content: JSONContent }) { export function Content(props: { content: JSONContent | string }) { const { content } = props return typeof content === 'string' ? ( - <Linkify text={content} /> + <div className="whitespace-pre-line break-words"> + <Linkify text={content} /> + </div> ) : ( <RichContent content={content} /> ) From a247e6d0de37c5cf6dacf7ed5e548b8deba5ca85 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 18 Jul 2022 07:38:02 -0600 Subject: [PATCH 212/519] Default to created time if no chat activity --- web/lib/firebase/groups.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index e49b012a..6dfc1b88 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -68,10 +68,10 @@ export function listenForMemberGroups( const q = query(groups, where('memberIds', 'array-contains', userId)) const sorter = (group: Group) => { if (sort?.by === 'mostRecentChatActivityTime') { - return group.mostRecentChatActivityTime ?? group.mostRecentActivityTime + return group.mostRecentChatActivityTime ?? group.createdTime } if (sort?.by === 'mostRecentContractAddedTime') { - return group.mostRecentContractAddedTime ?? group.mostRecentActivityTime + return group.mostRecentContractAddedTime ?? group.createdTime } return group.mostRecentActivityTime } From 906cfc29c88a428d07a573555f37bdc947750da9 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 18 Jul 2022 07:59:21 -0600 Subject: [PATCH 213/519] Endswith=>includes to handle sort query in group chat --- functions/src/create-notification.ts | 1 + web/components/nav/sidebar.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 4c42b00e..56493043 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -411,6 +411,7 @@ export const createGroupCommentNotification = async ( group: Group, idempotencyKey: string ) => { + if (toUserId === fromUser.id) return const notificationRef = firestore .collection(`/users/${toUserId}/notifications`) .doc(idempotencyKey) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 5c3b9128..821d1397 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -294,7 +294,7 @@ function GroupsList(props: { if ( notification.isSeenOnHref === currentPage || // Old chat style group chat notif ended just with the group slug - notification.isSeenOnHref?.endsWith(currentPageGroupSlug) || + notification.isSeenOnHref?.includes(currentPageGroupSlug) || // They're on the home page, so if they've a chat notif, they're seeing the chat (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && currentPage.endsWith(currentPageGroupSlug)) From d012561c50819687a6490271fa10da9e1096d8b2 Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Mon, 18 Jul 2022 15:13:16 +0100 Subject: [PATCH 214/519] Create 500-mana.html (#658) --- functions/src/email-templates/500-mana.html | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 functions/src/email-templates/500-mana.html diff --git a/functions/src/email-templates/500-mana.html b/functions/src/email-templates/500-mana.html new file mode 100644 index 00000000..5f0c450e --- /dev/null +++ b/functions/src/email-templates/500-mana.html @@ -0,0 +1,29 @@ +<iframe + style="border: 0px; width: 100%; height: 100%" + seamless + sandbox + srcdoc='<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title>7th Day Anniversary Gift!

Running low on Mana? Click the link below to recieve a one time gift of 500 Mana!

Did you know, besides making correct predictions, there are plenty of other ways to earn Mana?

  • Recieving tips on comments
  • Unique trader bonus for each user who bets on you markets
  • Reffering friends (click the share button on  a market or group!)
  • Reporting bugs and giving feedback

 

Cheers,

David from Manifold

 

{messages ? ( @@ -211,8 +211,19 @@ export default function GroupPage(props: { ) + const questionsTab = ( + + ) + const tabs = [ - ...(group.chatDisabled || showChatSidebar + ...(!showChatTab ? [] : [ { @@ -223,16 +234,7 @@ export default function GroupPage(props: { ]), { title: 'Questions', - content: ( - - ), + content: questionsTab, href: groupPath(group.slug, 'questions'), }, { @@ -251,8 +253,8 @@ export default function GroupPage(props: { return ( Date: Mon, 18 Jul 2022 14:52:28 -0600 Subject: [PATCH 220/519] Update firestore rules for question editing --- firestore.rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firestore.rules b/firestore.rules index 63df4d16..96378d8b 100644 --- a/firestore.rules +++ b/firestore.rules @@ -76,7 +76,7 @@ service cloud.firestore { allow update: if request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); allow update: if request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['description', 'closeTime']) + .hasOnly(['description', 'closeTime', 'question']) && resource.data.creatorId == request.auth.uid; allow update: if isAdmin(); match /comments/{commentId} { From e2a72dd0a23d8721ae4229b03842c58574ecdd6d Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 18 Jul 2022 15:01:50 -0600 Subject: [PATCH 221/519] Fix /create date input --- web/pages/create.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 8d4c3662..a3801223 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -379,12 +379,10 @@ export function NewContract(props: { type={'date'} className="input input-bordered mt-4" onClick={(e) => e.stopPropagation()} - onChange={(e) => - setCloseDate(dayjs(e.target.value).format('YYYY-MM-DD') || '') - } + onChange={(e) => setCloseDate(e.target.value)} min={Date.now()} disabled={isSubmitting} - value={dayjs(closeDate).format('YYYY-MM-DD')} + value={closeDate} /> Date: Mon, 18 Jul 2022 14:02:27 -0700 Subject: [PATCH 222/519] Make description text style more consistent - links and blockquotes have light font weight, like other text - font size in editor matches font size in description - old descriptions have same style as new - placeholder text matches editor style - decrease line-height a bit --- web/components/editor.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index fabce934..7063fa42 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,7 +21,8 @@ import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' const proseClass = clsx( - 'prose prose-p:my-2 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless font-light' + 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', + 'font-light prose-a:font-light prose-blockquote:font-light' ) export function useTextEditor(props: { @@ -34,7 +35,7 @@ export function useTextEditor(props: { const editorClass = clsx( proseClass, - 'box-content min-h-[6em] textarea textarea-bordered' + 'box-content min-h-[6em] textarea textarea-bordered text-base' ) const editor = useEditor({ @@ -98,7 +99,7 @@ export function TextEditor(props: { {editor && ( Type *markdown*. Paste or{' '} +
) : ( From 47a27bf3febd238c6acd89e1817826076b85dee6 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Mon, 18 Jul 2022 15:55:17 -0700 Subject: [PATCH 223/519] Label "Since June" for users who had an account prior to 2022-06-20 (#659) --- web/components/portfolio/portfolio-value-section.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 22f1478f..1fabbd06 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -15,6 +15,13 @@ export const PortfolioValueSection = memo( const lastPortfolioMetrics = last(portfolioHistory) const [portfolioPeriod, setPortfolioPeriod] = useState('allTime') + // PATCH: If portfolio history started on June 1st, then we label it as "Since June" + // instead of "All time" + const allTimeLabel = + portfolioHistory[0].timestamp < Date.parse('2022-06-20T00:00:00.000Z') + ? 'Since June' + : 'All time' + if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { return <> } @@ -39,7 +46,7 @@ export const PortfolioValueSection = memo( setPortfolioPeriod(e.target.value as Period) }} > - + From 61a21d34b2501a315f7ac2fa10bea24a52c52b96 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 18 Jul 2022 18:19:30 -0500 Subject: [PATCH 224/519] Order limit bets in sorted order on mobile --- web/components/limit-bets.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 70e06d79..7e32db25 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -178,7 +178,7 @@ export function OrderBookButton(props: {
- <Col className="justify-start gap-2 lg:flex-row lg:items-start"> + <Row className="hidden items-start justify-start gap-2 md:flex"> <LimitOrderTable limitBets={yesBets} contract={contract} @@ -189,6 +189,13 @@ export function OrderBookButton(props: { contract={contract} isYou={false} /> + </Row> + <Col className="md:hidden"> + <LimitOrderTable + limitBets={limitBets} + contract={contract} + isYou={false} + /> </Col> </Col> </Modal> From f2a7a145e4296fad2aba467499587279a41c1b43 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 18 Jul 2022 16:37:46 -0700 Subject: [PATCH 225/519] Add React key prop to homepage filter widget (#661) --- web/components/contract-search.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 78b28a94..013208d8 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -188,7 +188,11 @@ export function ContractSearch(props: { <Row className="gap-2"> {toPairs<filter>(filterOptions).map(([label, f]) => { return ( - <PillButton selected={filter === f} onSelect={() => setFilter(f)}> + <PillButton + key={f} + selected={filter === f} + onSelect={() => setFilter(f)} + > {label} </PillButton> ) From 8793288dc8e324f193d1477ec6e4ea829b5c09ea Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 18 Jul 2022 19:17:45 -0500 Subject: [PATCH 226/519] contract description: less prominent edit buttons --- web/components/button.tsx | 2 +- web/components/contract/contract-description.tsx | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/web/components/button.tsx b/web/components/button.tsx index 8cdeacdd..d279d9a0 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -39,7 +39,7 @@ export function Button(props: { color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500', color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500', color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600', - color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300', + color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200', className )} disabled={disabled} diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index d9864186..f9db0cd9 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -24,13 +24,10 @@ export function ContractDescription(props: { return ( <div className={clsx('mt-2 text-gray-700', className)}> {isCreator || isAdmin ? ( - <RichEditContract contract={contract} /> + <RichEditContract contract={contract} isAdmin={isAdmin && !isCreator} /> ) : ( <Content content={contract.description} /> )} - {isAdmin && !isCreator && ( - <div className="mt-2 text-red-400">(👆 admin powers)</div> - )} </div> ) } @@ -39,8 +36,8 @@ function editTimestamp() { return `${dayjs().format('MMM D, h:mma')}: ` } -function RichEditContract(props: { contract: Contract }) { - const { contract } = props +function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) { + const { contract, isAdmin } = props const [editing, setEditing] = useState(false) const [editingQ, setEditingQ] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) @@ -90,9 +87,11 @@ function RichEditContract(props: { contract: Contract }) { <> <Content content={contract.description} /> <Spacer h={2} /> - <Row className="gap-2"> + <Row className="items-center gap-2"> + {isAdmin && 'Admin: '} <Button color="gray" + size="xs" onClick={() => { setEditing(true) editor @@ -105,7 +104,7 @@ function RichEditContract(props: { contract: Contract }) { > Edit description </Button> - <Button color="gray" onClick={() => setEditingQ(true)}> + <Button color="gray" size="xs" onClick={() => setEditingQ(true)}> Edit question </Button> </Row> From dcd2ccae1bdc9eff61c712f2a9c5999f47b6870b Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 18 Jul 2022 23:29:32 -0700 Subject: [PATCH 227/519] Allow environments to override the referral bonus --- common/envs/prod.ts | 1 + common/user.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/envs/prod.ts b/common/envs/prod.ts index f8aaf4cc..5bd12095 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -22,6 +22,7 @@ export type EnvConfig = { // Currency controls fixedAnte?: number startingBalance?: number + referralBonus?: number } type FirebaseConfig = { diff --git a/common/user.ts b/common/user.ts index 1995ce34..0dac5a19 100644 --- a/common/user.ts +++ b/common/user.ts @@ -45,7 +45,7 @@ export type User = { export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000 // for sus users, i.e. multiple sign ups for same person export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10 -export const REFERRAL_AMOUNT = 500 +export const REFERRAL_AMOUNT = ENV_CONFIG.referralBonus ?? 500 export type PrivateUser = { id: string // same as User.id username: string // denormalized from User From b501776e3327abfe39468cb06b487569bd1a211d Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 19 Jul 2022 00:20:18 -0700 Subject: [PATCH 228/519] Remove quadratic matching from /charity --- web/components/charity/charity-card.tsx | 13 +++++----- web/pages/charity/index.tsx | 32 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/web/components/charity/charity-card.tsx b/web/components/charity/charity-card.tsx index 31995284..fc327b9f 100644 --- a/web/components/charity/charity-card.tsx +++ b/web/components/charity/charity-card.tsx @@ -6,10 +6,9 @@ import { Charity } from 'common/charity' import { useCharityTxns } from 'web/hooks/use-charity-txns' import { manaToUSD } from '../../../common/util/format' import { Row } from '../layout/row' -import { Col } from '../layout/col' export function CharityCard(props: { charity: Charity; match?: number }) { - const { charity, match } = props + const { charity } = props const { slug, photo, preview, id, tags } = charity const txns = useCharityTxns(id) @@ -36,18 +35,18 @@ export function CharityCard(props: { charity: Charity; match?: number }) { {raised > 0 && ( <> <Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900"> - <Col> + <Row className="items-baseline gap-1"> <span className="text-3xl font-semibold"> {formatUsd(raised)} </span> - <span>raised</span> - </Col> - {match && ( + raised + </Row> + {/* {match && ( <Col className="text-gray-500"> <span className="text-xl">+{formatUsd(match)}</span> <span className="">match</span> </Col> - )} + )} */} </Row> </> )} diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index 92e6b69f..f5295c59 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -13,7 +13,6 @@ import { CharityCard } from 'web/components/charity/charity-card' import { Col } from 'web/components/layout/col' import { Spacer } from 'web/components/layout/spacer' import { Page } from 'web/components/page' -import { SiteLink } from 'web/components/site-link' import { Title } from 'web/components/title' import { getAllCharityTxns } from 'web/lib/firebase/txns' import { manaToUSD } from 'common/util/format' @@ -21,6 +20,9 @@ import { quadraticMatches } from 'common/quadratic-funding' import { Txn } from 'common/txn' import { useTracking } from 'web/hooks/use-tracking' import { searchInAny } from 'common/util/parse' +import { getUser } from 'web/lib/firebase/users' +import { User } from 'common/lib/user' +import { SiteLink } from 'web/components/site-link' export async function getStaticProps() { const txns = await getAllCharityTxns() @@ -34,6 +36,7 @@ export async function getStaticProps() { ]) const matches = quadraticMatches(txns, totalRaised) const numDonors = uniqBy(txns, (txn) => txn.fromId).length + const mostRecentDonor = await getUser(txns[txns.length - 1].fromId) return { props: { @@ -42,6 +45,7 @@ export async function getStaticProps() { matches, txns, numDonors, + mostRecentDonor, }, revalidate: 60, } @@ -50,22 +54,28 @@ export async function getStaticProps() { type Stat = { name: string stat: string + url?: string } function DonatedStats(props: { stats: Stat[] }) { const { stats } = props return ( <dl className="mt-3 grid grid-cols-1 gap-5 rounded-lg bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-4 sm:grid-cols-3"> - {stats.map((item) => ( + {stats.map((stat) => ( <div - key={item.name} + key={stat.name} className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6" > <dt className="truncate text-sm font-medium text-gray-500"> - {item.name} + {stat.name} </dt> + <dd className="mt-1 text-3xl font-semibold text-gray-900"> - {item.stat} + {stat.url ? ( + <SiteLink href={stat.url}>{stat.stat}</SiteLink> + ) : ( + <span>{stat.stat}</span> + )} </dd> </div> ))} @@ -79,8 +89,9 @@ export default function Charity(props: { matches: { [charityId: string]: number } txns: Txn[] numDonors: number + mostRecentDonor: User }) { - const { totalRaised, charities, matches, numDonors } = props + const { totalRaised, charities, matches, numDonors, mostRecentDonor } = props const [query, setQuery] = useState('') const debouncedQuery = debounce(setQuery, 50) @@ -106,7 +117,7 @@ export default function Charity(props: { <Col className="w-full rounded px-4 py-6 sm:px-8 xl:w-[125%]"> <Col className=""> <Title className="!mt-0" text="Manifold for Charity" /> - <span className="text-gray-600"> + {/* <span className="text-gray-600"> Through July 15, up to $25k of donations will be matched via{' '} <SiteLink href="https://wtfisqf.com/" className="font-bold"> quadratic funding @@ -116,7 +127,7 @@ export default function Charity(props: { the FTX Future Fund </SiteLink> ! - </span> + </span> */} <DonatedStats stats={[ { @@ -128,8 +139,9 @@ export default function Charity(props: { stat: `${numDonors}`, }, { - name: 'Matched via quadratic funding', - stat: manaToUSD(sum(Object.values(matches))), + name: 'Most recent donor', + stat: mostRecentDonor.name ?? 'Nobody', + url: `/${mostRecentDonor.username}`, }, ]} /> From d1ad0716c8ce7724b6e612cfc83e41ec453d9fc2 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 19 Jul 2022 00:34:53 -0700 Subject: [PATCH 229/519] Fix import --- web/pages/charity/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index f5295c59..b1cfc353 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -21,8 +21,8 @@ import { Txn } from 'common/txn' import { useTracking } from 'web/hooks/use-tracking' import { searchInAny } from 'common/util/parse' import { getUser } from 'web/lib/firebase/users' -import { User } from 'common/lib/user' import { SiteLink } from 'web/components/site-link' +import { User } from 'common/user' export async function getStaticProps() { const txns = await getAllCharityTxns() From a103a2ee2cccb9b34c19fd9d5e613dae23b7058d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 00:50:11 -0700 Subject: [PATCH 230/519] Initial draft of Vercel Firebase auth (#593) * Set a cookie with an up-to-date Firebase ID token * Implement server-side authentication cookie reading logic * Change index page to redirect for authed users * No branch necessary for logged in users on index page * Add helpers for creating server-side redirects * Add some common sense redirects --- web/lib/firebase/auth.ts | 54 +++++++++++++++++++++ web/lib/firebase/server-auth.ts | 86 +++++++++++++++++++++++++++++++++ web/lib/firebase/users.ts | 10 ++-- web/lib/util/cookie.ts | 26 ++++++++++ web/pages/add-funds.tsx | 3 ++ web/pages/admin.tsx | 3 ++ web/pages/create.tsx | 11 ++--- web/pages/home.tsx | 12 ++--- web/pages/index.tsx | 25 ++-------- web/pages/links.tsx | 3 ++ web/pages/profile.tsx | 7 +-- web/pages/trades.tsx | 13 ++--- 12 files changed, 202 insertions(+), 51 deletions(-) create mode 100644 web/lib/firebase/auth.ts create mode 100644 web/lib/firebase/server-auth.ts create mode 100644 web/lib/util/cookie.ts diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts new file mode 100644 index 00000000..d1c440ec --- /dev/null +++ b/web/lib/firebase/auth.ts @@ -0,0 +1,54 @@ +import { PROJECT_ID } from 'common/envs/constants' +import { setCookie, getCookies } from '../util/cookie' +import { IncomingMessage, ServerResponse } from 'http' + +const TOKEN_KINDS = ['refresh', 'id'] as const +type TokenKind = typeof TOKEN_KINDS[number] + +const getAuthCookieName = (kind: TokenKind) => { + const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replaceAll('-', '_') + return `FIREBASE_TOKEN_${suffix}` +} + +const ID_COOKIE_NAME = getAuthCookieName('id') +const REFRESH_COOKIE_NAME = getAuthCookieName('refresh') + +export const getAuthCookies = (request?: IncomingMessage) => { + const data = request != null ? request.headers.cookie ?? '' : document.cookie + const cookies = getCookies(data) + return { + idToken: cookies[ID_COOKIE_NAME] as string | undefined, + refreshToken: cookies[REFRESH_COOKIE_NAME] as string | undefined, + } +} + +export const setAuthCookies = ( + idToken?: string, + refreshToken?: string, + response?: ServerResponse +) => { + // these tokens last an hour + const idMaxAge = idToken != null ? 60 * 60 : 0 + const idCookie = setCookie(ID_COOKIE_NAME, idToken ?? '', [ + ['path', '/'], + ['max-age', idMaxAge.toString()], + ['samesite', 'lax'], + ['secure'], + ]) + // these tokens don't expire + const refreshMaxAge = refreshToken != null ? 60 * 60 * 24 * 365 * 10 : 0 + const refreshCookie = setCookie(REFRESH_COOKIE_NAME, refreshToken ?? '', [ + ['path', '/'], + ['max-age', refreshMaxAge.toString()], + ['samesite', 'lax'], + ['secure'], + ]) + if (response != null) { + response.setHeader('Set-Cookie', [idCookie, refreshCookie]) + } else { + document.cookie = idCookie + document.cookie = refreshCookie + } +} + +export const deleteAuthCookies = () => setAuthCookies() diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts new file mode 100644 index 00000000..5f828683 --- /dev/null +++ b/web/lib/firebase/server-auth.ts @@ -0,0 +1,86 @@ +import * as admin from 'firebase-admin' +import fetch from 'node-fetch' +import { IncomingMessage, ServerResponse } from 'http' +import { FIREBASE_CONFIG, PROJECT_ID } from 'common/envs/constants' +import { getAuthCookies, setAuthCookies } from './auth' +import { GetServerSideProps, GetServerSidePropsContext } from 'next' + +const ensureApp = async () => { + // Note: firebase-admin can only be imported from a server context, + // because it relies on Node standard library dependencies. + if (admin.apps.length === 0) { + // never initialize twice + return admin.initializeApp({ projectId: PROJECT_ID }) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return admin.apps[0]! +} + +const requestFirebaseIdToken = async (refreshToken: string) => { + // See https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token + const refreshUrl = new URL('https://securetoken.googleapis.com/v1/token') + refreshUrl.searchParams.append('key', FIREBASE_CONFIG.apiKey) + const result = await fetch(refreshUrl.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), + }) + if (!result.ok) { + throw new Error(`Could not refresh ID token: ${await result.text()}`) + } + return (await result.json()) as any +} + +type RequestContext = { + req: IncomingMessage + res: ServerResponse +} + +export const getServerAuthenticatedUid = async (ctx: RequestContext) => { + const app = await ensureApp() + const auth = app.auth() + const { idToken, refreshToken } = getAuthCookies(ctx.req) + + // If we have a valid ID token, verify the user immediately with no network trips. + // If the ID token doesn't verify, we'll have to refresh it to see who they are. + // If they don't have any tokens, then we have no idea who they are. + if (idToken != null) { + try { + return (await auth.verifyIdToken(idToken))?.uid + } catch (e) { + if (refreshToken != null) { + const resp = await requestFirebaseIdToken(refreshToken) + setAuthCookies(resp.id_token, resp.refresh_token, ctx.res) + return (await auth.verifyIdToken(resp.id_token))?.uid + } + } + } + return undefined +} + +export const redirectIfLoggedIn = (dest: string, fn?: GetServerSideProps) => { + return async (ctx: GetServerSidePropsContext) => { + const uid = await getServerAuthenticatedUid(ctx) + if (uid == null) { + return fn != null ? await fn(ctx) : { props: {} } + } else { + return { redirect: { destination: dest, permanent: false } } + } + } +} + +export const redirectIfLoggedOut = (dest: string, fn?: GetServerSideProps) => { + return async (ctx: GetServerSidePropsContext) => { + const uid = await getServerAuthenticatedUid(ctx) + if (uid == null) { + return { redirect: { destination: dest, permanent: false } } + } else { + return fn != null ? await fn(ctx) : { props: {} } + } + } +} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index f3242a7e..77c5c48d 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -16,7 +16,7 @@ import { import { getAuth } from 'firebase/auth' import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage' import { - onAuthStateChanged, + onIdTokenChanged, GoogleAuthProvider, signInWithPopup, } from 'firebase/auth' @@ -43,6 +43,7 @@ import utc from 'dayjs/plugin/utc' dayjs.extend(utc) import { track } from '@amplitude/analytics-browser' +import { deleteAuthCookies, setAuthCookies } from './auth' export const users = coll<User>('users') export const privateUsers = coll<PrivateUser>('private-users') @@ -188,10 +189,9 @@ export function listenForLogin(onUser: (user: User | null) => void) { const cachedUser = local?.getItem(CACHED_USER_KEY) onUser(cachedUser && JSON.parse(cachedUser)) - return onAuthStateChanged(auth, async (fbUser) => { + return onIdTokenChanged(auth, async (fbUser) => { if (fbUser) { let user: User | null = await getUser(fbUser.uid) - if (!user) { if (createUserPromise == null) { const local = safeLocalStorage() @@ -204,17 +204,19 @@ export function listenForLogin(onUser: (user: User | null) => void) { } user = await createUserPromise } - onUser(user) // Persist to local storage, to reduce login blink next time. // Note: Cap on localStorage size is ~5mb local?.setItem(CACHED_USER_KEY, JSON.stringify(user)) setCachedReferralInfoForUser(user) + setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) } else { // User logged out; reset to null onUser(null) + createUserPromise = undefined local?.removeItem(CACHED_USER_KEY) + deleteAuthCookies() } }) } diff --git a/web/lib/util/cookie.ts b/web/lib/util/cookie.ts new file mode 100644 index 00000000..c0326cfc --- /dev/null +++ b/web/lib/util/cookie.ts @@ -0,0 +1,26 @@ +type CookieOptions = string[][] + +const encodeCookie = (name: string, val: string) => { + return `${name}=${encodeURIComponent(val)}` +} + +const decodeCookie = (cookie: string) => { + const parts = cookie.trim().split('=') + if (parts.length != 2) { + throw new Error(`Invalid cookie contents: ${cookie}`) + } + return [parts[0], decodeURIComponent(parts[1])] as const +} + +export const setCookie = (name: string, val: string, opts?: CookieOptions) => { + const parts = [encodeCookie(name, val)] + if (opts != null) { + parts.push(...opts.map((opt) => opt.join('='))) + } + return parts.join('; ') +} + +// Note that this intentionally ignores the case where multiple cookies have +// the same name but different paths. Hopefully we never need to think about it. +export const getCookies = (cookies: string) => + Object.fromEntries(cookies.split(';').map(decodeCookie)) diff --git a/web/pages/add-funds.tsx b/web/pages/add-funds.tsx index f680d47b..ed25a21a 100644 --- a/web/pages/add-funds.tsx +++ b/web/pages/add-funds.tsx @@ -8,6 +8,9 @@ import { checkoutURL } from 'web/lib/service/stripe' import { Page } from 'web/components/page' import { useTracking } from 'web/hooks/use-tracking' import { trackCallback } from 'web/lib/service/analytics' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' + +export const getServerSideProps = redirectIfLoggedOut('/') export default function AddFundsPage() { const user = useUser() diff --git a/web/pages/admin.tsx b/web/pages/admin.tsx index e709e875..81f23ba9 100644 --- a/web/pages/admin.tsx +++ b/web/pages/admin.tsx @@ -9,6 +9,9 @@ import { useContracts } from 'web/hooks/use-contracts' import { mapKeys } from 'lodash' import { useAdmin } from 'web/hooks/use-admin' import { contractPath } from 'web/lib/firebase/contracts' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' + +export const getServerSideProps = redirectIfLoggedOut('/') function avatarHtml(avatarUrl: string) { return `<img diff --git a/web/pages/create.tsx b/web/pages/create.tsx index a3801223..4294770e 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -28,6 +28,9 @@ import { GroupSelector } from 'web/components/groups/group-selector' import { User } from 'common/user' import { TextEditor, useTextEditor } from 'web/components/editor' import { Checkbox } from 'web/components/checkbox' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' + +export const getServerSideProps = redirectIfLoggedOut('/') type NewQuestionParams = { groupId?: string @@ -55,11 +58,7 @@ export default function Create() { }, [params.q]) const creator = useUser() - useEffect(() => { - if (creator === null) router.push('/') - }, [creator, router]) - - if (!router.isReady || !creator) return <div /> + if (!router.isReady || creator) return <div /> return ( <Page> @@ -93,7 +92,7 @@ export default function Create() { // Allow user to create a new contract export function NewContract(props: { - creator: User + creator?: User | null question: string params?: NewQuestionParams }) { diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 98d5036e..6aa99a07 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from 'react' -import Router, { useRouter } from 'next/router' +import { useRouter } from 'next/router' import { PlusSmIcon } from '@heroicons/react/solid' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' -import { useUser } from 'web/hooks/use-user' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ContractSearch } from 'web/components/contract-search' import { Contract } from 'common/contract' @@ -12,19 +11,16 @@ import { ContractPageContent } from './[username]/[contractSlug]' import { getContractFromSlug } from 'web/lib/firebase/contracts' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' + +export const getServerSideProps = redirectIfLoggedOut('/') const Home = () => { - const user = useUser() const [contract, setContract] = useContractPage() const router = useRouter() useTracking('view home') - if (user === null) { - Router.replace('/') - return <></> - } - return ( <> <Page suspend={!!contract}> diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 904fc014..44683a4f 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,14 +1,13 @@ import React from 'react' -import Router from 'next/router' import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts' import { Page } from 'web/components/page' import { LandingPagePanel } from 'web/components/landing-page-panel' import { Col } from 'web/components/layout/col' -import { useUser } from 'web/hooks/use-user' import { ManifoldLogo } from 'web/components/nav/manifold-logo' +import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth' -export async function getStaticProps() { +export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { // These hardcoded markets will be shown in the frontpage for signed-out users: const hotContracts = await getContractsBySlugs([ 'will-max-go-to-prom-with-a-girl', @@ -22,23 +21,11 @@ export async function getStaticProps() { 'will-congress-hold-any-hearings-abo-e21f987033b3', 'will-at-least-10-world-cities-have', ]) + return { props: { hotContracts } } +}) - return { - props: { hotContracts }, - revalidate: 60, // regenerate after a minute - } -} - -const Home = (props: { hotContracts: Contract[] }) => { +export default function Home(props: { hotContracts: Contract[] }) { const { hotContracts } = props - - const user = useUser() - - if (user) { - Router.replace('/home') - return <></> - } - return ( <Page> <div className="px-4 pt-2 md:mt-0 lg:hidden"> @@ -58,5 +45,3 @@ const Home = (props: { hotContracts: Contract[] }) => { </Page> ) } - -export default Home diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 76c62978..490f1878 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -18,11 +18,14 @@ import { Avatar } from 'web/components/avatar' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { UserLink } from 'web/components/user-page' import { CreateLinksButton } from 'web/components/manalinks/create-links-button' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' dayjs.extend(customParseFormat) +export const getServerSideProps = redirectIfLoggedOut('/') + export function getManalinkUrl(slug: string) { return `${location.protocol}//${location.host}/link/${slug}` } diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index b80698ae..541f5de9 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react' import { RefreshIcon } from '@heroicons/react/outline' -import Router from 'next/router' import { AddFundsButton } from 'web/components/add-funds-button' import { Page } from 'web/components/page' @@ -18,6 +17,9 @@ import { updateUser, updatePrivateUser } from 'web/lib/firebase/users' import { defaultBannerUrl } from 'web/components/user-page' import { SiteLink } from 'web/components/site-link' import Textarea from 'react-expanding-textarea' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' + +export const getServerSideProps = redirectIfLoggedOut('/') function EditUserField(props: { user: User @@ -134,8 +136,7 @@ export default function ProfilePage() { }) } - if (user === null) { - Router.replace('/') + if (user == null) { return <></> } diff --git a/web/pages/trades.tsx b/web/pages/trades.tsx index 55a08bc6..a29fb7f0 100644 --- a/web/pages/trades.tsx +++ b/web/pages/trades.tsx @@ -1,17 +1,10 @@ import Router from 'next/router' -import { useEffect } from 'react' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' -import { useUser } from 'web/hooks/use-user' +export const getServerSideProps = redirectIfLoggedOut('/') // Deprecated: redirects to /portfolio. // Eventually, this will be removed. export default function TradesPage() { - const user = useUser() - - useEffect(() => { - if (user === null) Router.replace('/') - else Router.replace('/portfolio') - }) - - return <></> + Router.replace('/portfolio') } From f6d2c56e430f1fffb89636667430a736c3c6a8e6 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 19 Jul 2022 01:23:36 -0700 Subject: [PATCH 231/519] Fix /create --- web/pages/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 4294770e..45eb120f 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -58,7 +58,7 @@ export default function Create() { }, [params.q]) const creator = useUser() - if (!router.isReady || creator) return <div /> + if (!router.isReady || !creator) return <div /> return ( <Page> From c256e9c0cc8c64b143c5144d2f5cd6d87765bd71 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 01:32:38 -0700 Subject: [PATCH 232/519] Attempt to fix up overly sensitive cookie parsing --- web/lib/util/cookie.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/lib/util/cookie.ts b/web/lib/util/cookie.ts index c0326cfc..4ffee21e 100644 --- a/web/lib/util/cookie.ts +++ b/web/lib/util/cookie.ts @@ -6,10 +6,11 @@ const encodeCookie = (name: string, val: string) => { const decodeCookie = (cookie: string) => { const parts = cookie.trim().split('=') - if (parts.length != 2) { + if (parts.length < 2) { throw new Error(`Invalid cookie contents: ${cookie}`) } - return [parts[0], decodeURIComponent(parts[1])] as const + const rest = parts.slice(1).join('') // there may be more = in the value + return [parts[0], decodeURIComponent(rest)] as const } export const setCookie = (name: string, val: string, opts?: CookieOptions) => { From 55775d9d37872fc133650d7d26104af87bce79b9 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 01:35:34 -0700 Subject: [PATCH 233/519] Also handle case where there are no cookies yet --- web/lib/util/cookie.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/lib/util/cookie.ts b/web/lib/util/cookie.ts index 4ffee21e..14999fd4 100644 --- a/web/lib/util/cookie.ts +++ b/web/lib/util/cookie.ts @@ -23,5 +23,11 @@ export const setCookie = (name: string, val: string, opts?: CookieOptions) => { // Note that this intentionally ignores the case where multiple cookies have // the same name but different paths. Hopefully we never need to think about it. -export const getCookies = (cookies: string) => - Object.fromEntries(cookies.split(';').map(decodeCookie)) +export const getCookies = (cookies: string) => { + const data = cookies.trim() + if (!data) { + return {} + } else { + return Object.fromEntries(data.split(';').map(decodeCookie)) + } +} From 2bae7dc200d9160ac8ba33ceb7454b0670f37351 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 19 Jul 2022 02:54:05 -0700 Subject: [PATCH 234/519] Fix error on no portfolio history --- .../portfolio/portfolio-value-section.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 1fabbd06..fa50365b 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -15,17 +15,17 @@ export const PortfolioValueSection = memo( const lastPortfolioMetrics = last(portfolioHistory) const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime') - // PATCH: If portfolio history started on June 1st, then we label it as "Since June" - // instead of "All time" - const allTimeLabel = - portfolioHistory[0].timestamp < Date.parse('2022-06-20T00:00:00.000Z') - ? 'Since June' - : 'All time' - if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { return <></> } + // PATCH: If portfolio history started on June 1st, then we label it as "Since June" + // instead of "All time" + const allTimeLabel = + lastPortfolioMetrics.timestamp < Date.parse('2022-06-20T00:00:00.000Z') + ? 'Since June' + : 'All time' + return ( <div> <Row className="gap-8"> From c236eb15b167fd6bdf8b409f65662301afe0b874 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 09:04:47 -0600 Subject: [PATCH 235/519] Cache notifs in local, gives instant load of old notifs (#662) * Cache notifs in local, gives instant load of old notifs * Small refactor, add ss auth * unused vars * Add back in replaceAll * Save all notifs * Memoize paginated notifs * Replace all => replace with regexp --- web/hooks/use-notifications.ts | 52 ++++++------- web/lib/firebase/auth.ts | 2 +- web/pages/notifications.tsx | 132 ++++++++++++++++++++++++--------- 3 files changed, 123 insertions(+), 63 deletions(-) diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index f5502b85..b9bef469 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { notification_subscribe_types, PrivateUser } from 'common/user' import { Notification } from 'common/notification' import { @@ -6,7 +6,7 @@ import { listenForNotifications, } from 'web/lib/firebase/notifications' import { groupBy, map } from 'lodash' -import { useFirestoreQuery } from '@react-query-firebase/firestore' +import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' export type NotificationGroup = { @@ -19,36 +19,38 @@ export type NotificationGroup = { // For some reason react-query subscriptions don't actually listen for notifications // Use useUnseenPreferredNotificationGroups to listen for new notifications -export function usePreferredGroupedNotifications(privateUser: PrivateUser) { - const [notificationGroups, setNotificationGroups] = useState< - NotificationGroup[] | undefined - >(undefined) - const [notifications, setNotifications] = useState<Notification[]>([]) - const key = `notifications-${privateUser.id}-all` - - const result = useFirestoreQuery([key], getNotificationsQuery(privateUser.id)) - useEffect(() => { - if (result.isLoading) return - if (!result.data) return setNotifications([]) - const notifications = result.data.docs.map( - (doc) => doc.data() as Notification - ) +export function usePreferredGroupedNotifications( + privateUser: PrivateUser, + cachedNotifications?: Notification[] +) { + const result = useFirestoreQueryData( + ['notifications-all', privateUser.id], + getNotificationsQuery(privateUser.id) + ) + const notifications = useMemo(() => { + if (result.isLoading) return cachedNotifications ?? [] + if (!result.data) return cachedNotifications ?? [] + const notifications = result.data as Notification[] const notificationsToShow = getAppropriateNotifications( notifications, privateUser.notificationPreferences ).filter((n) => !n.isSeenOnHref) - setNotifications(notificationsToShow) - }, [privateUser.notificationPreferences, result.data, result.isLoading]) + const cachedIds = cachedNotifications?.map((n) => n.id) + if (notificationsToShow.some((n) => !cachedIds?.includes(n.id))) { + return notificationsToShow + } + return cachedNotifications + }, [ + cachedNotifications, + privateUser.notificationPreferences, + result.data, + result.isLoading, + ]) - useEffect(() => { - if (!notifications) return - - const groupedNotifications = groupNotifications(notifications) - setNotificationGroups(groupedNotifications) + return useMemo(() => { + if (notifications) return groupNotifications(notifications) }, [notifications]) - - return notificationGroups } export function useUnseenPreferredNotificationGroups(privateUser: PrivateUser) { diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index d1c440ec..b6daea6e 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -6,7 +6,7 @@ const TOKEN_KINDS = ['refresh', 'id'] as const type TokenKind = typeof TOKEN_KINDS[number] const getAuthCookieName = (kind: TokenKind) => { - const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replaceAll('-', '_') + const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replace(/-/g, '_') return `FIREBASE_TOKEN_${suffix}` } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 9166109f..7500c2a8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,6 +1,6 @@ import { Tabs } from 'web/components/layout/tabs' import { usePrivateUser, useUser } from 'web/hooks/use-user' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' @@ -14,9 +14,14 @@ import { MANIFOLD_USERNAME, notification_subscribe_types, PrivateUser, + User, } from 'common/user' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' +import { + getUser, + listenForPrivateUser, + updatePrivateUser, +} from 'web/lib/firebase/users' import { LoadingIndicator } from 'web/components/loading-indicator' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' @@ -43,14 +48,38 @@ import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' import Router from 'next/router' +import { safeLocalStorage } from 'web/lib/util/local' +import { + getServerAuthenticatedUid, + redirectIfLoggedOut, +} from 'web/lib/firebase/server-auth' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' const HIGHLIGHT_CLASS = 'bg-indigo-50' -export default function Notifications() { - const user = useUser() +export const getServerSideProps = redirectIfLoggedOut('/', async (ctx) => { + const uid = await getServerAuthenticatedUid(ctx) + if (!uid) { + return { props: { user: null } } + } + const user = await getUser(uid) + return { props: { user } } +}) + +export default function Notifications(props: { user: User }) { + const { user } = props const privateUser = usePrivateUser(user?.id) + const local = safeLocalStorage() + let localNotifications = [] as Notification[] + const localSavedNotificationGroups = local?.getItem('notification-groups') + let localNotificationGroups = [] as NotificationGroup[] + if (localSavedNotificationGroups) { + localNotificationGroups = JSON.parse(localSavedNotificationGroups) + localNotifications = localNotificationGroups + .map((g) => g.notifications) + .flat() + } if (!user) return <Custom404 /> return ( @@ -67,7 +96,16 @@ export default function Notifications() { { title: 'Notifications', content: privateUser ? ( - <NotificationsList privateUser={privateUser} /> + <NotificationsList + privateUser={privateUser} + cachedNotifications={localNotifications} + /> + ) : localNotifications && localNotifications.length > 0 ? ( + <div className={'min-h-[100vh]'}> + <RenderNotificationGroups + notificationGroups={localNotificationGroups} + /> + </div> ) : ( <LoadingIndicator /> ), @@ -88,39 +126,13 @@ export default function Notifications() { ) } -function NotificationsList(props: { privateUser: PrivateUser }) { - const { privateUser } = props - const [page, setPage] = useState(0) - const allGroupedNotifications = usePreferredGroupedNotifications(privateUser) - const [paginatedGroupedNotifications, setPaginatedGroupedNotifications] = - useState<NotificationGroup[] | undefined>(undefined) - - useEffect(() => { - if (!allGroupedNotifications) return - const start = page * NOTIFICATIONS_PER_PAGE - const end = start + NOTIFICATIONS_PER_PAGE - const maxNotificationsToShow = allGroupedNotifications.slice(start, end) - const remainingNotification = allGroupedNotifications.slice(end) - for (const notification of remainingNotification) { - if (notification.isSeen) break - else setNotificationsAsSeen(notification.notifications) - } - setPaginatedGroupedNotifications(maxNotificationsToShow) - }, [allGroupedNotifications, page]) - - if (!paginatedGroupedNotifications || !allGroupedNotifications) - return <LoadingIndicator /> - +function RenderNotificationGroups(props: { + notificationGroups: NotificationGroup[] +}) { + const { notificationGroups } = props return ( - <div className={'min-h-[100vh]'}> - {paginatedGroupedNotifications.length === 0 && ( - <div className={'mt-2'}> - You don't have any notifications. Try changing your settings to see - more. - </div> - )} - - {paginatedGroupedNotifications.map((notification) => + <> + {notificationGroups.map((notification) => notification.type === 'income' ? ( <IncomeNotificationGroupItem notificationGroup={notification} @@ -138,6 +150,52 @@ function NotificationsList(props: { privateUser: PrivateUser }) { /> ) )} + </> + ) +} + +function NotificationsList(props: { + privateUser: PrivateUser + cachedNotifications: Notification[] +}) { + const { privateUser, cachedNotifications } = props + const [page, setPage] = useState(0) + const allGroupedNotifications = usePreferredGroupedNotifications( + privateUser, + cachedNotifications + ) + const paginatedGroupedNotifications = useMemo(() => { + if (!allGroupedNotifications) return + const start = page * NOTIFICATIONS_PER_PAGE + const end = start + NOTIFICATIONS_PER_PAGE + const maxNotificationsToShow = allGroupedNotifications.slice(start, end) + const remainingNotification = allGroupedNotifications.slice(end) + for (const notification of remainingNotification) { + if (notification.isSeen) break + else setNotificationsAsSeen(notification.notifications) + } + const local = safeLocalStorage() + local?.setItem( + 'notification-groups', + JSON.stringify(maxNotificationsToShow) + ) + return maxNotificationsToShow + }, [allGroupedNotifications, page]) + + if (!paginatedGroupedNotifications || !allGroupedNotifications) return <div /> + + return ( + <div className={'min-h-[100vh]'}> + {paginatedGroupedNotifications.length === 0 && ( + <div className={'mt-2'}> + You don't have any notifications. Try changing your settings to see + more. + </div> + )} + + <RenderNotificationGroups + notificationGroups={paginatedGroupedNotifications} + /> {paginatedGroupedNotifications.length > 0 && allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( <Pagination From a203f43142532c8ed99edce011e0a26d3f493144 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 09:29:12 -0600 Subject: [PATCH 236/519] Cache all notifs --- web/pages/notifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 7500c2a8..084b143c 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -177,7 +177,7 @@ function NotificationsList(props: { const local = safeLocalStorage() local?.setItem( 'notification-groups', - JSON.stringify(maxNotificationsToShow) + JSON.stringify(allGroupedNotifications) ) return maxNotificationsToShow }, [allGroupedNotifications, page]) From 0d282a962c4ecfe929e6db9b1572e3bf46040d19 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 08:35:43 -0700 Subject: [PATCH 237/519] Don't `setQuery` on group selector component during initial render (#660) --- web/components/groups/group-selector.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index 2417403a..c7b4cb39 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -53,9 +53,8 @@ export function GroupSelector(props: { nullable={true} className={'text-sm'} > - {({ open }) => ( + {() => ( <> - {!open && setQuery('')} <Combobox.Label className="label justify-start gap-2 text-base"> Add to Group <InfoTooltip text="Question will be displayed alongside the other questions in the group." /> From 4b3370e374a97c454562626be59903bedd120094 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 19 Jul 2022 12:30:13 -0500 Subject: [PATCH 238/519] fix formatting --- common/util/format.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 7dc1a341..4f123535 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -33,20 +33,24 @@ export function formatPercent(zeroToOne: number) { return (zeroToOne * 100).toFixed(decimalPlaces) + '%' } +const showPrecision = (x: number, sigfigs: number) => + // convert back to number for weird formatting reason + `${Number(x.toPrecision(sigfigs))}` + // Eg 1234567.89 => 1.23M; 5678 => 5.68K export function formatLargeNumber(num: number, sigfigs = 2): string { const absNum = Math.abs(num) - if (absNum < 1) return num.toPrecision(sigfigs) + if (absNum < 1) return showPrecision(num, sigfigs) - if (absNum < 100) return num.toPrecision(2) - if (absNum < 1000) return num.toPrecision(3) - if (absNum < 10000) return num.toPrecision(4) + if (absNum < 100) return showPrecision(num, 2) + if (absNum < 1000) return showPrecision(num, 3) + if (absNum < 10000) return showPrecision(num, 4) const suffix = ['', 'K', 'M', 'B', 'T', 'Q'] const i = Math.floor(Math.log10(absNum) / 3) - const numStr = (num / Math.pow(10, 3 * i)).toPrecision(sigfigs) - return `${numStr}${suffix[i]}` + const numStr = showPrecision(num / Math.pow(10, 3 * i), sigfigs) + return `${numStr}${suffix[i] ?? ''}` } export function toCamelCase(words: string) { From 12567074cc4430ac15295f9dca61ed5487dd36d2 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 19 Jul 2022 12:31:11 -0500 Subject: [PATCH 239/519] fix log scale graph --- common/pseudo-numeric.ts | 6 ++--- .../contract/contract-prob-graph.tsx | 25 ++++++++++++------- web/pages/create.tsx | 18 ++++++------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/common/pseudo-numeric.ts b/common/pseudo-numeric.ts index c99e670f..73f9fd01 100644 --- a/common/pseudo-numeric.ts +++ b/common/pseudo-numeric.ts @@ -16,8 +16,8 @@ export const getMappedValue = const { min, max, isLogScale } = contract if (isLogScale) { - const logValue = p * Math.log10(max - min) - return 10 ** logValue + min + const logValue = p * Math.log10(max - min + 1) + return 10 ** logValue + min - 1 } return p * (max - min) + min @@ -38,7 +38,7 @@ export const getPseudoProbability = ( isLogScale = false ) => { if (isLogScale) { - return Math.log10(value - min) / Math.log10(max - min) + return Math.log10(value - min + 1) / Math.log10(max - min + 1) } return (value - min) / (max - min) diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index a9d26e2e..c6e17cd6 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -7,7 +7,6 @@ import { Bet } from 'common/bet' import { getInitialProbability } from 'common/calculate' import { BinaryContract, PseudoNumericContract } from 'common/contract' import { useWindowSize } from 'web/hooks/use-window-size' -import { getMappedValue } from 'common/pseudo-numeric' import { formatLargeNumber } from 'common/util/format' export const ContractProbGraph = memo(function ContractProbGraph(props: { @@ -29,7 +28,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { ...bets.map((bet) => bet.createdTime), ].map((time) => new Date(time)) - const f = getMappedValue(contract) + const f: (p: number) => number = isBinary + ? (p) => p + : isLogScale + ? (p) => p * Math.log10(contract.max - contract.min + 1) + : (p) => p * (contract.max - contract.min) + contract.min const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f) @@ -69,10 +72,9 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { const points: { x: Date; y: number }[] = [] const s = isBinary ? 100 : 1 - const c = isLogScale && contract.min === 0 ? 1 : 0 for (let i = 0; i < times.length - 1; i++) { - points[points.length] = { x: times[i], y: s * probs[i] + c } + points[points.length] = { x: times[i], y: s * probs[i] } const numPoints: number = Math.floor( dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep ) @@ -84,7 +86,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { x: dayjs(times[i]) .add(thisTimeStep * n, 'ms') .toDate(), - y: s * probs[i] + c, + y: s * probs[i], } } } @@ -99,6 +101,9 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { const formatter = isBinary ? formatPercent + : isLogScale + ? (x: DatumValue) => + formatLargeNumber(10 ** +x.valueOf() + contract.min - 1) : (x: DatumValue) => formatLargeNumber(+x.valueOf()) return ( @@ -111,11 +116,13 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { yScale={ isBinary ? { min: 0, max: 100, type: 'linear' } - : { - min: contract.min + c, - max: contract.max + c, - type: contract.isLogScale ? 'log' : 'linear', + : isLogScale + ? { + min: 0, + max: Math.log10(contract.max - contract.min + 1), + type: 'linear', } + : { min: contract.min, max: contract.max, type: 'linear' } } yFormat={formatter} gridYValues={yTickValues} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 45eb120f..78ad8d19 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -206,7 +206,7 @@ export function NewContract(props: { min, max, initialValue, - isLogScale: (min ?? 0) < 0 ? false : isLogScale, + isLogScale, groupId: selectedGroup?.id, }) ) @@ -293,15 +293,13 @@ export function NewContract(props: { /> </Row> - {!(min !== undefined && min < 0) && ( - <Checkbox - className="my-2 text-sm" - label="Log scale" - checked={isLogScale} - toggle={() => setIsLogScale(!isLogScale)} - disabled={isSubmitting} - /> - )} + <Checkbox + className="my-2 text-sm" + label="Log scale" + checked={isLogScale} + toggle={() => setIsLogScale(!isLogScale)} + disabled={isSubmitting} + /> {min !== undefined && max !== undefined && min >= max && ( <div className="mt-2 mb-2 text-sm text-red-500"> From 61cbb07bd50978bd15badc12c341fd90e2e52e94 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 12:33:53 -0700 Subject: [PATCH 240/519] Fix some broken stuff on the homepage contract search routing (#664) * Use Next router more appropriately * Replace instead of push when modifying search query params --- web/hooks/use-sort-and-query-params.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index a2248c2e..5c9a247f 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -53,9 +53,12 @@ export function useInitialQueryAndSort(options?: { console.log('ready loading from storage ', sort ?? defaultSort) const localSort = getSavedSort() if (localSort) { - router.query.s = localSort // Use replace to not break navigating back. - router.replace(router, undefined, { shallow: true }) + router.replace( + { query: { ...router.query, s: localSort } }, + undefined, + { shallow: true } + ) } setInitialSort(localSort ?? defaultSort) } else { @@ -79,7 +82,9 @@ export function useUpdateQueryAndSort(props: { const setSort = (sort: Sort | undefined) => { if (sort !== router.query.s) { router.query.s = sort - router.push(router, undefined, { shallow: true }) + router.replace({ query: { ...router.query, s: sort } }, undefined, { + shallow: true, + }) if (shouldLoadFromStorage) { localStorage.setItem(MARKETS_SORT, sort || '') } @@ -97,7 +102,9 @@ export function useUpdateQueryAndSort(props: { } else { delete router.query.q } - router.push(router, undefined, { shallow: true }) + router.replace({ query: router.query }, undefined, { + shallow: true, + }) track('search', { query }) }, 500), [router] From 74760b1062f72fa7e08394374872be569b8a3df6 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 14:53:33 -0500 Subject: [PATCH 241/519] Reorder orderbook columns --- web/components/limit-bets.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 7e32db25..7aaa0601 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -78,8 +78,8 @@ export function LimitOrderTable(props: { <thead> {!isYou && <th></th>} <th>Outcome</th> - <th>Amount</th> <th>{isPseudoNumeric ? 'Value' : 'Prob'}</th> + <th>Amount</th> {isYou && <th></th>} </thead> <tbody> @@ -129,12 +129,12 @@ function LimitBet(props: { )} </div> </td> - <td>{formatMoney(orderAmount - amount)}</td> <td> {isPseudoNumeric ? getFormattedMappedValue(contract)(limitProb) : formatPercent(limitProb)} </td> + <td>{formatMoney(orderAmount - amount)}</td> {isYou && ( <td> {isCancelling ? ( From 93b9ace47715618c16c98b9052897a674c4a0b41 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 14:54:42 -0500 Subject: [PATCH 242/519] Comment email: Subject no longer varies between questions so emails are threaded in gmail --- functions/src/emails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 60534679..a29f982c 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -302,7 +302,7 @@ export const sendNewCommentEmail = async ( )}` } - const subject = `Comment from ${commentorName} on ${question}` + const subject = `Comment on ${question}` const from = `${commentorName} on Manifold <no-reply@manifold.markets>` if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) { From a1e03c3a25f1003c64e8d02982d9323f1994b3bf Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 13:58:51 -0600 Subject: [PATCH 243/519] Allow opening notifs in new tabs, return newest notifs --- web/hooks/use-notifications.ts | 7 +--- web/pages/notifications.tsx | 61 ++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index b9bef469..a3ddeb29 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -32,15 +32,10 @@ export function usePreferredGroupedNotifications( if (!result.data) return cachedNotifications ?? [] const notifications = result.data as Notification[] - const notificationsToShow = getAppropriateNotifications( + return getAppropriateNotifications( notifications, privateUser.notificationPreferences ).filter((n) => !n.isSeenOnHref) - const cachedIds = cachedNotifications?.map((n) => n.id) - if (notificationsToShow.some((n) => !cachedIds?.includes(n.id))) { - return notificationsToShow - } - return cachedNotifications }, [ cachedNotifications, privateUser.notificationPreferences, diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 084b143c..a45bf030 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -47,12 +47,12 @@ import Custom404 from 'web/pages/404' import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' -import Router from 'next/router' import { safeLocalStorage } from 'web/lib/util/local' import { getServerAuthenticatedUid, redirectIfLoggedOut, } from 'web/lib/firebase/server-auth' +import { SiteLink } from 'web/components/site-link' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -100,7 +100,8 @@ export default function Notifications(props: { user: User }) { privateUser={privateUser} cachedNotifications={localNotifications} /> - ) : localNotifications && localNotifications.length > 0 ? ( + ) : localNotificationGroups && + localNotificationGroups.length > 0 ? ( <div className={'min-h-[100vh]'}> <RenderNotificationGroups notificationGroups={localNotificationGroups} @@ -440,7 +441,11 @@ function IncomeNotificationItem(props: { highlighted && HIGHLIGHT_CLASS )} > - <a href={getSourceUrl(notification)}> + <div className={'relative'}> + <SiteLink + href={getSourceUrl(notification) ?? ''} + className={'absolute left-0 right-0 top-0 bottom-0 z-0'} + /> <Row className={'items-center text-gray-500 sm:justify-start'}> <div className={'line-clamp-2 flex max-w-xl shrink '}> <div className={'inline'}> @@ -466,7 +471,7 @@ function IncomeNotificationItem(props: { </div> </Row> <div className={'mt-4 border-b border-gray-300'} /> - </a> + </div> </div> ) } @@ -655,24 +660,24 @@ function NotificationItem(props: { highlighted && HIGHLIGHT_CLASS )} > - <div - className={'cursor-pointer'} - onClick={(event) => { - event.stopPropagation() - Router.push(getSourceUrl(notification) ?? '') - track('Notification Clicked', { - type: 'notification item', - sourceType, - sourceUserName, - sourceUserAvatarUrl, - sourceUpdateType, - reasonText, - reason, - sourceUserUsername, - sourceText, - }) - }} - > + <div className={'relative cursor-pointer'}> + <SiteLink + href={getSourceUrl(notification) ?? ''} + className={'absolute left-0 right-0 top-0 bottom-0 z-0'} + onClick={() => + track('Notification Clicked', { + type: 'notification item', + sourceType, + sourceUserName, + sourceUserAvatarUrl, + sourceUpdateType, + reasonText, + reason, + sourceUserUsername, + sourceText, + }) + } + /> <Row className={'items-center text-gray-500 sm:justify-start'}> <Avatar avatarUrl={ @@ -681,7 +686,7 @@ function NotificationItem(props: { : sourceUserAvatarUrl } size={'sm'} - className={'mr-2'} + className={'z-10 mr-2'} username={ questionNeedsResolution ? MANIFOLD_USERNAME : sourceUserUsername } @@ -697,7 +702,7 @@ function NotificationItem(props: { <UserLink name={sourceUserName || ''} username={sourceUserUsername || ''} - className={'mr-1 flex-shrink-0'} + className={'relative mr-1 flex-shrink-0'} justFirstName={true} /> )} @@ -764,10 +769,8 @@ function QuestionOrGroupLink(props: { </span> ) return ( - <a - className={ - 'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2 ' - } + <SiteLink + className={'relative ml-1 font-bold'} href={ sourceContractCreatorUsername ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` @@ -792,7 +795,7 @@ function QuestionOrGroupLink(props: { } > {sourceContractTitle || sourceTitle} - </a> + </SiteLink> ) } From b6c8390a460febbcf92a81cdb0e1d23809383409 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 15:01:13 -0500 Subject: [PATCH 244/519] Show order book button even on Quick bet --- web/components/bet-panel.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 351b012e..0cbee7b5 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -50,14 +50,10 @@ export function BetPanel(props: { const user = useUser() const userBets = useUserContractBets(user?.id, contract.id) const unfilledBets = useUnfilledBets(contract.id) ?? [] - const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id) const { sharesOutcome } = useSaveBinaryShares(contract, userBets) const [isLimitOrder, setIsLimitOrder] = useState(false) - const showLimitOrders = - (isLimitOrder && unfilledBets.length > 0) || yourUnfilledBets.length > 0 - return ( <Col className={className}> <SellRow @@ -85,7 +81,7 @@ export function BetPanel(props: { <SignUpPrompt /> </Col> - {showLimitOrders && ( + {unfilledBets.length > 0 && ( <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> )} </Col> @@ -105,9 +101,6 @@ export function SimpleBetPanel(props: { const [isLimitOrder, setIsLimitOrder] = useState(false) const unfilledBets = useUnfilledBets(contract.id) ?? [] - const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id) - const showLimitOrders = - (isLimitOrder && unfilledBets.length > 0) || yourUnfilledBets.length > 0 return ( <Col className={className}> @@ -138,7 +131,7 @@ export function SimpleBetPanel(props: { <SignUpPrompt /> </Col> - {showLimitOrders && ( + {unfilledBets.length > 0 && ( <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> )} </Col> From 6dcad6225b797e4c536097a7e7d6d8456f91a258 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 14:16:20 -0600 Subject: [PATCH 245/519] Next/Previous => Older/Newer --- web/components/pagination.tsx | 30 +++++++++++++++++++++--------- web/pages/notifications.tsx | 2 ++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx index a585985d..069ebda7 100644 --- a/web/components/pagination.tsx +++ b/web/components/pagination.tsx @@ -4,8 +4,18 @@ export function Pagination(props: { totalItems: number setPage: (page: number) => void scrollToTop?: boolean + nextTitle?: string + prevTitle?: string }) { - const { page, itemsPerPage, totalItems, setPage, scrollToTop } = props + const { + page, + itemsPerPage, + totalItems, + setPage, + scrollToTop, + nextTitle, + prevTitle, + } = props const maxPage = Math.ceil(totalItems / itemsPerPage) - 1 @@ -25,19 +35,21 @@ export function Pagination(props: { </p> </div> <div className="flex flex-1 justify-between sm:justify-end"> - <a - href={scrollToTop ? '#' : undefined} - className="relative inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" - onClick={() => page > 0 && setPage(page - 1)} - > - Previous - </a> + {page > 0 && ( + <a + href={scrollToTop ? '#' : undefined} + className="relative inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" + onClick={() => page > 0 && setPage(page - 1)} + > + {prevTitle ?? 'Previous'} + </a> + )} <a href={scrollToTop ? '#' : undefined} className="relative ml-3 inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => page < maxPage && setPage(page + 1)} > - Next + {nextTitle ?? 'Next'} </a> </div> </nav> diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index a45bf030..7867e197 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -205,6 +205,8 @@ function NotificationsList(props: { totalItems={allGroupedNotifications.length} setPage={setPage} scrollToTop + nextTitle={'Older'} + prevTitle={'Newer'} /> )} </div> From b5ef7490c3cb9b6713ed9214365448c2a394355b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 14:24:36 -0600 Subject: [PATCH 246/519] NotificationSettings to its own file --- web/components/NotificationSettings.tsx | 210 +++++++++++++++++++++++ web/pages/notifications.tsx | 214 +----------------------- 2 files changed, 214 insertions(+), 210 deletions(-) create mode 100644 web/components/NotificationSettings.tsx diff --git a/web/components/NotificationSettings.tsx b/web/components/NotificationSettings.tsx new file mode 100644 index 00000000..2c657857 --- /dev/null +++ b/web/components/NotificationSettings.tsx @@ -0,0 +1,210 @@ +import { useUser } from 'web/hooks/use-user' +import React, { useEffect, useState } from 'react' +import { notification_subscribe_types, PrivateUser } from 'common/lib/user' +import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' +import toast from 'react-hot-toast' +import { track } from '@amplitude/analytics-browser' +import { LoadingIndicator } from 'web/components/loading-indicator' +import { Row } from 'web/components/layout/row' +import clsx from 'clsx' +import { CheckIcon, XIcon } from '@heroicons/react/outline' +import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' + +export function NotificationSettings() { + const user = useUser() + const [notificationSettings, setNotificationSettings] = + useState<notification_subscribe_types>('all') + const [emailNotificationSettings, setEmailNotificationSettings] = + useState<notification_subscribe_types>('all') + const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null) + + useEffect(() => { + if (user) listenForPrivateUser(user.id, setPrivateUser) + }, [user]) + + useEffect(() => { + if (!privateUser) return + if (privateUser.notificationPreferences) { + setNotificationSettings(privateUser.notificationPreferences) + } + if ( + privateUser.unsubscribedFromResolutionEmails && + privateUser.unsubscribedFromCommentEmails && + privateUser.unsubscribedFromAnswerEmails + ) { + setEmailNotificationSettings('none') + } else if ( + !privateUser.unsubscribedFromResolutionEmails && + !privateUser.unsubscribedFromCommentEmails && + !privateUser.unsubscribedFromAnswerEmails + ) { + setEmailNotificationSettings('all') + } else { + setEmailNotificationSettings('less') + } + }, [privateUser]) + + const loading = 'Changing Notifications Settings' + const success = 'Notification Settings Changed!' + function changeEmailNotifications(newValue: notification_subscribe_types) { + if (!privateUser) return + if (newValue === 'all') { + toast.promise( + updatePrivateUser(privateUser.id, { + unsubscribedFromResolutionEmails: false, + unsubscribedFromCommentEmails: false, + unsubscribedFromAnswerEmails: false, + }), + { + loading, + success, + error: (err) => `${err.message}`, + } + ) + } else if (newValue === 'less') { + toast.promise( + updatePrivateUser(privateUser.id, { + unsubscribedFromResolutionEmails: false, + unsubscribedFromCommentEmails: true, + unsubscribedFromAnswerEmails: true, + }), + { + loading, + success, + error: (err) => `${err.message}`, + } + ) + } else if (newValue === 'none') { + toast.promise( + updatePrivateUser(privateUser.id, { + unsubscribedFromResolutionEmails: true, + unsubscribedFromCommentEmails: true, + unsubscribedFromAnswerEmails: true, + }), + { + loading, + success, + error: (err) => `${err.message}`, + } + ) + } + } + + function changeInAppNotificationSettings( + newValue: notification_subscribe_types + ) { + if (!privateUser) return + track('In-App Notification Preferences Changed', { + newPreference: newValue, + oldPreference: privateUser.notificationPreferences, + }) + toast.promise( + updatePrivateUser(privateUser.id, { + notificationPreferences: newValue, + }), + { + loading, + success, + error: (err) => `${err.message}`, + } + ) + } + + useEffect(() => { + if (privateUser && privateUser.notificationPreferences) + setNotificationSettings(privateUser.notificationPreferences) + else setNotificationSettings('all') + }, [privateUser]) + + if (!privateUser) { + return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} /> + } + + function NotificationSettingLine(props: { + label: string + highlight: boolean + }) { + const { label, highlight } = props + return ( + <Row className={clsx('my-1 text-gray-300', highlight && '!text-black')}> + {highlight ? <CheckIcon height={20} /> : <XIcon height={20} />} + {label} + </Row> + ) + } + + return ( + <div className={'p-2'}> + <div>In App Notifications</div> + <ChoicesToggleGroup + currentChoice={notificationSettings} + choicesMap={{ All: 'all', Less: 'less', None: 'none' }} + setChoice={(choice) => + changeInAppNotificationSettings( + choice as notification_subscribe_types + ) + } + className={'col-span-4 p-2'} + toggleClassName={'w-24'} + /> + <div className={'mt-4 text-sm'}> + <div> + <div className={''}> + You will receive notifications for: + <NotificationSettingLine + label={"Resolution of questions you've interacted with"} + highlight={notificationSettings !== 'none'} + /> + <NotificationSettingLine + highlight={notificationSettings !== 'none'} + label={'Activity on your own questions, comments, & answers'} + /> + <NotificationSettingLine + highlight={notificationSettings !== 'none'} + label={"Activity on questions you're betting on"} + /> + <NotificationSettingLine + highlight={notificationSettings !== 'none'} + label={"Income & referral bonuses you've received"} + /> + <NotificationSettingLine + label={"Activity on questions you've ever bet or commented on"} + highlight={notificationSettings === 'all'} + /> + </div> + </div> + </div> + <div className={'mt-4'}>Email Notifications</div> + <ChoicesToggleGroup + currentChoice={emailNotificationSettings} + choicesMap={{ All: 'all', Less: 'less', None: 'none' }} + setChoice={(choice) => + changeEmailNotifications(choice as notification_subscribe_types) + } + className={'col-span-4 p-2'} + toggleClassName={'w-24'} + /> + <div className={'mt-4 text-sm'}> + <div> + You will receive emails for: + <NotificationSettingLine + label={"Resolution of questions you're betting on"} + highlight={emailNotificationSettings !== 'none'} + /> + <NotificationSettingLine + label={'Closure of your questions'} + highlight={emailNotificationSettings !== 'none'} + /> + <NotificationSettingLine + label={'Activity on your questions'} + highlight={emailNotificationSettings === 'all'} + /> + <NotificationSettingLine + label={"Activity on questions you've answered or commented on"} + highlight={emailNotificationSettings === 'all'} + /> + </div> + </div> + </div> + ) +} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 7867e197..0d5ecdb9 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,5 +1,5 @@ import { Tabs } from 'web/components/layout/tabs' -import { usePrivateUser, useUser } from 'web/hooks/use-user' +import { usePrivateUser } from 'web/hooks/use-user' import React, { useEffect, useMemo, useState } from 'react' import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' @@ -12,16 +12,10 @@ import { UserLink } from 'web/components/user-page' import { MANIFOLD_AVATAR_URL, MANIFOLD_USERNAME, - notification_subscribe_types, PrivateUser, User, } from 'common/user' -import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { - getUser, - listenForPrivateUser, - updatePrivateUser, -} from 'web/lib/firebase/users' +import { getUser } from 'web/lib/firebase/users' import { LoadingIndicator } from 'web/components/loading-indicator' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' @@ -37,8 +31,7 @@ import { NotificationGroup, usePreferredGroupedNotifications, } from 'web/hooks/use-notifications' -import { CheckIcon, TrendingUpIcon, XIcon } from '@heroicons/react/outline' -import toast from 'react-hot-toast' +import { TrendingUpIcon } from '@heroicons/react/outline' import { formatMoney } from 'common/util/format' import { groupPath } from 'web/lib/firebase/groups' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' @@ -53,6 +46,7 @@ import { redirectIfLoggedOut, } from 'web/lib/firebase/server-auth' import { SiteLink } from 'web/components/site-link' +import { NotificationSettings } from 'web/components/NotificationSettings' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -986,203 +980,3 @@ function getReasonForShowingNotification( } return reasonText } - -// TODO: where should we put referral bonus notifications? -function NotificationSettings() { - const user = useUser() - const [notificationSettings, setNotificationSettings] = - useState<notification_subscribe_types>('all') - const [emailNotificationSettings, setEmailNotificationSettings] = - useState<notification_subscribe_types>('all') - const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null) - - useEffect(() => { - if (user) listenForPrivateUser(user.id, setPrivateUser) - }, [user]) - - useEffect(() => { - if (!privateUser) return - if (privateUser.notificationPreferences) { - setNotificationSettings(privateUser.notificationPreferences) - } - if ( - privateUser.unsubscribedFromResolutionEmails && - privateUser.unsubscribedFromCommentEmails && - privateUser.unsubscribedFromAnswerEmails - ) { - setEmailNotificationSettings('none') - } else if ( - !privateUser.unsubscribedFromResolutionEmails && - !privateUser.unsubscribedFromCommentEmails && - !privateUser.unsubscribedFromAnswerEmails - ) { - setEmailNotificationSettings('all') - } else { - setEmailNotificationSettings('less') - } - }, [privateUser]) - - const loading = 'Changing Notifications Settings' - const success = 'Notification Settings Changed!' - function changeEmailNotifications(newValue: notification_subscribe_types) { - if (!privateUser) return - if (newValue === 'all') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: false, - unsubscribedFromCommentEmails: false, - unsubscribedFromAnswerEmails: false, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } else if (newValue === 'less') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: false, - unsubscribedFromCommentEmails: true, - unsubscribedFromAnswerEmails: true, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } else if (newValue === 'none') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: true, - unsubscribedFromCommentEmails: true, - unsubscribedFromAnswerEmails: true, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } - } - - function changeInAppNotificationSettings( - newValue: notification_subscribe_types - ) { - if (!privateUser) return - track('In-App Notification Preferences Changed', { - newPreference: newValue, - oldPreference: privateUser.notificationPreferences, - }) - toast.promise( - updatePrivateUser(privateUser.id, { - notificationPreferences: newValue, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } - - useEffect(() => { - if (privateUser && privateUser.notificationPreferences) - setNotificationSettings(privateUser.notificationPreferences) - else setNotificationSettings('all') - }, [privateUser]) - - if (!privateUser) { - return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} /> - } - - function NotificationSettingLine(props: { - label: string - highlight: boolean - }) { - const { label, highlight } = props - return ( - <Row className={clsx('my-1 text-gray-300', highlight && '!text-black')}> - {highlight ? <CheckIcon height={20} /> : <XIcon height={20} />} - {label} - </Row> - ) - } - - return ( - <div className={'p-2'}> - <div>In App Notifications</div> - <ChoicesToggleGroup - currentChoice={notificationSettings} - choicesMap={{ All: 'all', Less: 'less', None: 'none' }} - setChoice={(choice) => - changeInAppNotificationSettings( - choice as notification_subscribe_types - ) - } - className={'col-span-4 p-2'} - toggleClassName={'w-24'} - /> - <div className={'mt-4 text-sm'}> - <div> - <div className={''}> - You will receive notifications for: - <NotificationSettingLine - label={"Resolution of questions you've interacted with"} - highlight={notificationSettings !== 'none'} - /> - <NotificationSettingLine - highlight={notificationSettings !== 'none'} - label={'Activity on your own questions, comments, & answers'} - /> - <NotificationSettingLine - highlight={notificationSettings !== 'none'} - label={"Activity on questions you're betting on"} - /> - <NotificationSettingLine - highlight={notificationSettings !== 'none'} - label={"Income & referral bonuses you've received"} - /> - <NotificationSettingLine - label={"Activity on questions you've ever bet or commented on"} - highlight={notificationSettings === 'all'} - /> - </div> - </div> - </div> - <div className={'mt-4'}>Email Notifications</div> - <ChoicesToggleGroup - currentChoice={emailNotificationSettings} - choicesMap={{ All: 'all', Less: 'less', None: 'none' }} - setChoice={(choice) => - changeEmailNotifications(choice as notification_subscribe_types) - } - className={'col-span-4 p-2'} - toggleClassName={'w-24'} - /> - <div className={'mt-4 text-sm'}> - <div> - You will receive emails for: - <NotificationSettingLine - label={"Resolution of questions you're betting on"} - highlight={emailNotificationSettings !== 'none'} - /> - <NotificationSettingLine - label={'Closure of your questions'} - highlight={emailNotificationSettings !== 'none'} - /> - <NotificationSettingLine - label={'Activity on your questions'} - highlight={emailNotificationSettings === 'all'} - /> - <NotificationSettingLine - label={"Activity on questions you've answered or commented on"} - highlight={emailNotificationSettings === 'all'} - /> - </div> - </div> - </div> - ) -} From 6c070464dd5ffaebaa70e90c2f89f0cf19515f10 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 15:39:15 -0500 Subject: [PATCH 247/519] Use static props to load leaderboard fast --- web/lib/firebase/users.ts | 4 +- web/pages/leaderboards.tsx | 152 ++++++++++++++++++------------------- 2 files changed, 76 insertions(+), 80 deletions(-) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 77c5c48d..884dde04 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -275,7 +275,7 @@ export function getTopTraders(period: Period) { limit(20) ) - return getValues(topTraders) + return getValues<User>(topTraders) } export function getTopCreators(period: Period) { @@ -284,7 +284,7 @@ export function getTopCreators(period: Period) { orderBy('creatorVolumeCached.' + period, 'desc'), limit(20) ) - return getValues(topCreators) + return getValues<User>(topCreators) } export async function getTopFollowed() { diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 061f3a19..6f15e13e 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -9,97 +9,96 @@ import { User, } from 'web/lib/firebase/users' import { formatMoney } from 'common/util/format' -import { fromPropz, usePropz } from 'web/hooks/use-propz' import { useEffect, useState } from 'react' -import { LoadingIndicator } from 'web/components/loading-indicator' import { Title } from 'web/components/title' import { Tabs } from 'web/components/layout/tabs' import { useTracking } from 'web/hooks/use-tracking' -export const getStaticProps = fromPropz(getStaticPropz) -export async function getStaticPropz() { - return queryLeaderboardUsers('allTime') -} -const queryLeaderboardUsers = async (period: Period) => { - const [topTraders, topCreators, topFollowed] = await Promise.all([ - getTopTraders(period).catch(() => {}), - getTopCreators(period).catch(() => {}), - getTopFollowed().catch(() => {}), - ]) +export async function getStaticProps() { + const props = await fetchProps() + return { - props: { - topTraders, - topCreators, - topFollowed, - }, + props, revalidate: 60, // regenerate after a minute } } -export default function Leaderboards(props: { +const fetchProps = async () => { + const [allTime, monthly, weekly, daily] = await Promise.all([ + queryLeaderboardUsers('allTime'), + queryLeaderboardUsers('monthly'), + queryLeaderboardUsers('weekly'), + queryLeaderboardUsers('daily'), + ]) + const topFollowed = await getTopFollowed() + + return { + allTime, + monthly, + weekly, + daily, + topFollowed, + } +} + +const queryLeaderboardUsers = async (period: Period) => { + const [topTraders, topCreators] = await Promise.all([ + getTopTraders(period), + getTopCreators(period), + ]) + return { + topTraders, + topCreators, + } +} + +type leaderboard = { topTraders: User[] topCreators: User[] +} + +export default function Leaderboards(_props: { + allTime: leaderboard + monthly: leaderboard + weekly: leaderboard + daily: leaderboard topFollowed: User[] }) { - props = usePropz(props, getStaticPropz) ?? { - topTraders: [], - topCreators: [], - topFollowed: [], - } - const { topFollowed } = props - const [topTradersState, setTopTraders] = useState(props.topTraders) - const [topCreatorsState, setTopCreators] = useState(props.topCreators) - const [isLoading, setLoading] = useState(false) - const [period, setPeriod] = useState<Period>('allTime') - + const [props, setProps] = useState<Parameters<typeof Leaderboards>[0]>(_props) useEffect(() => { - setLoading(true) - queryLeaderboardUsers(period).then((res) => { - setTopTraders(res.props.topTraders as User[]) - setTopCreators(res.props.topCreators as User[]) - setLoading(false) - }) - }, [period]) + fetchProps().then((props) => setProps(props)) + }, []) + + const { topFollowed } = props const LeaderboardWithPeriod = (period: Period) => { + const { topTraders, topCreators } = props[period] + return ( <> <Col className="mx-4 items-center gap-10 lg:flex-row"> - {!isLoading ? ( - <> - {period === 'allTime' || - period == 'weekly' || - period === 'daily' ? ( //TODO: show other periods once they're available - <Leaderboard - title="🏅 Top bettors" - users={topTradersState} - columns={[ - { - header: 'Total profit', - renderCell: (user) => - formatMoney(user.profitCached[period]), - }, - ]} - /> - ) : ( - <></> - )} + <Leaderboard + title="🏅 Top bettors" + users={topTraders} + columns={[ + { + header: 'Total profit', + renderCell: (user) => formatMoney(user.profitCached[period]), + }, + ]} + /> - <Leaderboard - title="🏅 Top creators" - users={topCreatorsState} - columns={[ - { - header: 'Total bet', - renderCell: (user) => - formatMoney(user.creatorVolumeCached[period]), - }, - ]} - /> - </> - ) : ( - <LoadingIndicator spinnerClassName={'border-gray-500'} /> - )} + <Leaderboard + title="🏅 Top creators" + users={topCreators} + columns={[ + { + header: 'Total bet', + renderCell: (user) => + formatMoney(user.creatorVolumeCached[period]), + }, + ]} + /> </Col> {period === 'allTime' ? ( <Col className="mx-4 my-10 items-center gap-10 lg:mx-0 lg:w-1/2 lg:flex-row"> @@ -128,19 +127,16 @@ export default function Leaderboards(props: { <Tabs currentPageForAnalytics={'leaderboards'} defaultIndex={0} - onClick={(title, index) => { - const period = ['allTime', 'monthly', 'weekly', 'daily'][index] - setPeriod(period as Period) - }} tabs={[ { title: 'All Time', content: LeaderboardWithPeriod('allTime'), }, - { - title: 'Monthly', - content: LeaderboardWithPeriod('monthly'), - }, + // TODO: Enable this near the end of July! + // { + // title: 'Monthly', + // content: LeaderboardWithPeriod('monthly'), + // }, { title: 'Weekly', content: LeaderboardWithPeriod('weekly'), From 2684c8bcca3bd5659b0ba92fed35959a789e1379 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 15:39:40 -0500 Subject: [PATCH 248/519] Default to weekly leaderboard --- web/pages/leaderboards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 6f15e13e..7ee13172 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -126,7 +126,7 @@ export default function Leaderboards(_props: { <Title text={'Leaderboards'} className={'hidden md:block'} /> <Tabs currentPageForAnalytics={'leaderboards'} - defaultIndex={0} + defaultIndex={1} tabs={[ { title: 'All Time', From e9ad30cc74440ad53e6bcd664a4b8f4500a4fadc Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 15:43:37 -0500 Subject: [PATCH 249/519] Increase number of contracts shown in bets list --- web/components/bets-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index db6b0d05..2114ec2b 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -50,7 +50,7 @@ import { LimitOrderTable } from './limit-bets' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' -const CONTRACTS_PER_PAGE = 20 +const CONTRACTS_PER_PAGE = 50 export function BetsList(props: { user: User From fc9e26160108ca8025e16c6112a13e7920d3db67 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 15:45:47 -0500 Subject: [PATCH 250/519] Fix build --- web/components/NotificationSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/NotificationSettings.tsx b/web/components/NotificationSettings.tsx index 2c657857..7a839a7a 100644 --- a/web/components/NotificationSettings.tsx +++ b/web/components/NotificationSettings.tsx @@ -1,6 +1,6 @@ import { useUser } from 'web/hooks/use-user' import React, { useEffect, useState } from 'react' -import { notification_subscribe_types, PrivateUser } from 'common/lib/user' +import { notification_subscribe_types, PrivateUser } from 'common/user' import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' import toast from 'react-hot-toast' import { track } from '@amplitude/analytics-browser' From f9aab390399f62e7bf9f1dbe98387222c48e3b31 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 13:57:32 -0700 Subject: [PATCH 251/519] Clean up font loading and see if it fixes our problem (#667) --- web/pages/_document.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index 2ff0b494..b8cb657c 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -6,16 +6,15 @@ export default function Document() { <Html data-theme="mantic" className="min-h-screen"> <Head> <link rel="icon" href={ENV_CONFIG.faviconPath} /> - - <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" - crossOrigin="true" + crossOrigin="anonymous" /> <link href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@300;400;600;700&display=swap" rel="stylesheet" + crossOrigin="anonymous" /> <link rel="stylesheet" @@ -24,7 +23,6 @@ export default function Document() { crossOrigin="anonymous" /> </Head> - <body className="font-readex-pro bg-base-200 min-h-screen"> <Main /> <NextScript /> From af6552958fd669b6ac7573faa149173d0bddce65 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 16:05:49 -0500 Subject: [PATCH 252/519] Show all-time profit on UserPage --- web/components/user-page.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 38efe345..09c28920 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -39,6 +39,7 @@ import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' import { ReferralsButton } from 'web/components/referrals-button' +import { formatMoney } from 'common/util/format' export function UserLink(props: { name: string @@ -123,6 +124,7 @@ export function UserPage(props: { const yourFollows = useFollows(currentUser?.id) const isFollowing = yourFollows?.includes(user.id) + const profit = user.profitCached.allTime const onFollow = () => { if (!currentUser) return @@ -187,6 +189,17 @@ export function UserPage(props: { <Col className="mx-4 -mt-6"> <span className="text-2xl font-bold">{user.name}</span> <span className="text-gray-500">@{user.username}</span> + <span className="text-gray-500"> + <span + className={clsx( + 'text-md', + profit >= 0 ? 'text-green-600' : 'text-red-400' + )} + > + {formatMoney(profit)} + </span>{' '} + profit + </span> <Spacer h={4} /> From 6d3490cd689f14248718537b9757b6ac760d4f8b Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 14:20:23 -0700 Subject: [PATCH 253/519] Turn off Next.js font inlining (#668) --- web/next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/next.config.js b/web/next.config.js index 56f643d3..37758952 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -4,6 +4,7 @@ const API_DOCS_URL = 'https://docs.manifold.markets/api' module.exports = { staticPageGenerationTimeout: 600, // e.g. stats page reactStrictMode: true, + optimizeFonts: false, experimental: { externalDir: true, optimizeCss: true, From 6124ea01f694df4cd7eeeb4314b1565d79e502b0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 16:57:32 -0500 Subject: [PATCH 254/519] Fix a DOM error in console --- web/components/limit-bets.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 7aaa0601..8c9f4e6b 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -76,11 +76,13 @@ export function LimitOrderTable(props: { return ( <table className="table-compact table w-full rounded text-gray-500"> <thead> - {!isYou && <th></th>} - <th>Outcome</th> - <th>{isPseudoNumeric ? 'Value' : 'Prob'}</th> - <th>Amount</th> - {isYou && <th></th>} + <tr> + {!isYou && <th></th>} + <th>Outcome</th> + <th>{isPseudoNumeric ? 'Value' : 'Prob'}</th> + <th>Amount</th> + {isYou && <th></th>} + </tr> </thead> <tbody> {limitBets.map((bet) => ( From 58d62863615992ea33818cd9fc0f86b395cd49f5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 17:22:58 -0500 Subject: [PATCH 255/519] Fix chart area extending into labels below --- web/components/contract/contract-prob-graph.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index c6e17cd6..c829c646 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -150,7 +150,8 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { enableSlices="x" enableGridX={!!width && width >= 800} enableArea - margin={{ top: 20, right: 20, bottom: 25, left: 40 }} + areaBaselineValue={isBinary ? 0 : contract.min} + margin={{ top: 20, right: 20, bottom: 65, left: 40 }} animate={false} sliceTooltip={SliceTooltip} /> From 2152e5286af54ef1e8f3dc0a000b294f211b0df3 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 16:29:41 -0600 Subject: [PATCH 256/519] Score & sort by unique bettors in last 3 days --- common/contract.ts | 1 + functions/src/index.ts | 1 + functions/src/score-contracts.ts | 41 +++++++++++++++++++++++++ web/components/contract-search.tsx | 5 +-- web/hooks/use-sort-and-query-params.tsx | 5 +-- web/pages/contract-search-firestore.tsx | 8 ++--- web/pages/home.tsx | 4 +-- 7 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 functions/src/score-contracts.ts diff --git a/common/contract.ts b/common/contract.ts index 5ddcf0b8..b1242ab9 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -48,6 +48,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { groupSlugs?: string[] uniqueBettorIds?: string[] uniqueBettorCount?: number + popularityScore?: number } & T export type BinaryContract = Contract & Binary diff --git a/functions/src/index.ts b/functions/src/index.ts index 3055f8dc..df311886 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -22,6 +22,7 @@ export * from './on-update-user' export * from './on-create-comment-on-group' export * from './on-create-txn' export * from './on-delete-group' +export * from './score-contracts' // v2 export * from './health' diff --git a/functions/src/score-contracts.ts b/functions/src/score-contracts.ts new file mode 100644 index 00000000..ab6512d0 --- /dev/null +++ b/functions/src/score-contracts.ts @@ -0,0 +1,41 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' +import { Bet } from 'common/bet' +import { uniq } from 'lodash' +import { Contract } from 'common/contract' + +export const scoreContracts = functions.pubsub + .schedule('every 1 hours') + .onRun(async () => { + await scoreContractsInternal() + }) +const firestore = admin.firestore() + +async function scoreContractsInternal() { + const now = Date.now() + const lastHour = now - 3600000 + const last3Days = now - 2592000000 + + const contracts = await firestore + .collection('contracts') + .where('lastUpdatedTime', '>', lastHour) + .get() + + for (const contractSnap of contracts.docs) { + const contract = contractSnap.data() as Contract + const contractId = contractSnap.id + const bets = await firestore + .collection(`contracts/${contractId}/bets`) + .where('createdTime', '>', last3Days) + .get() + const bettors = bets.docs + .map((doc) => doc.data() as Bet) + .map((bet) => bet.userId) + const score = uniq(bettors).length + if (contract.popularityScore !== score) + await firestore + .collection('contracts') + .doc(contractId) + .update({ popularityScore: score }) + } +} diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 013208d8..dc97f482 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -39,13 +39,14 @@ const indexPrefix = ENV === 'DEV' ? 'dev-' : '' const sortIndexes = [ { label: 'Newest', value: indexPrefix + 'contracts-newest' }, { label: 'Oldest', value: indexPrefix + 'contracts-oldest' }, - { label: 'Most popular', value: indexPrefix + 'contracts-most-popular' }, + { label: 'Most popular', value: indexPrefix + 'contracts-score' }, { label: 'Most traded', value: indexPrefix + 'contracts-most-traded' }, { label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' }, { label: 'Last updated', value: indexPrefix + 'contracts-last-updated' }, { label: 'Close date', value: indexPrefix + 'contracts-close-date' }, { label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' }, ] +export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' const filterOptions: { [label: string]: filter } = { @@ -95,7 +96,7 @@ export function ContractSearch(props: { .map(({ value }) => value) .includes(`${indexPrefix}contracts-${initialSort ?? ''}`) ? initialSort - : querySortOptions?.defaultSort ?? 'most-popular' + : querySortOptions?.defaultSort ?? DEFAULT_SORT const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index 5c9a247f..9023dc1a 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import { useSearchBox } from 'react-instantsearch-hooks-web' import { track } from 'web/lib/service/analytics' +import { DEFAULT_SORT } from 'web/components/contract-search' const MARKETS_SORT = 'markets_sort' @@ -10,11 +11,11 @@ export type Sort = | 'newest' | 'oldest' | 'most-traded' - | 'most-popular' | '24-hour-vol' | 'close-date' | 'resolve-date' | 'last-updated' + | 'score' export function getSavedSort() { // TODO: this obviously doesn't work with SSR, common sense would suggest @@ -31,7 +32,7 @@ export function useInitialQueryAndSort(options?: { shouldLoadFromStorage?: boolean }) { const { defaultSort, shouldLoadFromStorage } = defaults(options, { - defaultSort: 'most-popular', + defaultSort: DEFAULT_SORT, shouldLoadFromStorage: true, }) const router = useRouter() diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 0ef8cdfe..2d45e831 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -54,10 +54,8 @@ export default function ContractSearchFirestore(props: { ) } else if (sort === 'most-traded') { matches.sort((a, b) => b.volume - a.volume) - } else if (sort === 'most-popular') { - matches.sort( - (a, b) => (b.uniqueBettorCount ?? 0) - (a.uniqueBettorCount ?? 0) - ) + } else if (sort === 'score') { + matches.sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0)) } else if (sort === '24-hour-vol') { // Use lodash for stable sort, so previous sort breaks all ties. matches = sortBy(matches, ({ volume7Days }) => -1 * volume7Days) @@ -104,7 +102,7 @@ export default function ContractSearchFirestore(props: { > <option value="newest">Newest</option> <option value="oldest">Oldest</option> - <option value="most-popular">Most popular</option> + <option value="score">Most popular</option> <option value="most-traded">Most traded</option> <option value="24-hour-vol">24h volume</option> <option value="close-date">Closing soon</option> diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 6aa99a07..53bb6ec9 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -5,7 +5,7 @@ import { PlusSmIcon } from '@heroicons/react/solid' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' -import { ContractSearch } from 'web/components/contract-search' +import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search' import { Contract } from 'common/contract' import { ContractPageContent } from './[username]/[contractSlug]' import { getContractFromSlug } from 'web/lib/firebase/contracts' @@ -28,7 +28,7 @@ const Home = () => { <ContractSearch querySortOptions={{ shouldLoadFromStorage: true, - defaultSort: getSavedSort() ?? 'most-popular', + defaultSort: getSavedSort() ?? DEFAULT_SORT, }} onContractClick={(c) => { // Show contract without navigating to contract page. From 4aface583d921773eb7f158f63d0bee55624f4f3 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 16:41:11 -0600 Subject: [PATCH 257/519] Remove pesky loading spinner --- web/pages/notifications.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 0d5ecdb9..d011e757 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -94,15 +94,12 @@ export default function Notifications(props: { user: User }) { privateUser={privateUser} cachedNotifications={localNotifications} /> - ) : localNotificationGroups && - localNotificationGroups.length > 0 ? ( + ) : ( <div className={'min-h-[100vh]'}> <RenderNotificationGroups notificationGroups={localNotificationGroups} /> </div> - ) : ( - <LoadingIndicator /> ), }, { From 1f0983a145c2d82f679e6e174887eb2a4f26eff4 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 17:08:51 -0600 Subject: [PATCH 258/519] Find old contracts to decrement score on --- functions/src/score-contracts.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/functions/src/score-contracts.ts b/functions/src/score-contracts.ts index ab6512d0..733f1397 100644 --- a/functions/src/score-contracts.ts +++ b/functions/src/score-contracts.ts @@ -3,6 +3,7 @@ import * as admin from 'firebase-admin' import { Bet } from 'common/bet' import { uniq } from 'lodash' import { Contract } from 'common/contract' +import { log } from './utils' export const scoreContracts = functions.pubsub .schedule('every 1 hours') @@ -15,17 +16,29 @@ async function scoreContractsInternal() { const now = Date.now() const lastHour = now - 3600000 const last3Days = now - 2592000000 - - const contracts = await firestore + const activeContractsSnap = await firestore .collection('contracts') .where('lastUpdatedTime', '>', lastHour) .get() + const activeContracts = activeContractsSnap.docs.map( + (doc) => doc.data() as Contract + ) + // We have to downgrade previously active contracts to allow the new ones to bubble up + const previouslyActiveContractsSnap = await firestore + .collection('contracts') + .where('popularityScore', '>', 0) + .get() + const activeContractIds = activeContracts.map((c) => c.id) + const previouslyActiveContracts = previouslyActiveContractsSnap.docs + .map((doc) => doc.data() as Contract) + .filter((c) => !activeContractIds.includes(c.id)) - for (const contractSnap of contracts.docs) { - const contract = contractSnap.data() as Contract - const contractId = contractSnap.id + const contracts = activeContracts.concat(previouslyActiveContracts) + log(`Found ${contracts.length} contracts to score`) + + for (const contract of contracts) { const bets = await firestore - .collection(`contracts/${contractId}/bets`) + .collection(`contracts/${contract.id}/bets`) .where('createdTime', '>', last3Days) .get() const bettors = bets.docs @@ -35,7 +48,7 @@ async function scoreContractsInternal() { if (contract.popularityScore !== score) await firestore .collection('contracts') - .doc(contractId) + .doc(contract.id) .update({ popularityScore: score }) } } From bab828412ba913686794e0a99e5e9da85929f07e Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 19 Jul 2022 18:15:55 -0500 Subject: [PATCH 259/519] group: add question button --- web/components/create-question-button.tsx | 1 + web/pages/group/[...slugs]/index.tsx | 90 +++++++++-------------- 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index a9161ac6..f2371d11 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -5,6 +5,7 @@ import React from 'react' export const createButtonStyle = 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' + export const CreateQuestionButton = (props: { user: User | null | undefined overrideText?: string diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 4039fe17..7cce843e 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,4 +1,5 @@ import { take, sortBy, debounce } from 'lodash' +import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon' import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Page } from 'web/components/page' @@ -32,10 +33,7 @@ import { SEO } from 'web/components/SEO' import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' -import { - createButtonStyle, - CreateQuestionButton, -} from 'web/components/create-question-button' +import { CreateQuestionButton } from 'web/components/create-question-button' import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' @@ -265,9 +263,7 @@ export default function GroupPage(props: { <Row className={'items-center justify-between gap-4'}> <div className={'sm:mb-1'}> <div - className={ - 'line-clamp-1 my-1 text-lg text-indigo-700 sm:my-3 sm:text-2xl' - } + className={'line-clamp-1 my-2 text-2xl text-indigo-700 sm:my-3'} > {group.name} </div> @@ -275,7 +271,7 @@ export default function GroupPage(props: { <Linkify text={group.about} /> </div> </div> - <div className="hidden sm:block xl:hidden"> + <div className="mt-2"> <JoinOrAddQuestionsButtons group={group} user={user} @@ -283,13 +279,6 @@ export default function GroupPage(props: { /> </div> </Row> - <div className="block sm:hidden"> - <JoinOrAddQuestionsButtons - group={group} - user={user} - isMember={!!isMember} - /> - </div> </Col> <Tabs currentPageForAnalytics={groupPath(group.slug)} @@ -308,21 +297,7 @@ function JoinOrAddQuestionsButtons(props: { }) { const { group, user, isMember } = props return user && isMember ? ( - <Row - className={'-mt-2 justify-between sm:mt-0 sm:flex-col sm:justify-center'} - > - <CreateQuestionButton - user={user} - overrideText={'Add a new question'} - className={'hidden w-48 flex-shrink-0 sm:block'} - query={`?groupId=${group.id}`} - /> - <CreateQuestionButton - user={user} - overrideText={'New question'} - className={'block w-40 flex-shrink-0 sm:hidden'} - query={`?groupId=${group.id}`} - /> + <Row className={'mt-0 justify-end'}> <AddContractButton group={group} user={user} /> </Row> ) : group.anyoneCanJoin ? ( @@ -559,7 +534,7 @@ function GroupLeaderboards(props: { } function AddContractButton(props: { group: Group; user: User }) { - const { group } = props + const { group, user } = props const [open, setOpen] = useState(false) async function addContractToCurrentGroup(contract: Contract) { @@ -569,16 +544,39 @@ function AddContractButton(props: { group: Group; user: User }) { return ( <> + <div className={'flex justify-center'}> + <button + className={clsx('btn btn-sm btn-outline')} + onClick={() => setOpen(true)} + > + <PlusSmIcon className="h-6 w-6" aria-hidden="true" /> question + </button> + </div> + <Modal open={open} setOpen={setOpen} className={'sm:p-0'}> <Col className={ - 'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white p-8' + 'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white' } > - <div className={'text-lg text-indigo-700'}> - Add a question to your group - </div> - <div className={'overflow-y-scroll p-1'}> + <Col className="p-8 pb-0"> + <div className={'text-xl text-indigo-700'}> + Add a question to your group + </div> + + <Col className="items-center"> + <CreateQuestionButton + user={user} + overrideText={'New question'} + className={'w-48 flex-shrink-0 '} + query={`?groupId=${group.id}`} + /> + + <div className={'mt-2 text-lg text-indigo-700'}>or</div> + </Col> + </Col> + + <div className={'overflow-y-scroll sm:px-8'}> <ContractSearch hideOrderSelector={true} onContractClick={addContractToCurrentGroup} @@ -590,26 +588,6 @@ function AddContractButton(props: { group: Group; user: User }) { </div> </Col> </Modal> - <div className={'flex justify-center'}> - <button - className={clsx( - createButtonStyle, - 'hidden w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:block' - )} - onClick={() => setOpen(true)} - > - Add an old question - </button> - <button - className={clsx( - createButtonStyle, - 'block w-40 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:hidden' - )} - onClick={() => setOpen(true)} - > - Old question - </button> - </div> </> ) } From b48e910f703f11401986d62ee798bcd4af633bfe Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 19 Jul 2022 18:20:03 -0500 Subject: [PATCH 260/519] fix areaBaselineValue --- web/components/contract/contract-prob-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index c829c646..bafb84fe 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -150,7 +150,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { enableSlices="x" enableGridX={!!width && width >= 800} enableArea - areaBaselineValue={isBinary ? 0 : contract.min} + areaBaselineValue={isBinary || isLogScale ? 0 : contract.min} margin={{ top: 20, right: 20, bottom: 65, left: 40 }} animate={false} sliceTooltip={SliceTooltip} From 921ac4b2a9f1b1b6264facb3c9a9fd530460adf7 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 19 Jul 2022 17:22:23 -0600 Subject: [PATCH 261/519] Fix last 3 days number --- functions/src/score-contracts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/score-contracts.ts b/functions/src/score-contracts.ts index 733f1397..57976ff2 100644 --- a/functions/src/score-contracts.ts +++ b/functions/src/score-contracts.ts @@ -14,8 +14,8 @@ const firestore = admin.firestore() async function scoreContractsInternal() { const now = Date.now() - const lastHour = now - 3600000 - const last3Days = now - 2592000000 + const lastHour = now - 60 * 60 * 1000 + const last3Days = now - 1000 * 60 * 60 * 24 * 3 const activeContractsSnap = await firestore .collection('contracts') .where('lastUpdatedTime', '>', lastHour) From b2c89d36cf4aeb03fb8d9a6c89e2397c2b2bb73c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 19 Jul 2022 18:22:55 -0500 Subject: [PATCH 262/519] Home: Show pills that are groups (in addition to All, For you) --- web/components/buttons/pill-button.tsx | 2 +- web/components/contract-search.tsx | 101 ++++++++++++++++--------- web/tailwind.config.js | 17 +++++ 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/web/components/buttons/pill-button.tsx b/web/components/buttons/pill-button.tsx index 796036d1..5b4962b7 100644 --- a/web/components/buttons/pill-button.tsx +++ b/web/components/buttons/pill-button.tsx @@ -13,7 +13,7 @@ export function PillButton(props: { return ( <button className={clsx( - 'cursor-pointer select-none rounded-full', + 'cursor-pointer select-none whitespace-nowrap rounded-full', selected ? ['text-white', color ?? 'bg-gray-700'] : 'bg-gray-100 hover:bg-gray-200', diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index dc97f482..fbbb9fe9 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -27,7 +27,7 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' -import { toPairs } from 'lodash' +import { sortBy } from 'lodash' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -49,13 +49,6 @@ const sortIndexes = [ export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' -const filterOptions: { [label: string]: filter } = { - All: 'all', - Open: 'open', - Closed: 'closed', - Resolved: 'resolved', - 'For you': 'personal', -} export function ContractSearch(props: { querySortOptions?: { @@ -86,9 +79,14 @@ export function ContractSearch(props: { } = props const user = useUser() - const memberGroupSlugs = useMemberGroups(user?.id) - ?.map((g) => g.slug) - .filter((s) => !NEW_USER_GROUP_SLUGS.includes(s)) + const memberGroups = (useMemberGroups(user?.id) ?? []).filter( + (group) => !NEW_USER_GROUP_SLUGS.includes(group.slug) + ) + const memberGroupSlugs = memberGroups.map((g) => g.slug) + const pillGroups = sortBy( + memberGroups.filter((group) => group.contractIds.length > 0), + (group) => group.contractIds.length + ).reverse() const follows = useFollows(user?.id) const { initialSort } = useInitialQueryAndSort(querySortOptions) @@ -101,15 +99,27 @@ export function ContractSearch(props: { const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' ) + const [pillFilter, setPillFilter] = useState<string | undefined>() const { filters, numericFilters } = useMemo(() => { let filters = [ filter === 'open' ? 'isResolved:false' : '', filter === 'closed' ? 'isResolved:false' : '', filter === 'resolved' ? 'isResolved:true' : '', - filter === 'personal' + additionalFilter?.creatorId + ? `creatorId:${additionalFilter.creatorId}` + : '', + additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', + additionalFilter?.groupSlug + ? `groupSlugs:${additionalFilter.groupSlug}` + : '', + pillFilter && pillFilter !== 'personal' + ? `groupSlugs:${pillFilter}` + : '', + pillFilter === 'personal' ? // Show contracts in groups that the user is a member of - (memberGroupSlugs?.map((slug) => `groupSlugs:${slug}`) ?? []) + memberGroupSlugs + .map((slug) => `groupSlugs:${slug}`) // Show contracts created by users the user follows .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) // Show contracts bet on by users the user follows @@ -119,13 +129,6 @@ export function ContractSearch(props: { ) .concat(user ? `uniqueBettorIds:${user.id}` : []) : '', - additionalFilter?.creatorId - ? `creatorId:${additionalFilter.creatorId}` - : '', - additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', - additionalFilter?.groupSlug - ? `groupSlugs:${additionalFilter.groupSlug}` - : '', ].filter((f) => f) // Hack to make Algolia work. filters = ['', ...filters] @@ -139,8 +142,9 @@ export function ContractSearch(props: { }, [ filter, Object.values(additionalFilter ?? {}).join(','), - (memberGroupSlugs ?? []).join(','), + memberGroupSlugs.join(','), (follows ?? []).join(','), + pillFilter, ]) const indexName = `${indexPrefix}contracts-${sort}` @@ -167,6 +171,17 @@ export function ContractSearch(props: { }} /> {/*// TODO track WHICH filter users are using*/} + <select + className="!select !select-bordered" + value={filter} + onChange={(e) => setFilter(e.target.value as filter)} + onBlur={trackCallback('select search filter')} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> {!hideOrderSelector && ( <SortBy items={sortIndexes} @@ -186,25 +201,41 @@ export function ContractSearch(props: { <Spacer h={3} /> - <Row className="gap-2"> - {toPairs<filter>(filterOptions).map(([label, f]) => { - return ( - <PillButton - key={f} - selected={filter === f} - onSelect={() => setFilter(f)} - > - {label} - </PillButton> - ) - })} - </Row> + {!additionalFilter?.creatorId && !additionalFilter?.groupSlug && ( + <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> + <PillButton + key={'all'} + selected={pillFilter === undefined} + onSelect={() => setPillFilter(undefined)} + > + All + </PillButton> + <PillButton + key={'personal'} + selected={pillFilter === 'personal'} + onSelect={() => setPillFilter('personal')} + > + For you + </PillButton> + {pillGroups.map(({ name, slug }) => { + return ( + <PillButton + key={slug} + selected={pillFilter === slug} + onSelect={() => setPillFilter(slug)} + > + {name} + </PillButton> + ) + })} + </Row> + )} <Spacer h={3} /> {filter === 'personal' && (follows ?? []).length === 0 && - (memberGroupSlugs ?? []).length === 0 ? ( + memberGroupSlugs.length === 0 ? ( <>You're not following anyone, nor in any of your own groups yet.</> ) : ( <ContractSearchInner diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 0a1616b6..3457b7a6 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,4 +1,5 @@ const defaultTheme = require('tailwindcss/defaultTheme') +const plugin = require('tailwindcss/plugin') module.exports = { content: [ @@ -32,6 +33,22 @@ module.exports = { require('@tailwindcss/typography'), require('@tailwindcss/line-clamp'), require('daisyui'), + plugin(function ({ addUtilities }) { + addUtilities({ + '.scrollbar-hide': { + /* IE and Edge */ + '-ms-overflow-style': 'none', + + /* Firefox */ + 'scrollbar-width': 'none', + + /* Safari and Chrome */ + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + }) + }), ], daisyui: { themes: [ From 61094ea17df5210c2002b495aa72d305aa2b933a Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 20:08:33 -0700 Subject: [PATCH 263/519] Properly handle expired ID token cookie, be robust to errors (#671) --- web/lib/firebase/server-auth.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts index 5f828683..47eadb45 100644 --- a/web/lib/firebase/server-auth.ts +++ b/web/lib/firebase/server-auth.ts @@ -52,12 +52,19 @@ export const getServerAuthenticatedUid = async (ctx: RequestContext) => { if (idToken != null) { try { return (await auth.verifyIdToken(idToken))?.uid + } catch { + // plausibly expired; try the refresh token, if it's present + } + } + if (refreshToken != null) { + try { + const resp = await requestFirebaseIdToken(refreshToken) + setAuthCookies(resp.id_token, resp.refresh_token, ctx.res) + return (await auth.verifyIdToken(resp.id_token))?.uid } catch (e) { - if (refreshToken != null) { - const resp = await requestFirebaseIdToken(refreshToken) - setAuthCookies(resp.id_token, resp.refresh_token, ctx.res) - return (await auth.verifyIdToken(resp.id_token))?.uid - } + // this is a big unexpected problem -- either their cookies are corrupt + // or the refresh token API is down. functionally, they are not logged in + console.error(e) } } return undefined From bacd546e5d27863e460348c6ca3a504a1f6fef6d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 19 Jul 2022 20:10:54 -0700 Subject: [PATCH 264/519] Fix unused import from Ian's code --- web/pages/notifications.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index d011e757..3db345ef 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -16,7 +16,6 @@ import { User, } from 'common/user' import { getUser } from 'web/lib/firebase/users' -import { LoadingIndicator } from 'web/components/loading-indicator' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { Linkify } from 'web/components/linkify' From 83e9408d6958a957ee29508b359f69775b4f437d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 19 Jul 2022 23:48:09 -0500 Subject: [PATCH 265/519] remove tags from info panel --- web/components/contract/contract-info-dialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index b5ecea15..81ef59c4 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -141,9 +141,10 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { </tbody> </table> - <div>Tags</div> + {/* <div>Tags</div> <TagsInput contract={contract} /> - <div /> + <div /> */} + {contract.mechanism === 'cpmm-1' && !contract.resolution && ( <LiquidityPanel contract={contract} /> )} From 0013f76873e958ea01a7b5beb657553f8ecc5d2f Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 00:03:03 -0500 Subject: [PATCH 266/519] search defaults to 'for you'; hide pills for additional filters --- web/components/contract-search.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index fbbb9fe9..95075986 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -99,7 +99,7 @@ export function ContractSearch(props: { const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' ) - const [pillFilter, setPillFilter] = useState<string | undefined>() + const [pillFilter, setPillFilter] = useState<string | undefined>('personal') const { filters, numericFilters } = useMemo(() => { let filters = [ @@ -201,7 +201,7 @@ export function ContractSearch(props: { <Spacer h={3} /> - {!additionalFilter?.creatorId && !additionalFilter?.groupSlug && ( + {!additionalFilter && ( <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> <PillButton key={'all'} From 2b13085dff4a112b048716fbc833cc81fb83bba3 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 00:23:00 -0500 Subject: [PATCH 267/519] search: use default categories for non-authed users --- common/categories.ts | 6 ++++++ web/components/contract-search.tsx | 22 ++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/common/categories.ts b/common/categories.ts index 232aa526..672f3200 100644 --- a/common/categories.ts +++ b/common/categories.ts @@ -1,6 +1,7 @@ import { difference } from 'lodash' export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default' + export const CATEGORIES = { politics: 'Politics', technology: 'Technology', @@ -37,3 +38,8 @@ export const EXCLUDED_CATEGORIES: category[] = [ ] export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES) + +export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({ + slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX, + name: CATEGORIES[c as category], +})) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 95075986..65c09608 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -25,9 +25,10 @@ import { useFollows } from 'web/hooks/use-follows' import { trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' -import { NEW_USER_GROUP_SLUGS } from 'common/group' +import { Group, NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' import { sortBy } from 'lodash' +import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -82,11 +83,21 @@ export function ContractSearch(props: { const memberGroups = (useMemberGroups(user?.id) ?? []).filter( (group) => !NEW_USER_GROUP_SLUGS.includes(group.slug) ) - const memberGroupSlugs = memberGroups.map((g) => g.slug) - const pillGroups = sortBy( + const memberGroupSlugs = + memberGroups.length > 0 + ? memberGroups.map((g) => g.slug) + : DEFAULT_CATEGORY_GROUPS.map((g) => g.slug) + + const memberPillGroups = sortBy( memberGroups.filter((group) => group.contractIds.length > 0), (group) => group.contractIds.length ).reverse() + + const defaultPillGroups = DEFAULT_CATEGORY_GROUPS as Group[] + + const pillGroups = + memberPillGroups.length > 0 ? memberPillGroups : defaultPillGroups + const follows = useFollows(user?.id) const { initialSort } = useInitialQueryAndSort(querySortOptions) @@ -113,9 +124,7 @@ export function ContractSearch(props: { additionalFilter?.groupSlug ? `groupSlugs:${additionalFilter.groupSlug}` : '', - pillFilter && pillFilter !== 'personal' - ? `groupSlugs:${pillFilter}` - : '', + pillFilter && pillFilter !== 'personal' ? `groupSlugs:${pillFilter}` : '', pillFilter === 'personal' ? // Show contracts in groups that the user is a member of memberGroupSlugs @@ -217,6 +226,7 @@ export function ContractSearch(props: { > For you </PillButton> + {pillGroups.map(({ name, slug }) => { return ( <PillButton From b517f7cfa744f9660308f35aaecdd3ec65926451 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 00:35:27 -0500 Subject: [PATCH 268/519] eslint; remove unused tags import --- web/components/contract/contract-info-dialog.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 81ef59c4..a0c7fcc9 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -16,7 +16,6 @@ import { ShareEmbedButton } from '../share-embed-button' import { Title } from '../title' import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' -import { TagsInput } from 'web/components/tags-input' import { DuplicateContractButton } from '../copy-contract-button' export const contractDetailsButtonClassName = @@ -141,10 +140,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { </tbody> </table> - {/* <div>Tags</div> - <TagsInput contract={contract} /> - <div /> */} - {contract.mechanism === 'cpmm-1' && !contract.resolution && ( <LiquidityPanel contract={contract} /> )} From c8361f1748d73bdc0633c2762ab478706fb2e25d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Wed, 20 Jul 2022 01:59:14 -0700 Subject: [PATCH 269/519] Make it so that if you sign in on / you get redirected to /home (#672) --- web/pages/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 44683a4f..d9ff7f51 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,5 +1,6 @@ -import React from 'react' - +import React, { useEffect } from 'react' +import { useRouter } from 'next/router' +import { useUser } from 'web/hooks/use-user' import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts' import { Page } from 'web/components/page' import { LandingPagePanel } from 'web/components/landing-page-panel' @@ -26,6 +27,17 @@ export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { export default function Home(props: { hotContracts: Contract[] }) { const { hotContracts } = props + + // for now this redirect in the component is how we handle the case where they are + // on this page and they log in -- in the future we will make some cleaner way + const user = useUser() + const router = useRouter() + useEffect(() => { + if (user != null) { + router.replace('/home') + } + }, [router, user]) + return ( <Page> <div className="px-4 pt-2 md:mt-0 lg:hidden"> From b60892fada294e42481e1372a3f0474ee419fa64 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 11:15:55 -0500 Subject: [PATCH 270/519] group 'rankings' => 'leaderboards' (friendlier, more consistent terminology) --- web/lib/firebase/groups.ts | 2 +- web/pages/group/[...slugs]/index.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 8adb5606..0122e2ee 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -22,7 +22,7 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'leaderboards' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 7cce843e..c6485a0e 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -113,7 +113,7 @@ const groupSubpages = [ undefined, GROUP_CHAT_SLUG, 'questions', - 'rankings', + 'leaderboards', 'about', ] as const @@ -236,9 +236,9 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'questions'), }, { - title: 'Rankings', + title: 'Leaderboards', content: leaderboard, - href: groupPath(group.slug, 'rankings'), + href: groupPath(group.slug, 'leaderboards'), }, { title: 'About', @@ -487,14 +487,14 @@ function GroupLeaderboards(props: { <SortedLeaderboard users={members} scoreFunction={(user) => traderScores[user.id] ?? 0} - title="🏅 Bettor rankings" + title="🏅 Top bettors" header="Profit" maxToShow={maxToShow} /> <SortedLeaderboard users={members} scoreFunction={(user) => creatorScores[user.id] ?? 0} - title="🏅 Creator rankings" + title="🏅 Top creators" header="Market volume" maxToShow={maxToShow} /> From 45b883477dae977cda488616db5736fe369fadab Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 11:42:49 -0500 Subject: [PATCH 271/519] generic copy link button --- web/components/copy-link-button.tsx | 87 +++++++++++++++-------------- web/components/share-market.tsx | 16 +++--- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/web/components/copy-link-button.tsx b/web/components/copy-link-button.tsx index ab6dd66f..4ce4140d 100644 --- a/web/components/copy-link-button.tsx +++ b/web/components/copy-link-button.tsx @@ -3,58 +3,63 @@ import { LinkIcon } from '@heroicons/react/outline' import { Menu, Transition } from '@headlessui/react' import clsx from 'clsx' -import { Contract } from 'common/contract' import { copyToClipboard } from 'web/lib/util/copy' -import { contractPath } from 'web/lib/firebase/contracts' -import { ENV_CONFIG } from 'common/envs/constants' import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' - -function copyContractUrl(contract: Contract) { - copyToClipboard(`https://${ENV_CONFIG.domain}${contractPath(contract)}`) -} +import { Row } from './layout/row' export function CopyLinkButton(props: { - contract: Contract + url: string + displayUrl?: string + tracking?: string buttonClassName?: string toastClassName?: string }) { - const { contract, buttonClassName, toastClassName } = props + const { url, displayUrl, tracking, buttonClassName, toastClassName } = props return ( - <Menu - as="div" - className="relative z-10 flex-shrink-0" - onMouseUp={() => { - copyContractUrl(contract) - track('copy share link') - }} - > - <Menu.Button - className={clsx( - 'btn btn-xs border-2 border-green-600 bg-white normal-case text-green-600 hover:border-green-600 hover:bg-white', - buttonClassName - )} - > - <LinkIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> - Copy link - </Menu.Button> + <Row className="w-full"> + <input + className="input input-bordered flex-1 rounded-r-none text-gray-500" + readOnly + type="text" + value={displayUrl ?? url} + /> - <Transition - as={Fragment} - enter="transition ease-out duration-100" - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leave="transition ease-in duration-75" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" + <Menu + as="div" + className="relative z-10 flex-shrink-0" + onMouseUp={() => { + copyToClipboard(url) + track(tracking ?? 'copy share link') + }} > - <Menu.Items> - <Menu.Item> - <ToastClipboard className={toastClassName} /> - </Menu.Item> - </Menu.Items> - </Transition> - </Menu> + <Menu.Button + className={clsx( + 'btn btn-xs border-2 border-green-600 bg-white normal-case text-green-600 hover:border-green-600 hover:bg-white', + buttonClassName + )} + > + <LinkIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> + Copy link + </Menu.Button> + + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items> + <Menu.Item> + <ToastClipboard className={toastClassName} /> + </Menu.Item> + </Menu.Items> + </Transition> + </Menu> + </Row> ) } diff --git a/web/components/share-market.tsx b/web/components/share-market.tsx index a5da585f..be943a34 100644 --- a/web/components/share-market.tsx +++ b/web/components/share-market.tsx @@ -1,5 +1,8 @@ import clsx from 'clsx' -import { Contract, contractUrl } from 'web/lib/firebase/contracts' + +import { ENV_CONFIG } from 'common/envs/constants' + +import { Contract, contractPath, contractUrl } from 'web/lib/firebase/contracts' import { CopyLinkButton } from './copy-link-button' import { Col } from './layout/col' import { Row } from './layout/row' @@ -7,18 +10,15 @@ import { Row } from './layout/row' export function ShareMarket(props: { contract: Contract; className?: string }) { const { contract, className } = props + const url = `https://${ENV_CONFIG.domain}${contractPath(contract)}` + return ( <Col className={clsx(className, 'gap-3')}> <div>Share your market</div> <Row className="mb-6 items-center"> - <input - className="input input-bordered flex-1 rounded-r-none text-gray-500" - readOnly - type="text" - value={contractUrl(contract)} - /> <CopyLinkButton - contract={contract} + url={url} + displayUrl={contractUrl(contract)} buttonClassName="btn-md rounded-l-none" toastClassName={'-left-28 mt-1'} /> From d65a60984d4dc08cb95652790890e35dc6530d9b Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 11:45:53 -0500 Subject: [PATCH 272/519] make group invite link more prominent --- web/pages/group/[...slugs]/index.tsx | 36 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c6485a0e..f95cf399 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -52,6 +52,8 @@ import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { searchInAny } from 'common/util/parse' import { useWindowSize } from 'web/hooks/use-window-size' +import { CopyLinkButton } from 'web/components/copy-link-button' +import { ENV_CONFIG } from 'common/envs/constants' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -328,6 +330,11 @@ function GroupOverview(props: { }) } + const postFix = user ? '?referrer=' + user.username : '' + const shareUrl = `https://${ENV_CONFIG.domain}${groupPath( + group.slug + )}${postFix}` + return ( <> <Col className="gap-2 rounded-b bg-white p-2"> @@ -372,21 +379,26 @@ function GroupOverview(props: { </span> )} </Row> + {anyoneCanJoin && user && ( - <Row className={'flex-wrap items-center gap-1'}> - <span className={'text-gray-500'}>Share</span> - <ShareIconButton - group={group} - username={user.username} - buttonClassName={'hover:bg-gray-300 mt-1 !text-gray-700'} - > - <span className={'mx-2'}> - Invite a friend and get M${REFERRAL_AMOUNT} if they sign up! - </span> - </ShareIconButton> - </Row> + <Col className="my-4 px-2"> + <div className="text-lg">Invite</div> + <div className={'mb-2 text-gray-500'}> + Invite a friend to this group and get M${REFERRAL_AMOUNT} if they + sign up! + </div> + + <CopyLinkButton + url={shareUrl} + tracking="copy group share link" + buttonClassName="btn-md rounded-l-none" + toastClassName={'-left-28 mt-1'} + /> + </Col> )} + <Col className={'mt-2'}> + <div className="mb-2 text-lg">Members</div> <GroupMemberSearch members={members} group={group} /> </Col> </Col> From 202132868f0054a975c6a6adec7619da6544104e Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 20 Jul 2022 12:35:04 -0700 Subject: [PATCH 273/519] lint and prettier --- web/lib/firebase/groups.ts | 7 ++++++- web/pages/group/[...slugs]/index.tsx | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 0122e2ee..fc028642 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -22,7 +22,12 @@ export const groups = coll<Group>('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'leaderboards' + subpath?: + | 'edit' + | 'questions' + | 'about' + | typeof GROUP_CHAT_SLUG + | 'leaderboards' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index f95cf399..8f1b6593 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -42,7 +42,6 @@ import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' -import { ShareIconButton } from 'web/components/share-icon-button' import { REFERRAL_AMOUNT } from 'common/user' import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' From 0870397fea13df257903a53ef0de60c885c69cf4 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 20 Jul 2022 12:36:23 -0700 Subject: [PATCH 274/519] Show line in menu on mobile --- web/components/nav/sidebar.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 77af99e0..ff740540 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -235,7 +235,10 @@ export default function Sidebar(props: { className?: string }) { buttonContent={<MoreButton />} /> )} - + {/* Spacer if there are any groups */} + {memberItems.length > 0 && ( + <hr className="!my-4 mr-2 border-gray-300" /> + )} {privateUser && ( <GroupsList currentPage={router.asPath} @@ -256,11 +259,7 @@ export default function Sidebar(props: { className?: string }) { /> {/* Spacer if there are any groups */} - {memberItems.length > 0 && ( - <div className="py-3"> - <div className="h-[1px] bg-gray-300" /> - </div> - )} + {memberItems.length > 0 && <hr className="!my-4 border-gray-300" />} {privateUser && ( <GroupsList currentPage={router.asPath} From e45d81513c0a592e43906cdecf8eb88f8fa851a5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 14:49:14 -0500 Subject: [PATCH 275/519] Don't filter by personal unless pills enabled --- web/components/contract-search.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 65c09608..b0fab34b 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -110,7 +110,10 @@ export function ContractSearch(props: { const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' ) - const [pillFilter, setPillFilter] = useState<string | undefined>('personal') + const pillsEnabled = !additionalFilter + const [pillFilter, setPillFilter] = useState<string | undefined>( + pillsEnabled ? 'personal' : undefined + ) const { filters, numericFilters } = useMemo(() => { let filters = [ @@ -210,7 +213,7 @@ export function ContractSearch(props: { <Spacer h={3} /> - {!additionalFilter && ( + {pillsEnabled && ( <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> <PillButton key={'all'} From 44afa92b58664faf9cd6dbcc96483aa66552d0c4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 15:05:48 -0500 Subject: [PATCH 276/519] Turn off for you by default --- web/components/contract-search.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index b0fab34b..01a65610 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -111,9 +111,7 @@ export function ContractSearch(props: { querySortOptions?.defaultFilter ?? 'open' ) const pillsEnabled = !additionalFilter - const [pillFilter, setPillFilter] = useState<string | undefined>( - pillsEnabled ? 'personal' : undefined - ) + const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) const { filters, numericFilters } = useMemo(() => { let filters = [ From c35d0a8bc6aeaa709018719a913f8c7abc25ec53 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 15:30:07 -0500 Subject: [PATCH 277/519] Split out "Your bets" from "For you" --- web/components/contract-search.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 01a65610..730b113f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -125,7 +125,9 @@ export function ContractSearch(props: { additionalFilter?.groupSlug ? `groupSlugs:${additionalFilter.groupSlug}` : '', - pillFilter && pillFilter !== 'personal' ? `groupSlugs:${pillFilter}` : '', + pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' + ? `groupSlugs:${pillFilter}` + : '', pillFilter === 'personal' ? // Show contracts in groups that the user is a member of memberGroupSlugs @@ -135,9 +137,13 @@ export function ContractSearch(props: { // Show contracts bet on by users the user follows .concat( follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] - // Show contracts bet on by the user ) - .concat(user ? `uniqueBettorIds:${user.id}` : []) + : '', + // Subtract contracts you bet on from For you. + pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', + pillFilter === 'your-bets' && user + ? // Show contracts bet on by the user + `uniqueBettorIds:${user.id}` : '', ].filter((f) => f) // Hack to make Algolia work. @@ -228,6 +234,14 @@ export function ContractSearch(props: { For you </PillButton> + <PillButton + key={'your-bets'} + selected={pillFilter === 'your-bets'} + onSelect={() => setPillFilter('your-bets')} + > + Your bets + </PillButton> + {pillGroups.map(({ name, slug }) => { return ( <PillButton From 302a6355425fe98675c44b33cceb31e28e86d1c7 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 16:06:24 -0500 Subject: [PATCH 278/519] group page max width --- web/pages/group/[...slugs]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 8f1b6593..0d38580c 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -253,7 +253,7 @@ export default function GroupPage(props: { <Page rightSidebar={showChatSidebar ? chatTab : undefined} rightSidebarClassName={showChatSidebar ? '!top-0' : ''} - className={showChatSidebar ? '!max-w-none !pb-0' : ''} + className={showChatSidebar ? '!max-w-7xl !pb-0' : ''} > <SEO title={group.name} From 75a1d606cb25590a38b16e14a8395bce326fce91 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 16:28:25 -0500 Subject: [PATCH 279/519] feed bets: show change in prob --- web/components/feed/feed-bets.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 1520e57c..ea73fe7b 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -113,8 +113,16 @@ export function BetStatusText(props: { truncate="short" />{' '} {isPseudoNumeric - ? ' than ' + formatNumericProbability(bet.probAfter, contract) - : ' at ' + + ? ' from ' + formatNumericProbability(bet.probBefore, contract) + : ' from ' + + formatPercent( + hadPoolMatch || isFreeResponse + ? bet.probBefore + : bet.limitProb ?? bet.probBefore + )} + {isPseudoNumeric + ? ' to ' + formatNumericProbability(bet.probAfter, contract) + : ' to ' + formatPercent( hadPoolMatch || isFreeResponse ? bet.probAfter From 49dcd97d7061b1007744307d29c060cd705a0cef Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 20 Jul 2022 17:04:11 -0500 Subject: [PATCH 280/519] feed bets: better prob display --- web/components/feed/feed-bets.tsx | 37 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index ea73fe7b..408404ba 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -93,6 +93,24 @@ export function BetStatusText(props: { bet.fills?.some((fill) => fill.matchedBetId === null)) ?? false + const fromProb = + hadPoolMatch || isFreeResponse + ? isPseudoNumeric + ? formatNumericProbability(bet.probBefore, contract) + : formatPercent(bet.probBefore) + : isPseudoNumeric + ? formatNumericProbability(bet.limitProb ?? bet.probBefore, contract) + : formatPercent(bet.limitProb ?? bet.probBefore) + + const toProb = + hadPoolMatch || isFreeResponse + ? isPseudoNumeric + ? formatNumericProbability(bet.probAfter, contract) + : formatPercent(bet.probAfter) + : isPseudoNumeric + ? formatNumericProbability(bet.limitProb ?? bet.probAfter, contract) + : formatPercent(bet.limitProb ?? bet.probAfter) + return ( <div className="text-sm text-gray-500"> {bettor ? ( @@ -112,22 +130,9 @@ export function BetStatusText(props: { contract={contract} truncate="short" />{' '} - {isPseudoNumeric - ? ' from ' + formatNumericProbability(bet.probBefore, contract) - : ' from ' + - formatPercent( - hadPoolMatch || isFreeResponse - ? bet.probBefore - : bet.limitProb ?? bet.probBefore - )} - {isPseudoNumeric - ? ' to ' + formatNumericProbability(bet.probAfter, contract) - : ' to ' + - formatPercent( - hadPoolMatch || isFreeResponse - ? bet.probAfter - : bet.limitProb ?? bet.probAfter - )} + {fromProb === toProb + ? `at ${fromProb}` + : `from ${fromProb} to ${toProb}`} </> )} <RelativeTimestamp time={createdTime} /> From ace39ef73d5e0d3bf318b7c22a44d921398e346b Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Wed, 20 Jul 2022 15:42:31 -0700 Subject: [PATCH 281/519] Update Next.js 12.1.2 -> 12.2.0 (#669) * Update Next.js 12.1.2 -> 12.2.0 * Further bump Next to 12.2.2 --- web/package.json | 2 +- yarn.lock | 178 +++++++++++++++++++++++++---------------------- 2 files changed, 96 insertions(+), 84 deletions(-) diff --git a/web/package.json b/web/package.json index f8e1881b..d09ccaf0 100644 --- a/web/package.json +++ b/web/package.json @@ -40,7 +40,7 @@ "gridjs-react": "5.0.2", "lodash": "4.17.21", "nanoid": "^3.3.4", - "next": "12.1.2", + "next": "12.2.2", "node-fetch": "3.2.4", "react": "17.0.2", "react-confetti": "6.0.1", diff --git a/yarn.lock b/yarn.lock index 6fcdf53a..ffa8e6f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2385,10 +2385,10 @@ resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@next/env@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.2.tgz#4b0f5fd448ac60b821d2486d2987948e3a099f03" - integrity sha512-A/P4ysmFScBFyu1ZV0Mr1Y89snyQhqGwsCrkEpK+itMF+y+pMqBoPVIyakUf4LXqGWJGiGFuIerihvSG70Ad8Q== +"@next/env@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.2.tgz#cc1a0a445bd254499e30f632968c03192455f4cc" + integrity sha512-BqDwE4gDl1F608TpnNxZqrCn6g48MBjvmWFEmeX5wEXDXh3IkAOw6ASKUgjT8H4OUePYFqghDFUss5ZhnbOUjw== "@next/eslint-plugin-next@12.1.6": version "12.1.6" @@ -2397,65 +2397,70 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.2.tgz#675e952d9032ac7bec02f3f413c17d33bbd90857" - integrity sha512-iwalfLBhYmCIlj09czFbovj1SmTycf0AGR8CB357wgmEN8xIuznIwSsCH87AhwQ9apfNtdeDhxvuKmhS9T3FqQ== +"@next/swc-android-arm-eabi@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.2.tgz#f6c4111e6371f73af6bf80c9accb3d96850a92cd" + integrity sha512-VHjuCHeq9qCprUZbsRxxM/VqSW8MmsUtqB5nEpGEgUNnQi/BTm/2aK8tl7R4D0twGKRh6g1AAeFuWtXzk9Z/vQ== -"@next/swc-android-arm64@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.2.tgz#d9710c50853235f258726b19a649df9c29a49682" - integrity sha512-ZoR0Vx7czJhTgRAcFbzTKQc2n2ChC036/uc6PbgYiI/LreEnfmsV/CiREP0pUVs5ndntOX8kBA3BSbh4zCO5tQ== +"@next/swc-android-arm64@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.2.tgz#b69de59c51e631a7600439e7a8993d6e82f3369e" + integrity sha512-v5EYzXUOSv0r9mO/2PX6mOcF53k8ndlu9yeFHVAWW1Dhw2jaJcvTRcCAwYYN8Q3tDg0nH3NbEltJDLKmcJOuVA== -"@next/swc-darwin-arm64@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.2.tgz#aadd21b711c82b3efa9b4ecf7665841259e1fa7e" - integrity sha512-VXv7lpqFjHwkK65CZHkjvBxlSBTG+l3O0Zl2zHniHj0xHzxJZvR8VFjV2zIMZCYSfVqeQ5yt2rjwuQ9zbpGtXQ== +"@next/swc-darwin-arm64@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.2.tgz#80157c91668eff95b72d052428c353eab0fc4c50" + integrity sha512-JCoGySHKGt+YBk7xRTFGx1QjrnCcwYxIo3yGepcOq64MoiocTM3yllQWeOAJU2/k9MH0+B5E9WUSme4rOCBbpA== -"@next/swc-darwin-x64@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.2.tgz#3b1a389828f5c88ecb828a6394692fdeaf175081" - integrity sha512-evXxJQnXEnU+heWyun7d0UV6bhBcmoiyFGR3O3v9qdhGbeXh+SXYVxRO69juuh6V7RWRdlb1KQ0rGUNa1k0XSw== +"@next/swc-darwin-x64@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.2.tgz#12be2f58e676fccff3d48a62921b9927ed295133" + integrity sha512-dztDtvfkhUqiqpXvrWVccfGhLe44yQ5tQ7B4tBfnsOR6vxzI9DNPHTlEOgRN9qDqTAcFyPxvg86mn4l8bB9Jcw== -"@next/swc-linux-arm-gnueabihf@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.2.tgz#db4371ca716bf94c94d4f6b001ac3c9d08d97d79" - integrity sha512-LJV/wo6R0Ot7Y/20bZs00aBG4J333RT6H/5Q2AROE4Hnx7cenSktSnfU6WCnJgzYLSIHdbLs549LcZMULuVquw== +"@next/swc-freebsd-x64@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.2.tgz#de1363431a49059f1efb8c0f86ce6a79c53b3a95" + integrity sha512-JUnXB+2xfxqsAvhFLPJpU1NeyDsvJrKoOjpV7g3Dxbno2Riu4tDKn3kKF886yleAuD/1qNTUCpqubTvbbT2VoA== -"@next/swc-linux-arm64-gnu@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.2.tgz#0e71db03b8b12ed315c8be7d15392ecefe562b7c" - integrity sha512-fjlYU1Y8kVjjRKyuyQBYLHPxjGOS2ox7U8TqAvtgKvd2PxqdsgW4sP+VDovRVPrZlGXNllKoJiqMO1OoR9fB6w== +"@next/swc-linux-arm-gnueabihf@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.2.tgz#d5b8e0d1bb55bbd9db4d2fec018217471dc8b9e6" + integrity sha512-XeYC/qqPLz58R4pjkb+x8sUUxuGLnx9QruC7/IGkK68yW4G17PHwKI/1njFYVfXTXUukpWjcfBuauWwxp9ke7Q== -"@next/swc-linux-arm64-musl@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.2.tgz#f1b055793da1c12167ed3b6e32aef8289721a1fb" - integrity sha512-Y1JRDMHqSjLObjyrD1hf6ePrJcOF/mkw+LbAzoNgrHL1dSuIAqcz3jYunJt8T7Yw48xSJy6LPSL9BclAHwEwOA== +"@next/swc-linux-arm64-gnu@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.2.tgz#3bc75984e1d5ec8f59eb53702cc382d8e1be2061" + integrity sha512-d6jT8xgfKYFkzR7J0OHo2D+kFvY/6W8qEo6/hmdrTt6AKAqxs//rbbcdoyn3YQq1x6FVUUd39zzpezZntg9Naw== -"@next/swc-linux-x64-gnu@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.2.tgz#69764ffaacb3b9b373897fff15d7dd871455efe2" - integrity sha512-5N4QSRT60ikQqCU8iHfYZzlhg6MFTLsKhMTARmhn8wLtZfN9VVyTFwZrJQWjV64dZc4JFeXDANGao8fm55y6bw== +"@next/swc-linux-arm64-musl@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.2.tgz#270db73e07a18d999f61e79a917943fa5bc1ef56" + integrity sha512-rIZRFxI9N/502auJT1i7coas0HTHUM+HaXMyJiCpnY8Rimbo0495ir24tzzHo3nQqJwcflcPTwEh/DV17sdv9A== -"@next/swc-linux-x64-musl@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.2.tgz#0ddaedb5ec578c01771f83be2046dafb2f70df91" - integrity sha512-b32F/xAgdYG4Pt0foFzhF+2uhvNxnEj7aJNp1R4EhZotdej2PzvFWcP/dGkc7MJl205pBz5oC3gHyILIIlW6XA== +"@next/swc-linux-x64-gnu@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.2.tgz#e6c72fa20478552e898c434f4d4c0c5e89d2ea78" + integrity sha512-ir1vNadlUDj7eQk15AvfhG5BjVizuCHks9uZwBfUgT5jyeDCeRvaDCo1+Q6+0CLOAnYDR/nqSCvBgzG2UdFh9A== -"@next/swc-win32-arm64-msvc@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.2.tgz#9e17ed56d5621f8c6961193da3a0b155cea511c9" - integrity sha512-hVOcGmWDeVwO00Aclopsj6MoYhfJl5zA4vjAai9KjgclQTFZa/DC0vQjgKAHHKGT5oMHgjiq/G7L6P1/UfwYnw== +"@next/swc-linux-x64-musl@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.2.tgz#b9ef9efe2c401839cdefa5e70402386aafdce15a" + integrity sha512-bte5n2GzLN3O8JdSFYWZzMgEgDHZmRz5wiispiiDssj4ik3l8E7wq/czNi8RmIF+ioj2sYVokUNa/ekLzrESWw== -"@next/swc-win32-ia32-msvc@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.2.tgz#ddd260cbe8bc4002fb54415b80baccf37f8db783" - integrity sha512-wnVDGIVz2pR3vIkyN6IE+1NvMSBrBj1jba11iR16m8TAPzZH/PrNsxr0a9N5VavEXXLcQpoUVvT+N7nflbRAHg== +"@next/swc-win32-arm64-msvc@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.2.tgz#18fa7ec7248da3a7926a0601d9ececc53ac83157" + integrity sha512-ZUGCmcDmdPVSAlwJ/aD+1F9lYW8vttseiv4n2+VCDv5JloxiX9aY32kYZaJJO7hmTLNrprvXkb4OvNuHdN22Jg== -"@next/swc-win32-x64-msvc@12.1.2": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.2.tgz#37412a314bcf4c6006a74e1ef9764048344f3848" - integrity sha512-MLNcurEpQp0+7OU9261f7PkN52xTGkfrt4IYTIXau7DO/aHj927oK6piIJdl9EOHdX/KN5W6qlyErj170PSHtw== +"@next/swc-win32-ia32-msvc@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.2.tgz#54936e84f4a219441d051940354da7cd3eafbb4f" + integrity sha512-v7ykeEDbr9eXiblGSZiEYYkWoig6sRhAbLKHUHQtk8vEWWVEqeXFcxmw6LRrKu5rCN1DY357UlYWToCGPQPCRA== + +"@next/swc-win32-x64-msvc@12.2.2": + version "12.2.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.2.tgz#7460be700a60d75816f01109400b51fe929d7e89" + integrity sha512-2D2iinWUL6xx8D9LYVZ5qi7FP6uLAoWymt8m8aaG2Ld/Ka8/k723fJfiklfuAcwOxfufPJI+nRbT5VcgHGzHAQ== "@nivo/annotations@0.74.0": version "0.74.0" @@ -2837,6 +2842,13 @@ "@svgr/plugin-jsx" "^6.2.1" "@svgr/plugin-svgo" "^6.2.0" +"@swc/helpers@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.2.tgz#ed1f6997ffbc22396665d9ba74e2a5c0a2d782f8" + integrity sha512-556Az0VX7WR6UdoTn4htt/l3zPQ7bsQWK+HqdG4swV7beUCxo/BqmvbOpUkTIm/9ih86LIf1qsUnywNL3obGHw== + dependencies: + tslib "^2.4.0" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -4290,7 +4302,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz#8a1e7fdc4db9c2ec79a05e9fd68eb93a761888bb" integrity sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g== -caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001283, caniuse-lite@^1.0.30001332: +caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001332: version "1.0.30001341" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== @@ -8320,29 +8332,31 @@ next-sitemap@^2.5.14: "@corex/deepmerge" "^2.6.148" minimist "^1.2.6" -next@12.1.2: - version "12.1.2" - resolved "https://registry.yarnpkg.com/next/-/next-12.1.2.tgz#c5376a8ae17d3e404a2b691c01f94c8943306f29" - integrity sha512-JHPCsnFTBO0Z4SQxSYc611UA1WA+r/3y3Neg66AH5/gSO/oksfRnFw/zGX/FZ9+oOUHS9y3wJFawNpVYR2gJSQ== +next@12.2.2: + version "12.2.2" + resolved "https://registry.yarnpkg.com/next/-/next-12.2.2.tgz#029bf5e4a18a891ca5d05b189b7cd983fd22c072" + integrity sha512-zAYFY45aBry/PlKONqtlloRFqU/We3zWYdn2NoGvDZkoYUYQSJC8WMcalS5C19MxbCZLUVCX7D7a6gTGgl2yLg== dependencies: - "@next/env" "12.1.2" - caniuse-lite "^1.0.30001283" + "@next/env" "12.2.2" + "@swc/helpers" "0.4.2" + caniuse-lite "^1.0.30001332" postcss "8.4.5" - styled-jsx "5.0.1" - use-subscription "1.5.1" + styled-jsx "5.0.2" + use-sync-external-store "1.1.0" optionalDependencies: - "@next/swc-android-arm-eabi" "12.1.2" - "@next/swc-android-arm64" "12.1.2" - "@next/swc-darwin-arm64" "12.1.2" - "@next/swc-darwin-x64" "12.1.2" - "@next/swc-linux-arm-gnueabihf" "12.1.2" - "@next/swc-linux-arm64-gnu" "12.1.2" - "@next/swc-linux-arm64-musl" "12.1.2" - "@next/swc-linux-x64-gnu" "12.1.2" - "@next/swc-linux-x64-musl" "12.1.2" - "@next/swc-win32-arm64-msvc" "12.1.2" - "@next/swc-win32-ia32-msvc" "12.1.2" - "@next/swc-win32-x64-msvc" "12.1.2" + "@next/swc-android-arm-eabi" "12.2.2" + "@next/swc-android-arm64" "12.2.2" + "@next/swc-darwin-arm64" "12.2.2" + "@next/swc-darwin-x64" "12.2.2" + "@next/swc-freebsd-x64" "12.2.2" + "@next/swc-linux-arm-gnueabihf" "12.2.2" + "@next/swc-linux-arm64-gnu" "12.2.2" + "@next/swc-linux-arm64-musl" "12.2.2" + "@next/swc-linux-x64-gnu" "12.2.2" + "@next/swc-linux-x64-musl" "12.2.2" + "@next/swc-win32-arm64-msvc" "12.2.2" + "@next/swc-win32-ia32-msvc" "12.2.2" + "@next/swc-win32-x64-msvc" "12.2.2" no-case@^3.0.4: version "3.0.4" @@ -10892,10 +10906,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-jsx@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80" - integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw== +styled-jsx@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729" + integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ== stylehacks@^5.1.0: version "5.1.0" @@ -11437,12 +11451,10 @@ use-latest@^1.2.1: dependencies: use-isomorphic-layout-effect "^1.1.1" -use-subscription@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== - dependencies: - object-assign "^4.1.1" +use-sync-external-store@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" + integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" From aa554ca9f6bf8ffb403bbd7939406191348e83d0 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 20 Jul 2022 16:31:18 -0700 Subject: [PATCH 282/519] migrate useUsers hook to react-query (#674) --- web/hooks/use-users.ts | 32 ++++++++++++++------------------ web/lib/firebase/users.ts | 10 ---------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/web/hooks/use-users.ts b/web/hooks/use-users.ts index 1312444e..659395b8 100644 --- a/web/hooks/use-users.ts +++ b/web/hooks/use-users.ts @@ -1,32 +1,28 @@ import { useState, useEffect } from 'react' import { PrivateUser, User } from 'common/user' -import { - listenForAllUsers, - listenForPrivateUsers, -} from 'web/lib/firebase/users' import { groupBy, sortBy, difference } from 'lodash' import { getContractsOfUserBets } from 'web/lib/firebase/bets' import { useFollows } from './use-follows' import { useUser } from './use-user' +import { useFirestoreQueryData } from '@react-query-firebase/firestore' +import { DocumentData } from 'firebase/firestore' +import { users, privateUsers } from 'web/lib/firebase/users' export const useUsers = () => { - const [users, setUsers] = useState<User[]>([]) - - useEffect(() => { - listenForAllUsers(setUsers) - }, []) - - return users + const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, { + subscribe: true, + includeMetadataChanges: true, + }) + return result.data ?? [] } export const usePrivateUsers = () => { - const [users, setUsers] = useState<PrivateUser[]>([]) - - useEffect(() => { - listenForPrivateUsers(setUsers) - }, []) - - return users + const result = useFirestoreQueryData<DocumentData, PrivateUser[]>( + ['private users'], + privateUsers, + { subscribe: true, includeMetadataChanges: true } + ) + return result.data || [] } export const useDiscoverUsers = (userId: string | null | undefined) => { diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 884dde04..89852851 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -258,16 +258,6 @@ export async function listAllUsers() { return docs.map((doc) => doc.data()) } -export function listenForAllUsers(setUsers: (users: User[]) => void) { - listenForValues(users, setUsers) -} - -export function listenForPrivateUsers( - setUsers: (users: PrivateUser[]) => void -) { - listenForValues(privateUsers, setUsers) -} - export function getTopTraders(period: Period) { const topTraders = query( users, From 5ddf496dae0f05831beaa7b766f33af37495a6fb Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 18:34:15 -0500 Subject: [PATCH 283/519] Remove bet button from free response comments --- .../feed/feed-answer-comment-group.tsx | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 5c3be539..e1a17370 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,7 +1,6 @@ import { Answer } from 'common/answer' import { Bet } from 'common/bet' import { Comment } from 'common/comment' -import { formatPercent } from 'common/util/format' import React, { useEffect, useState } from 'react' import { Col } from 'web/components/layout/col' import { Modal } from 'web/components/layout/modal' @@ -11,8 +10,6 @@ import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { Linkify } from 'web/components/linkify' import clsx from 'clsx' -import { tradingAllowed } from 'web/lib/firebase/contracts' -import { BuyButton } from 'web/components/yes-no-selector' import { CommentInput, CommentRepliesList, @@ -23,7 +20,6 @@ import { useRouter } from 'next/router' import { groupBy } from 'lodash' import { User } from 'common/user' import { useEvent } from 'web/hooks/use-event' -import { getDpmOutcomeProbability } from 'common/calculate-dpm' import { CommentTipMap } from 'web/hooks/use-tip-txns' export function FeedAnswerCommentGroup(props: { @@ -50,11 +46,6 @@ export function FeedAnswerCommentGroup(props: { const commentsList = comments.filter( (comment) => comment.answerOutcome === answer.number.toString() ) - const thisAnswerProb = getDpmOutcomeProbability( - contract.totalShares, - answer.id - ) - const probPercent = formatPercent(thisAnswerProb) const betsByCurrentUser = (user && betsByUserId[user.id]) ?? [] const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? [] const isFreeResponseContractPage = !!commentsByCurrentUser @@ -125,7 +116,7 @@ export function FeedAnswerCommentGroup(props: { <Row className={clsx( - 'my-4 flex gap-3 space-x-3 transition-all duration-1000', + 'mt-4 flex gap-3 space-x-3 transition-all duration-1000', highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : '' )} id={answerElementId} @@ -162,24 +153,6 @@ export function FeedAnswerCommentGroup(props: { </button> </div> )} - - <div className={'align-items flex w-full justify-end gap-4 '}> - <span - className={clsx( - 'text-2xl', - tradingAllowed(contract) ? 'text-primary' : 'text-gray-500' - )} - > - {probPercent} - </span> - <BuyButton - className={clsx( - 'btn-sm flex-initial !px-6 sm:flex', - tradingAllowed(contract) ? '' : '!hidden' - )} - onClick={() => setOpen(true)} - /> - </div> </Row> </Col> {isFreeResponseContractPage && ( From 528dd2b28a1366685162256c7d0fd3ebf28d06ff Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 18:35:07 -0500 Subject: [PATCH 284/519] Make answer replies more closely spaced together --- web/components/feed/feed-answer-comment-group.tsx | 2 +- web/components/feed/feed-comments.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index e1a17370..a48a7e9c 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -103,7 +103,7 @@ export function FeedAnswerCommentGroup(props: { }, [answerElementId, router.asPath]) return ( - <Col className={'relative flex-1 gap-2'} key={answer.id + 'comment'}> + <Col className={'relative flex-1 gap-3'} key={answer.id + 'comment'}> <Modal open={open} setOpen={setOpen}> <AnswerBetPanel answer={answer} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 195c5343..d5accef0 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -142,7 +142,7 @@ export function CommentRepliesList(props: { id={comment.id} className={clsx( 'relative', - !treatFirstIndexEqually && commentIdx === 0 ? '' : 'mt-3 ml-6' + !treatFirstIndexEqually && commentIdx === 0 ? '' : 'ml-6' )} > {/*draw a gray line from the comment to the left:*/} From a3f150b1d984b3e4bdd3ecd9622695004b8865ec Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 16:57:51 -0700 Subject: [PATCH 285/519] Host Ida and Alex's MTG Guesser game (#656) * Copy over code from Mtg Guesser * Run Prettier * CSS Tweaks: Hover feedback, button positioning * Hide all but counterspell & burn, for now * Move to /mtg directory * Fix prettierignore * smaller jsons (#673) limited burn to only red cards and also added limited json files to only have fields needed to play * Add Ida's tweak to card position Co-authored-by: marsteralex <bob.masteralex@gmail.com> --- web/.prettierignore | 3 +- web/public/mtg/app.js | 362 ++++++++++++++++ web/public/mtg/choose.html | 225 ++++++++++ web/public/mtg/importCards.py | 92 ++++ web/public/mtg/index.html | 554 ++++++++++++++++++++++++ web/public/mtg/jsons/beast1.json | 1 + web/public/mtg/jsons/beast2.json | 1 + web/public/mtg/jsons/beast3.json | 1 + web/public/mtg/jsons/burn1.json | 1 + web/public/mtg/jsons/burn2.json | 1 + web/public/mtg/jsons/burn3.json | 1 + web/public/mtg/jsons/counterspell1.json | 1 + web/public/mtg/jsons/counterspell2.json | 1 + web/public/mtg/jsons/counterspell3.json | 1 + web/public/mtg/jsons/terror1.json | 1 + web/public/mtg/jsons/terror2.json | 1 + web/public/mtg/jsons/terror3.json | 1 + web/public/mtg/jsons/wrath1.json | 1 + web/public/mtg/jsons/wrath2.json | 1 + web/public/mtg/jsons/wrath3.json | 1 + 20 files changed, 1250 insertions(+), 1 deletion(-) create mode 100644 web/public/mtg/app.js create mode 100644 web/public/mtg/choose.html create mode 100644 web/public/mtg/importCards.py create mode 100644 web/public/mtg/index.html create mode 100644 web/public/mtg/jsons/beast1.json create mode 100644 web/public/mtg/jsons/beast2.json create mode 100644 web/public/mtg/jsons/beast3.json create mode 100644 web/public/mtg/jsons/burn1.json create mode 100644 web/public/mtg/jsons/burn2.json create mode 100644 web/public/mtg/jsons/burn3.json create mode 100644 web/public/mtg/jsons/counterspell1.json create mode 100644 web/public/mtg/jsons/counterspell2.json create mode 100644 web/public/mtg/jsons/counterspell3.json create mode 100644 web/public/mtg/jsons/terror1.json create mode 100644 web/public/mtg/jsons/terror2.json create mode 100644 web/public/mtg/jsons/terror3.json create mode 100644 web/public/mtg/jsons/wrath1.json create mode 100644 web/public/mtg/jsons/wrath2.json create mode 100644 web/public/mtg/jsons/wrath3.json diff --git a/web/.prettierignore b/web/.prettierignore index b79c5513..6cc1e5c7 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -1,3 +1,4 @@ # Ignore Next artifacts .next/ -out/ \ No newline at end of file +out/ +public/**/*.json \ No newline at end of file diff --git a/web/public/mtg/app.js b/web/public/mtg/app.js new file mode 100644 index 00000000..983b8651 --- /dev/null +++ b/web/public/mtg/app.js @@ -0,0 +1,362 @@ +mode = 'PLAY' +allData = {} +total = 0 +unseenTotal = 0 +probList = [] +nameList = [] +k = 12 +extra = 3 +artDict = {} +totalCorrect = 0 +totalSeen = 0 +wordsLeft = k + extra +imagesLeft = k +maxRounds = 20 +whichGuesser = 'counterspell' +un = false +online = false +firstPrint = false +flag = true +page = 1 + +document.location.search.split('&').forEach((pair) => { + let v = pair.split('=') + if (v[0] === '?whichguesser') { + whichGuesser = v[1] + } else if (v[0] === 'un') { + un = v[1] + } else if (v[0] === 'digital') { + online = v[1] + } else if (v[0] === 'original') { + firstPrint = v[1] + } +}) + +let firstFetch = fetch('jsons/' + whichGuesser + page + '.json') +fetchToResponse(firstFetch) + +function putIntoMapAndFetch(data) { + putIntoMap(data.data) + if (data.has_more) { + page += 1 + window.setTimeout(() => + fetchToResponse(fetch('jsons/' + whichGuesser + page + '.json')) + ) + } else { + for (const [key, value] of Object.entries(allData)) { + nameList.push(key) + probList.push( + value.length + + (probList.length === 0 ? 0 : probList[probList.length - 1]) + ) + unseenTotal = total + } + window.console.log(allData) + window.console.log(total) + window.console.log(probList) + window.console.log(nameList) + if (whichGuesser === 'counterspell') { + document.getElementById('guess-type').innerText = 'Counterspell Guesser' + } else if (whichGuesser === 'beast') { + document.getElementById('guess-type').innerText = + 'Finding Fantastic Beasts' + } else if (whichGuesser === 'terror') { + document.getElementById('guess-type').innerText = + "I'm a Terror-able Guesser" + } else if (whichGuesser === 'wrath') { + document.getElementById('guess-type').innerText = "I'll Clean Sweep" + } else if (whichGuesser === 'burn') { + document.getElementById('guess-type').innerText = 'Match With Hot Singles' + } + setUpNewGame() + } +} + +function getKSamples() { + let usedCounters = new Set() + let currentTotal = unseenTotal + let samples = {} + let i = 0 + while (i < k) { + let rand = Math.floor(Math.random() * currentTotal) + let count = 0 + for (const [key, value] of Object.entries(allData)) { + if (usedCounters.has(key)) { + continue + } else if (count >= rand) { + usedCounters.add(key) + currentTotal -= value.length + unseenTotal-- + let randIndex = Math.floor(Math.random() * value.length) + let arts = allData[key].splice(randIndex, 1) + samples[arts[0].artImg] = [key, arts[0].normalImg] + i++ + break + } else { + count += value.length + } + } + } + for (const key of usedCounters) { + if (allData[key].length === 0) { + delete allData[key] + } + } + let count = 0 + while (count < extra) { + let rand = Math.floor(Math.random() * total) + for (let j = 0; j < nameList.length; j++) { + if (j >= rand) { + if (usedCounters.has(nameList[j])) { + break + } + usedCounters.add(nameList[j]) + count += 1 + break + } + } + } + return [samples, usedCounters] +} + +function fetchToResponse(fetch) { + return fetch + .then((response) => response.json()) + .then((json) => { + putIntoMapAndFetch(json) + }) +} + +function determineIfSkip(card) { + if (!un) { + if (card.set_type === 'funny') { + return true + } + } + if (!online) { + if (card.digital) { + return true + } + } + if (firstPrint) { + if ( + card.reprint === true || + (card.frame_effects && card.frame_effects.includes('showcase')) + ) { + return true + } + } + // reskinned card names show in art crop + if (card.flavor_name) { + return true + } + // don't include racist cards + return card.content_warning +} + +function putIntoMap(data) { + for (let i = 0; i < data.length; i++) { + let card = data[i] + if (determineIfSkip(card)) { + continue + } + let name = card.name + // remove slashes from adventure cards + if (card.card_faces) { + name = card.card_faces[0].name + } + let normalImg = '' + if (card.image_uris.normal) { + normalImg = card.image_uris.normal + } else if (card.image_uris.large) { + normalImg = card.image_uris.large + } else if (card.image_uris.small) { + normalImg = card.image_uris.small + } else { + continue + } + let artImg = '' + if (card.image_uris.art_crop) { + artImg = card.image_uris.art_crop + } else { + continue + } + total += 1 + if (!allData[name]) { + allData[name] = [{ artImg: artImg, normalImg: normalImg }] + } else { + allData[name].push({ artImg: artImg, normalImg: normalImg }) + } + } +} + +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)) + let temp = array[i] + array[i] = array[j] + array[j] = temp + } +} + +function setUpNewGame() { + wordsLeft = k + extra + imagesLeft = k + let currentRound = totalSeen / k + if (currentRound + 1 === maxRounds) { + document.getElementById('round-number').innerText = 'Final Round' + } else { + document.getElementById('round-number').innerText = + 'Round ' + (1 + currentRound) + } + + setWordsLeft() + // select new cards + let sampledData = getKSamples() + artDict = sampledData[0] + let randomImages = Object.keys(artDict) + shuffleArray(randomImages) + let namesList = Array.from(sampledData[1]).sort() + // fill in the new cards and names + for (let cardIndex = 1; cardIndex <= k; cardIndex++) { + let currCard = document.getElementById('card-' + cardIndex) + currCard.classList.remove('incorrect') + currCard.dataset.name = '' + currCard.dataset.url = randomImages[cardIndex - 1] + currCard.style.backgroundImage = "url('" + currCard.dataset.url + "')" + } + const nameBank = document.querySelector('.names-bank') + for (nameIndex = 1; nameIndex <= k + extra; nameIndex++) { + currName = document.getElementById('name-' + nameIndex) + // window.console.log(currName) + currName.innerText = namesList[nameIndex - 1] + nameBank.appendChild(currName) + } +} + +function checkAnswers() { + let score = k + // show the correct full cards + for (cardIndex = 1; cardIndex <= k; cardIndex++) { + currCard = document.getElementById('card-' + cardIndex) + let incorrect = true + if (currCard.dataset.name) { + let guess = document.getElementById(currCard.dataset.name).innerText + // window.console.log(artDict[currCard.dataset.url][0], guess); + incorrect = artDict[currCard.dataset.url][0] !== guess + // decide if their guess was correct + } + if (incorrect) currCard.classList.add('incorrect') + // tally some kind of score + if (incorrect) score-- + // show the correct card + currCard.style.backgroundImage = + "url('" + artDict[currCard.dataset.url][1] + "')" + } + totalSeen += k + totalCorrect += score + document.getElementById('score-amount').innerText = score + '/' + k + document.getElementById('score-percent').innerText = Math.round( + (totalCorrect * 100) / totalSeen + ) + document.getElementById('score-amount-total').innerText = + totalCorrect + '/' + totalSeen +} + +function toggleMode() { + event.preventDefault() + if (mode === 'PLAY') { + mode = 'ANSWER' + document.querySelector('.play-page').classList.add('answer-page') + window.console.log(totalSeen) + if (totalSeen / k === maxRounds - 1) { + document.getElementById('submit').style.display = 'none' + } else { + document.getElementById('submit').value = 'Next Round' + } + checkAnswers() + } else { + mode = 'PLAY' + document.querySelector('.play-page').classList.remove('answer-page') + document.getElementById('submit').value = 'Submit' + setUpNewGame() + } +} + +function allowDrop(ev, id) { + ev.preventDefault() +} + +function drag(ev) { + ev.dataTransfer.setData('text', ev.target.id) + let nameEl = document.querySelector('.selected') + if (nameEl) nameEl.classList.remove('selected') +} + +function drop(ev, id) { + ev.preventDefault() + var data = ev.dataTransfer.getData('text') + dropOnCard(id, data) +} + +function returnDrop(ev) { + ev.preventDefault() + var data = ev.dataTransfer.getData('text') + returnToNameBank(data) +} + +function returnToNameBank(name) { + document + .querySelector('.names-bank') + .appendChild(document.getElementById(name)) + let prevContainer = document.querySelector('[data-name=' + name + ']') + if (prevContainer) { + prevContainer.dataset.name = '' + wordsLeft += 1 + imagesLeft += 1 + setWordsLeft() + } +} + +function selectName(ev) { + if (ev.target.parentNode.classList.contains('names-bank')) { + let nameEl = document.querySelector('.selected') + if (nameEl) nameEl.classList.remove('selected') + ev.target.classList.add('selected') + } else { + returnToNameBank(ev.target.id) + } +} + +function dropSelected(ev, id) { + ev.preventDefault() + let nameEl = document.querySelector('.selected') + window.console.log('drop selected', nameEl) + if (!nameEl) return + nameEl.classList.remove('selected') + dropOnCard(id, nameEl.id) +} + +function dropOnCard(id, data) { + let target = document.getElementById('card-' + id) + target.appendChild(document.getElementById(data)) + // if this already has a name, remove that name + if (target.dataset.name) { + returnToNameBank(target.dataset.name) + } + // remove name data from a previous card if there is one + let prevContainer = document.querySelector('[data-name=' + data + ']') + if (prevContainer) { + prevContainer.dataset.name = '' + } else { + wordsLeft -= 1 + imagesLeft -= 1 + setWordsLeft() + } + target.dataset.name = data +} + +function setWordsLeft() { + document.getElementById('words-left').innerText = + 'Unused Card Names: ' + wordsLeft + '/Images: ' + imagesLeft +} diff --git a/web/public/mtg/choose.html b/web/public/mtg/choose.html new file mode 100644 index 00000000..cb84ced5 --- /dev/null +++ b/web/public/mtg/choose.html @@ -0,0 +1,225 @@ +<!DOCTYPE html> +<html> + <head> + <!-- Google Tag Manager --> + <script> + ;(function (w, d, s, l, i) { + w[l] = w[l] || [] + w[l].push({ + 'gtm.start': new Date().getTime(), + event: 'gtm.js', + }) + var f = d.getElementsByTagName(s)[0], + j = d.createElement(s), + dl = l !== 'dataLayer' ? '&l=' + l : '' + j.async = true + j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl + f.parentNode.insertBefore(j, f) + })(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG') + </script> + <!-- End Google Tag Manager --> + <meta charset="UTF-8" /> + <style type="text/css"> + body { + position: relative; + } + + .play-page { + display: flex; + flex-direction: row-reverse; + font-family: Georgia, 'Times New Roman', Times, serif; + min-height: 200px; + } + + h1, + h3 { + font-family: Verdana, Geneva, Tahoma, sans-serif; + text-align: center; + } + + #submit { + margin-top: 10px; + padding: 8px 20px; + background-color: cadetblue; + border: none; + border-radius: 3px; + font-size: 1.1em; + color: white; + cursor: pointer; + } + + #submit:hover { + background-color: rgb(0, 146, 156); + } + + [type='radio'] { + display: none; + } + + [type='radio'] + label.radio-label { + background: lightgrey; + display: block; + padding: 10px; + border-radius: 4px; + cursor: pointer; + } + + label.radio-label:hover { + background: darkgrey; + } + + [type='radio']:checked + label.radio-label { + background: lightcoral; + } + + .radio-label h3 { + margin: 0; + display: inline-block; + vertical-align: middle; + width: 220px; + } + + .thumbnail { + display: inline-block; + vertical-align: middle; + width: 67px; + height: 48px; + margin-right: 4px; + } + + body { + padding: 70px 0 30px; + } + + #addl-options { + position: absolute; + top: 30px; + right: 30px; + background-color: white; + padding: 10px; + cursor: pointer; + width: 200px; + } + + #addl-options > summary { + list-style: none; + text-align: right; + } + </style> + </head> + <body> + <!-- Google Tag Manager (noscript) --> + <noscript> + <iframe + src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG" + height="0" + width="0" + style="display: none; visibility: hidden" + ></iframe> + </noscript> + <!-- End Google Tag Manager (noscript) --> + <h1>Magic the Guessering</h1> + <div class="play-page" style="justify-content: center"> + <form + method="get" + action="index.html" + style="display: flex; flex-direction: column; align-items: center" + > + <!-- <input type="radio" id="wrath" name="whichguesser" value="wrath" /> + <label class="radio-label" for="wrath"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0619d670-7b53-4185-a25d-2fab5db1aab5.jpg?1562896185" + /> + <h3>I'll Clean Sweep</h3></label + ><br /> --> + + <input + type="radio" + id="counterspell" + name="whichguesser" + value="counterspell" + checked + /> + <label class="radio-label" for="counterspell"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855" + /> + <h3>Counterspell Guesser</h3></label + ><br /> + + <!-- <input type="radio" id="terror" name="whichguesser" value="terror" /> + <label class="radio-label" for="terror"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dd5d601-aff7-4b7a-ab6c-b89f403af076.jpg?1562905752" + /> + <h3>I'm a Terror-able Guesser</h3></label + ><br /> --> + + <input type="radio" id="burn" name="whichguesser" value="burn" /> + <label class="radio-label" for="burn"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596" + /> + <h3>Match With Hot Singles</h3></label + ><br /> + + <!-- <input type="radio" id="beast" name="whichguesser" value="beast" /> + <label class="radio-label" for="beast"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469" + /> + <h3>Finding Fantastic Beasts</h3></label + > + <br /> --> + + <details id="addl-options"> + <summary> + <img + src="http://mythicspoiler.com/images/buttons/ustset.png" + style="width: 32px; vertical-align: top" + /> + Options + </summary> + <input type="checkbox" name="digital" id="digital" checked /> + <label for="digital">include digital cards</label> + <br /> + <input type="checkbox" name="un" id="un" checked /> + <label for="un">include un-cards</label> + <br /> + <input type="checkbox" name="original" id="original" /> + <label for="original">restrict to only original printing</label> + </details> + <input type="submit" id="submit" value="Play" /> + </form> + </div> + + <div style="margin: -40px 0 0; height: 60px"> + <a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a> + </div> + + <div + style=" + font-size: 0.9em; + position: absolute; + bottom: 0; + left: 0; + right: 0; + color: grey; + font-style: italic; + " + > + made by + <a + style="color: rgb(0, 146, 156); font-style: italic" + href="https://idamayer.com" + >Ida Mayer</a + > + & Alex Lien 2022 + </div> + </body> +</html> diff --git a/web/public/mtg/importCards.py b/web/public/mtg/importCards.py new file mode 100644 index 00000000..343cba1a --- /dev/null +++ b/web/public/mtg/importCards.py @@ -0,0 +1,92 @@ +import time +import requests +import json + +# add category name here +allCategories = ['counterspell', 'beast', 'terror', 'wrath', 'burn'] + + +def generate_initial_query(category): + string_query = 'https://api.scryfall.com/cards/search?q=' + if category == 'counterspell': + string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure' + elif category == 'beast': + string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken' + elif category == 'terror': + string_query += 'otag%3Acreature-removal+o%3A%2Fdestroy+target.%2A+%28creature%7Cpermanent%29%2F+%28t' \ + '%3Ainstant+or+t%3Asorcery%29+o%3Atarget+not%3Aadventure' + elif category == 'wrath': + string_query += 'otag%3Asweeper-creature+%28t%3Ainstant+or+t%3Asorcery%29+not%3Aadventure' + elif category == 'burn': + string_query += '%28c>%3Dr+or+mana>%3Dr%29+%28o%3A%2Fdamage+to+them%2F+or+%28o%3Adeals+o%3Adamage+o%3A' \ + '%2Fcontroller%28%5C.%7C+%29%2F%29+or+o%3A%2F~+deals+%28.%7C..%29+damage+to+%28any+target%7C' \ + '.*player%28%5C.%7C+or+planeswalker%29%7C.*opponent%28%5C.%7C+or+planeswalker%29%29%2F%29' \ + '+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure' + # add category string query here + string_query += '+-%28set%3Asld+%28%28cn>%3D231+cn<%3D233%29+or+%28cn>%3D321+cn<%3D324%29+or+%28cn>%3D185+cn' \ + '<%3D189%29+or+%28cn>%3D138+cn<%3D142%29+or+%28cn>%3D364+cn<%3D368%29+or+cn%3A669+or+cn%3A670%29' \ + '%29+-name%3A%2F%5EA-%2F+not%3Adfc+not%3Asplit+-set%3Acmb2+-set%3Acmb1+-set%3Aplist+-set%3Adbl' \ + '+-frame%3Aextendedart+language%3Aenglish&unique=art&page=' + print(string_query) + return string_query + + +def fetch_and_write_all(category, query): + count = 1 + will_repeat = True + while will_repeat: + will_repeat = fetch_and_write(category, query, count) + count += 1 + + +def fetch_and_write(category, query, count): + query += str(count) + response = requests.get(f"{query}").json() + time.sleep(0.1) + with open('jsons/' + category + str(count) + '.json', 'w') as f: + json.dump(to_compact_write_form(response), f) + return response['has_more'] + + +def to_compact_write_form(response): + fieldsToUse = ['has_more'] + fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital', + 'set_type'] + smallJson = dict() + data = [] + # write all fields needed in response + for field in fieldsToUse: + smallJson[field] = response[field] + # write all fields needed in card + for card in response['data']: + write_card = dict() + for field in fieldsInCard: + if field == 'name' and 'card_faces' in card: + write_card['name'] = card['card_faces'][0]['name'] + elif field == 'image_uris': + write_card['image_uris'] = write_image_uris(card['image_uris']) + elif field in card: + write_card[field] = card[field] + data.append(write_card) + smallJson['data'] = data + return smallJson + + +# only write images needed +def write_image_uris(card_image_uris): + image_uris = dict() + if 'normal' in card_image_uris: + image_uris['normal'] = card_image_uris['normal'] + elif 'large' in card_image_uris: + image_uris['normal'] = card_image_uris['large'] + elif 'small' in card_image_uris: + image_uris['normal'] = card_image_uris['small'] + if card_image_uris: + image_uris['art_crop'] = card_image_uris['art_crop'] + return image_uris + + +if __name__ == "__main__": + for category in allCategories: + print(category) + fetch_and_write_all(category, generate_initial_query(category)) diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html new file mode 100644 index 00000000..8ca9264c --- /dev/null +++ b/web/public/mtg/index.html @@ -0,0 +1,554 @@ +<!DOCTYPE html> +<html> + <head> + <!-- Google Tag Manager --> + <script> + ;(function (w, d, s, l, i) { + w[l] = w[l] || [] + w[l].push({ + 'gtm.start': new Date().getTime(), + event: 'gtm.js', + }) + var f = d.getElementsByTagName(s)[0], + j = d.createElement(s), + dl = l !== 'dataLayer' ? '&l=' + l : '' + j.async = true + j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl + f.parentNode.insertBefore(j, f) + })(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG') + </script> + <!-- End Google Tag Manager --> + <meta charset="UTF-8" /> + <script type="text/javascript" src="app.js"></script> + <style type="text/css"> + body { + position: relative; + } + + .play-page { + display: flex; + flex-direction: row-reverse; + font-family: Georgia, 'Times New Roman', Times, serif; + } + + h1 { + font-family: Verdana, Geneva, Tahoma, sans-serif; + text-align: center; + } + + form { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-right: 240px; + } + + .cards-container { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + } + + .card { + width: 230px; + height: 208px; + border: 5px solid lightgrey; + margin: 5px; + align-items: flex-end; + box-sizing: border-box; + border-radius: 11px; + position: relative; + display: flex; + justify-content: center; + /*background-size: contain;*/ + background-size: 220px; + background-repeat: no-repeat; + transition: height 1s, background-image 1s, border 0.4s 0.6s; + background-position-y: calc(50% - 20px); + } + + .card:not([data-name^='name'])::after { + content: ''; + height: 34px; + background: white; + width: 100%; + } + + .answer-page .card { + height: 350px; + /*padding-top: 310px;*/ + /*background-size: cover;*/ + overflow: hidden; + border-color: rgb(0, 146, 156); + } + + .answer-page .card.incorrect { + border-color: rgb(216, 27, 96); + } + + .names-bank { + position: fixed; + padding: 10px 10px 40px; + } + + .names-bank .name { + margin: 6px 0; + } + + .answer-page .names-bank .name { + display: none; + } + + .answer-page .names-bank .word-count { + display: none; + } + + .word-count { + text-align: center; + font-style: italic; + color: #444; + } + + .score { + width: 100%; + text-align: center; + background-color: rgb(255, 193, 7); + width: 200px; + font-family: Verdana, Geneva, Tahoma, sans-serif; + opacity: 0; + } + + .names-bank .score { + overflow: hidden; + height: 0; + } + + .answer-page .names-bank .score { + height: auto; + display: block; + opacity: 1; + transition: opacity 1.2s 0.2s; + padding: 20px; + } + + .name { + width: 230px; + min-height: 36px; + border-radius: 2px; + background-color: lightgrey; + padding: 8px 12px 2px; + box-sizing: border-box; + } + + .card .name { + border-radius: 0 0 5px 5px; + } + + #submit { + margin-top: 10px; + padding: 8px 20px; + background-color: cadetblue; + border: none; + border-radius: 3px; + font-size: 1.1em; + color: white; + cursor: pointer; + } + + #submit:hover { + background-color: rgb(0, 146, 156); + } + + #newGame { + padding: 8px 20px; + background-color: lightpink; + border: none; + position: absolute; + top: 5px; + left: 20px; + border-radius: 3px; + font-size: 0.7em; + cursor: pointer; + } + + #newGame:hover { + background-color: coral; + } + + .selected { + background-color: orange; + } + + @media screen and (orientation: landscape) and (max-height: 680px) { + /* CSS applied when the device is in landscape mode*/ + .names-bank { + padding: 0; + top: 0; + max-height: 100vh; + overflow: scroll; + } + + body { + font-size: 20px; + } + + .word-count { + font-size: 14px; + } + + h1 { + margin-right: 240px; + } + } + + @media screen and (orientation: portrait) and (max-width: 1100px) { + body { + font-size: 1.8em; + } + + .play-page { + flex-direction: column; + } + + .names-bank { + flex-direction: row; + display: flex; + flex-wrap: wrap; + /* position: fixed; */ + padding: 10px 10px 40px; + position: sticky; + top: 0; + z-index: 100; + background: white; + } + + .answer-page .names-bank { + min-width: 100%; + justify-content: center; + } + + form { + margin: 0; + } + + .names-bank .name { + margin: 6px; + } + + .names-bank .score { + width: 0; + } + + .answer-page .names-bank .score { + width: auto; + } + + .word-count { + position: absolute; + margin-top: -20px; + } + + .name { + width: 300px; + } + + .card { + width: 300px; + background-size: 300px; + height: 266px; + } + + .answer-page .card { + height: 454px; + } + } + </style> + </head> + <body> + <!-- Google Tag Manager (noscript) --> + <noscript> + <iframe + src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG" + height="0" + width="0" + style="display: none; visibility: hidden" + ></iframe> + </noscript> + <!-- End Google Tag Manager (noscript) --> + + <h1><span id="guess-type"></span>: <span id="round-number"></span></h1> + + <div class="play-page"> + <div + class="names-bank" + ondrop="returnDrop(event)" + ondragover="event.preventDefault()" + > + <div class="score"> + YOUR SCORE + <div>Correct Answers This Round: <span id="score-amount"></span></div> + <div> + Correct Answers In Total: <span id="score-amount-total"></span> + </div> + <div>Overall Percent: <span id="score-percent"></span>%</div> + </div> + <div class="word-count"><span id="words-left"></span></div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-1" + > + Name 1 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-2" + > + Name 2 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-3" + > + Name 3 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-4" + > + Name 4 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-5" + > + Name 5 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-6" + > + Name 6 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-7" + > + Name 7 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-8" + > + Name 8 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-9" + > + Name 9 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-10" + > + Name 10 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-11" + > + Name 11 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-12" + > + Name 12 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-13" + > + Name 13 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-14" + > + Name 14 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-15" + > + Name 15 + </div> + </div> + <form onsubmit="toggleMode(event)"> + <div class="cards-container"> + <div + class="card" + ondrop="drop(event,1)" + ondragover="allowDrop(event,1)" + onclick="dropSelected(event, 1)" + id="card-1" + ></div> + <div + class="card" + ondrop="drop(event,2)" + ondragover="allowDrop(event,2)" + onclick="dropSelected(event, 2)" + id="card-2" + ></div> + <div + class="card" + ondrop="drop(event,3)" + ondragover="allowDrop(event,3)" + onclick="dropSelected(event, 3)" + id="card-3" + ></div> + <div + class="card" + ondrop="drop(event,4)" + ondragover="allowDrop(event,4)" + onclick="dropSelected(event, 4)" + id="card-4" + ></div> + <div + class="card" + ondrop="drop(event,5)" + ondragover="allowDrop(event,5)" + onclick="dropSelected(event, 5)" + id="card-5" + ></div> + <div + class="card" + ondrop="drop(event, 6)" + ondragover="allowDrop(event,6)" + onclick="dropSelected(event,6)" + id="card-6" + ></div> + <div + class="card" + ondrop="drop(event,7)" + ondragover="allowDrop(event,7)" + onclick="dropSelected(event, 7)" + id="card-7" + ></div> + <div + class="card" + ondrop="drop(event,8)" + ondragover="allowDrop(event,8)" + onclick="dropSelected(event, 8)" + id="card-8" + ></div> + <div + class="card" + ondrop="drop(event,9)" + ondragover="allowDrop(event,9)" + onclick="dropSelected(event, 9)" + id="card-9" + ></div> + <div + class="card" + ondrop="drop(event,10)" + ondragover="allowDrop(event,10)" + onclick="dropSelected(event, 10)" + id="card-10" + ></div> + <div + class="card" + ondrop="drop(event,11)" + ondragover="allowDrop(event,11)" + onclick="dropSelected(event, 11)" + id="card-11" + ></div> + <div + class="card" + ondrop="drop(event,12)" + ondragover="allowDrop(event,12)" + onclick="dropSelected(event, 12)" + id="card-12" + ></div> + </div> + <input type="submit" id="submit" value="Submit" /> + </form> + </div> + + <div style="position: absolute; top: 0; left: 0; right: 0; color: grey"> + <form method="get" action="choose.html"> + <input type="submit" id="newGame" value="New Game" /> + </form> + </div> + <div style="margin: -40px 0 0; height: 60px"> + <a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a> + </div> + + <div + style=" + font-size: 0.9em; + position: absolute; + bottom: 0; + left: 0; + right: 0; + color: grey; + font-style: italic; + " + > + made by + <a + style="color: rgb(0, 146, 156); font-style: italic" + href="https://idamayer.com" + >Ida Mayer</a + > + & Alex Lien 2022 + </div> + </body> +</html> diff --git a/web/public/mtg/jsons/beast1.json b/web/public/mtg/jsons/beast1.json new file mode 100644 index 00000000..6a5b26c0 --- /dev/null +++ b/web/public/mtg/jsons/beast1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Adaptive Snapjaw", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aeromoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Affectionate Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alms Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alpha Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Barkripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Brushhopper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Murkdiver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Scavenger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Swarmsnapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquamoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquus Steed", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arborback Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arboreal Grazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Hybrid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Lancer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Arc-Slogger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Armguard Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ashen Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assault Zeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aura Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Avarax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Axebane Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Null", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Packhunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060"}, "reprint": false, "digital": true, "set_type": "starter"}, {"name": "Baloth Pup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Woodcrasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bannerhide Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Barbarian Outcast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barkhide Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Batterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battlefront Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bazaar Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beacon Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beasts of Bogardan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast Walkers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Berserk Murlodont", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blight-Breath Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blind Creeper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodstoke Howler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blossoming Bogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Bog Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bogstomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Bonethorn Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Book Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boot Nipper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brackish Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Branchsnap Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brontotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broodstar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Bull Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bumbling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Caller of the Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Canopy Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnassid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnivorous Moss-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cavern Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cavern Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cerodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chainflinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chambered Nautilus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chancellor of the Tangle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Charging Binox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Charging Slateback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cliffrunner Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Clockwork Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Coalhauler Swine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colossodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colos Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Copperhoof Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cosmic Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cragplate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crater Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Creature Guy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Crested Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryptic Annelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cultivator Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cystbearer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Darba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathbringer Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathmist Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deepwood Tantiv", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desert Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Displacer Kitten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Domri's Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dreg Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drekavac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dromad Purebred", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drooling Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Durkwood Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthshaking Si", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Elder Gargaroth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electryte", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Enormous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Erithizon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Symbiote", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ettercap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Excavating Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Fangren Firstborn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Hunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Marauder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Pathcutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Guardian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Sovereign", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Feral Hydra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Throwback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ferocious Zheng", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Ferrovore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fledgling Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Overseer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenetic Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenzied Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frondland Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fungal Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Furnace Scamp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fylamarid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Galvanoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Garruk's Companion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Gorehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Harbinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Garruk's Packleader", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast2.json b/web/public/mtg/jsons/beast2.json new file mode 100644 index 00000000..de0f2279 --- /dev/null +++ b/web/public/mtg/jsons/beast2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022"}, "flavor_name": "Anguirus, Armored Killer", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geyser Glider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Giant Warthog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gilded Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glade Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glademuse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Gloomshrieker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowering Rogon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowing Anemone", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gluetius Maximus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Gnarlid Colony", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gnarlid Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Godsire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goretusk Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Graf Mole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grave Sifter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Graxiplon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grazing Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Greater Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Great-Horn Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gristleback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Groffskithur", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grollub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Ragebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guardian Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gulf Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gurzigost", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Helium Squirter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Herald of the Forgotten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Herd Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Herd Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Highcliff Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130"}, "reprint": false, "digital": false, "set_type": "box"}, {"name": "Hollowhenge Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hundroog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunting Moa", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hydroid Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Indrik Stomphowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infernal Spawn of Evil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Infernius Spawnington III, Esq.", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Insatiable Souleater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Intrusive Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Iron-Barb Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ironclad Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jackalope Herd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kalonian Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Kalonian Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Karplusan Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Karstoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kazandu Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kelpie Guide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kezzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kiln Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kjeldoran Frostbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krakilin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kranioceros", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Krosan Cloudscraper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Groundshaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Vorine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Warchief", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kurgadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Grunt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Titan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Warrior", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Whelp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Landscaper Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lazotep Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Leery Fogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leopard-Spotted Jiao", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Lesser Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lexivore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Leyline Prowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Loathsome Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Longhorn Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lullmage's Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Battlement", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Satyr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurching Rotbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Macetail Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Makindi Sliderunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Manglehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Marauding Maulhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Marsh Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Megatherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Meglonoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Mischievous Quanar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mockery of Nature", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Slug", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mold Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mosscoat Goriak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Muck Drubb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murasa Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Murasa Rootgrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nalfeshnee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Naya Soulbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Needleshot Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Demolok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Game Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Noxious Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nucklavee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nullhide Ferox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nulltread Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nylea's Forerunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Obstinate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ondu Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oraxid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oxidda Scrapmelter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Paleoloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pallimud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Petradon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Petravark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phantom Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Phantom Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian Ingester", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Belcher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaguemaw Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Plated Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plated Seastrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaxmanta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Primal Huntbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Primoc Escapee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrid Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quagnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quartzwood Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quicksilver Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quillspike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Qumulox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Raging Kronch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakeclaw Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Rampaging Rendhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast3.json b/web/public/mtg/jsons/beast3.json new file mode 100644 index 00000000..3bf8f454 --- /dev/null +++ b/web/public/mtg/jsons/beast3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Ravenous Gigantotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Ravenous Leucrocota", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Razing Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Realm Razer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Relic Sloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Renegade Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Ridgeline Rager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridge Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgescale Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgetop Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Riptide Mangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "River Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Roaring Primadox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rock Badger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ronom Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Root Greevil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rotted Hystrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rumbling Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Sabertooth Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sagu Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctuary Smasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctum Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sandstorm Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Strangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saprazzan Breaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sawtusk Demolisher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Scalpelexis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scragnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Screeching Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sea Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Bruiser", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Plower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shivan Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Shore Snapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Siege Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Sigiled Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silt Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Six-y Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Skittish Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Cutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Ridgeback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slipstream Eel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Thragg", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snarling Undorak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snorting Gahr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spearbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Species Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiked Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spinal Villain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spinebiter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spined Basher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spirespine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Spitting Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sproutback Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spur Grappler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spurred Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Serow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Wildebeests", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stomper Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stonework Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratozeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swarm Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sylvan Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Symbiotic Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tangle Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tenement Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tephraderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terra Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Terra Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbound Primoc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Eater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Nibbler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Thrashing Mudspawn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thrashing Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thresher Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Thundering Tanadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Timbermaw Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanic Bulvox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818"}, "flavor_name": "Godzilla, Primeval Champion", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trapjaw Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Treespring Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trove Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Trusty Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tusked Colossodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undying Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Ursapine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vagrant Plowbeasts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valley Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Venomspout Brackus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vigilant Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Voracious Typhon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vulshok War Boar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "War Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wayward Guide-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Weaver of Lies", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wild Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woodland Bellower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Woodripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Razorback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Wormfang Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Manta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Newt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Turtle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrecking Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wretched Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yoked Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zhur-Taa Ancient", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/burn1.json b/web/public/mtg/jsons/burn1.json new file mode 100644 index 00000000..885fcb4e --- /dev/null +++ b/web/public/mtg/jsons/burn1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Angrath's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Annihilating Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae12fd10-c13e-4777-a233-96204ec75ac1.jpg?1562791532", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae12fd10-c13e-4777-a233-96204ec75ac1.jpg?1562791532"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arc Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d1c04fb-213f-4be1-9bba-94c737826bf8.jpg?1562910601", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d1c04fb-213f-4be1-9bba-94c737826bf8.jpg?1562910601"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arc Trail", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/445e3a0a-29a7-4dc0-80fe-569b9e751db3.jpg?1562816934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/445e3a0a-29a7-4dc0-80fe-569b9e751db3.jpg?1562816934"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arrow Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c57534fb-2591-4003-aeec-6452faa4a759.jpg?1562793262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c57534fb-2591-4003-aeec-6452faa4a759.jpg?1562793262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Artillerize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/034522ae-f531-44d9-b186-ada046ce0abc.jpg?1562875185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/034522ae-f531-44d9-b186-ada046ce0abc.jpg?1562875185"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Atarka's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/903d78c9-c5b3-45c3-a6d0-7e92b4196ae3.jpg?1562789860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/903d78c9-c5b3-45c3-a6d0-7e92b4196ae3.jpg?1562789860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Backlash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/dadf030d-5451-43fc-bf0c-c1629fdf88ec.jpg?1562938984", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/dadf030d-5451-43fc-bf0c-c1629fdf88ec.jpg?1562938984"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Banefire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b188c68a-e9df-4803-a722-1993dd88f833.jpg?1562803150", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b188c68a-e9df-4803-a722-1993dd88f833.jpg?1562803150"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barbed Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2509482a-68d8-4e94-9d1e-5b069ebdc2e4.jpg?1562635839", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2509482a-68d8-4e94-9d1e-5b069ebdc2e4.jpg?1562635839"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beacon of Destruction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0fae532-7189-450e-aa7f-e639163278fc.jpg?1562879532", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0fae532-7189-450e-aa7f-e639163278fc.jpg?1562879532"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blast from the Past", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5ca23782-80d3-4656-afba-f8440c813253.jpg?1562488402", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5ca23782-80d3-4656-afba-f8440c813253.jpg?1562488402"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "funny"}, {"name": "Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/26f8c6ab-ae62-4e2e-a5ba-2ec5bbe22445.jpg?1562234516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/26f8c6ab-ae62-4e2e-a5ba-2ec5bbe22445.jpg?1562234516"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8b6cfd3-4fb1-40a7-a090-de6f8b283cb3.jpg?1562257515", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8b6cfd3-4fb1-40a7-a090-de6f8b283cb3.jpg?1562257515"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/3940d0ca-0ca2-4446-9330-a554c3e89824.jpg?1562908488", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/3940d0ca-0ca2-4446-9330-a554c3e89824.jpg?1562908488"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f175c959-3b5d-46a3-9194-fad2359bbff9.jpg?1546740055", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f175c959-3b5d-46a3-9194-fad2359bbff9.jpg?1546740055"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Blazing Salvo", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7d192ef-a174-4df5-b67f-22918c32cf71.jpg?1562941547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7d192ef-a174-4df5-b67f-22918c32cf71.jpg?1562941547"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68ba5a86-ef90-45fd-bc7a-e870e91a207c.jpg?1592714653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68ba5a86-ef90-45fd-bc7a-e870e91a207c.jpg?1592714653"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Blightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c05e8a2-b7d0-4f24-b2ae-8e4db30e5842.jpg?1562702945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c05e8a2-b7d0-4f24-b2ae-8e4db30e5842.jpg?1562702945"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a1b4c07-588a-444f-9677-3eb1493b5394.jpg?1561757438", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a1b4c07-588a-444f-9677-3eb1493b5394.jpg?1561757438"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Blur of Blades", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b53f5b9b-d24b-4e9a-bc90-7ed198cd1132.jpg?1562811539", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b53f5b9b-d24b-4e9a-bc90-7ed198cd1132.jpg?1562811539"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bolt of Keranos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4df70b14-5d67-4a92-aaba-72480c621d10.jpg?1593092169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4df70b14-5d67-4a92-aaba-72480c621d10.jpg?1593092169"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bonfire of the Damned", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Book Burning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bead678c-7b6a-4668-9919-623312e08a65.jpg?1562631756", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bead678c-7b6a-4668-9919-623312e08a65.jpg?1562631756"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boros Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4ddf9cc-40a7-4b4f-bb51-b08171453c9a.jpg?1561848093", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4ddf9cc-40a7-4b4f-bb51-b08171453c9a.jpg?1561848093"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boros Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac8cd7a1-3f79-405b-8930-2206f32c2035.jpg?1622938151", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac8cd7a1-3f79-405b-8930-2206f32c2035.jpg?1622938151"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Breaking Point", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath of Darigaaz", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath of Malfegor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a12e4d0-8471-46ac-85e4-a2ea5be8bf8f.jpg?1562642287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a12e4d0-8471-46ac-85e4-a2ea5be8bf8f.jpg?1562642287"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath of Malfegor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5b3eb5c5-7ff8-4557-afe7-056ea5f09a49.jpg?1561757216", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5b3eb5c5-7ff8-4557-afe7-056ea5f09a49.jpg?1561757216"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Brimstone Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/6960f2da-6b84-4680-8ab2-f0567a5d1b0a.jpg?1562831550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/6960f2da-6b84-4680-8ab2-f0567a5d1b0a.jpg?1562831550"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Browbeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77c0eb52-8e09-471a-b00c-aaa1ae244afc.jpg?1592714628", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77c0eb52-8e09-471a-b00c-aaa1ae244afc.jpg?1592714628"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Browbeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74f20068-f225-4055-be7a-5c4a18e33b0b.jpg?1562630478", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74f20068-f225-4055-be7a-5c4a18e33b0b.jpg?1562630478"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Browbeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/1170ee2d-ab25-4c7f-a910-cc01471a2cab.jpg?1562639679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/1170ee2d-ab25-4c7f-a910-cc01471a2cab.jpg?1562639679"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Burn from Within", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8bdc165-4c6f-47e6-8bda-877c0be3613b.jpg?1576384673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8bdc165-4c6f-47e6-8bda-877c0be3613b.jpg?1576384673"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burning Fields", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dee12f01-581e-4a3c-a8b5-41bef2516781.jpg?1562257986", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dee12f01-581e-4a3c-a8b5-41bef2516781.jpg?1562257986"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Burn the Accursed", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ff4d4e6b-564d-46da-8e32-09ed08c8ddc5.jpg?1634350484", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ff4d4e6b-564d-46da-8e32-09ed08c8ddc5.jpg?1634350484"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burn the Impure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5641730-428d-4484-866e-ec1ac669537f.jpg?1562614054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5641730-428d-4484-866e-ec1ac669537f.jpg?1562614054"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burn Trail", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7f01f9a0-f1d0-4241-a270-df4ed673d1fd.jpg?1562832261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7f01f9a0-f1d0-4241-a270-df4ed673d1fd.jpg?1562832261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burst Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dc16614-5cf8-444d-a5ae-cac25018af68.jpg?1562610949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dc16614-5cf8-444d-a5ae-cac25018af68.jpg?1562610949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burst Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db539e3e-cefe-4f2c-bc8e-df049426895f.jpg?1561758208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db539e3e-cefe-4f2c-bc8e-df049426895f.jpg?1561758208"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Cackling Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a54a371e-fb82-41f1-892c-975f932b668e.jpg?1593273099", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a54a371e-fb82-41f1-892c-975f932b668e.jpg?1593273099"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Call In a Professional", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ead68c0a-eed1-4a9c-a790-56f8a79b444c.jpg?1649936108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ead68c0a-eed1-4a9c-a790-56f8a79b444c.jpg?1649936108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carbonize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4b4767b-edd1-4e36-b363-52114a9afe5e.jpg?1580014480", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4b4767b-edd1-4e36-b363-52114a9afe5e.jpg?1580014480"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Carbonize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f565fa1-a1a0-4dd0-b7f4-df65a807d156.jpg?1562530228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f565fa1-a1a0-4dd0-b7f4-df65a807d156.jpg?1562530228"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cave-In", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chain Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5883762-ca0a-4932-8d2a-41a45796a5f8.jpg?1562860651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5883762-ca0a-4932-8d2a-41a45796a5f8.jpg?1562860651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chain Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfb7fe8e-e348-4bf9-aa71-65f0675147e4.jpg?1636769610", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfb7fe8e-e348-4bf9-aa71-65f0675147e4.jpg?1636769610"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Chain Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14bd3d19-033e-41a7-8710-02b73ba0b4e4.jpg?1562899148", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14bd3d19-033e-41a7-8710-02b73ba0b4e4.jpg?1562899148"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Chain Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9ca05db2-ad92-4f4a-992d-b7f08f4f9c26.jpg?1562928035", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9ca05db2-ad92-4f4a-992d-b7f08f4f9c26.jpg?1562928035"}, "reprint": true, "digital": false, "set_type": "premium_deck"}, {"name": "Chain of Plasma", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f94aa774-9036-4016-8880-4bde2710cb90.jpg?1562954081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f94aa774-9036-4016-8880-4bde2710cb90.jpg?1562954081"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chandra's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Chandra's Outburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f1e849c3-f357-4e81-a580-be5056bed51b.jpg?1562745440", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f1e849c3-f357-4e81-a580-be5056bed51b.jpg?1562745440"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chandra's Outrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/3282db18-8564-418e-8c26-62e610b160f2.jpg?1562905547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/3282db18-8564-418e-8c26-62e610b160f2.jpg?1562905547"}, "reprint": false, "digital": false, "set_type": "archenemy"}, {"name": "Char", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ff3a24af-e995-4d05-ac2c-e9676048675d.jpg?1598915384", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ff3a24af-e995-4d05-ac2c-e9676048675d.jpg?1598915384"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Char", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3dc5f957-c1e4-452d-a78b-8d772ea0b940.jpg?1561756964", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3dc5f957-c1e4-452d-a78b-8d772ea0b940.jpg?1561756964"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Cinder Cloud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cinder Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e2d16c1-6226-438f-be1e-eaab3df687e1.jpg?1562875024", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e2d16c1-6226-438f-be1e-eaab3df687e1.jpg?1562875024"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Clan Defiance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa05298-9c94-4179-b75a-49ee2ca92920.jpg?1561851654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa05298-9c94-4179-b75a-49ee2ca92920.jpg?1561851654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cleansing Screech", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79928b26-fcac-4c3f-9edd-292769c2e56e.jpg?1562131561", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79928b26-fcac-4c3f-9edd-292769c2e56e.jpg?1562131561"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Collateral Damage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb738362-b0b4-4811-9fbf-5f45c852c822.jpg?1562831834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb738362-b0b4-4811-9fbf-5f45c852c822.jpg?1562831834"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Collective Defiance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/8960883f-3813-412b-9a5b-f8cf8d566fac.jpg?1576384546", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/8960883f-3813-412b-9a5b-f8cf8d566fac.jpg?1576384546"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Concussive Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41b68e85-a381-441d-aa18-491f9e202a10.jpg?1562610848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41b68e85-a381-441d-aa18-491f9e202a10.jpg?1562610848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cone of Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bec5e56a-5bab-4965-9035-128c3f1ae175.jpg?1562554444", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bec5e56a-5bab-4965-9035-128c3f1ae175.jpg?1562554444"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Cone of Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/5713f17a-9a57-41f8-b492-ced876e1a37f.jpg?1562800924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/5713f17a-9a57-41f8-b492-ced876e1a37f.jpg?1562800924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Consuming Sinkhole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/82a42b28-3d1b-4432-b8c9-2d42e4d0e1c5.jpg?1562921426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/82a42b28-3d1b-4432-b8c9-2d42e4d0e1c5.jpg?1562921426"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Crackling Doom", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f83c7d53-2599-42a9-ae96-a2699c5164cb.jpg?1562796251", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f83c7d53-2599-42a9-ae96-a2699c5164cb.jpg?1562796251"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crater's Claws", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95dde66b-b4a1-4a1e-8c9e-0bec4790b1e5.jpg?1562790652", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95dde66b-b4a1-4a1e-8c9e-0bec4790b1e5.jpg?1562790652"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Creative Outburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eab58d87-bf01-45dc-8958-e2b3375f914b.jpg?1627428357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eab58d87-bf01-45dc-8958-e2b3375f914b.jpg?1627428357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryoclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a892711-a1a4-4402-957f-92077d00320d.jpg?1593275219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a892711-a1a4-4402-957f-92077d00320d.jpg?1593275219"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Culmination of Studies", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/2483060e-9d3f-48ae-80ea-0119bf6b4d67.jpg?1627428427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/2483060e-9d3f-48ae-80ea-0119bf6b4d67.jpg?1627428427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cunning Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4991f81-3190-4d33-bf09-9d5387cbec11.jpg?1562830894", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4991f81-3190-4d33-bf09-9d5387cbec11.jpg?1562830894"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Darigaaz's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cf4c9d6a-86eb-45be-9405-473eb263b94c.jpg?1562938851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cf4c9d6a-86eb-45be-9405-473eb263b94c.jpg?1562938851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deal Damage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de905517-983d-4996-a680-3a5cf91bfe11.jpg?1562489827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de905517-983d-4996-a680-3a5cf91bfe11.jpg?1562489827"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Death Spark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba841b44-475c-402c-ac11-763de0cf27d9.jpg?1562770162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba841b44-475c-402c-ac11-763de0cf27d9.jpg?1562770162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deflecting Palm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32374918-1bcb-4516-96af-f27da752517e.jpg?1562784565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32374918-1bcb-4516-96af-f27da752517e.jpg?1562784565"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Demonfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af2ad333-722e-4d7e-972a-903c24068931.jpg?1593273111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af2ad333-722e-4d7e-972a-903c24068931.jpg?1593273111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Destructive Revelry", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc2eb53a-3d0f-4bb3-be36-f8024f2a1d4d.jpg?1592752246", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc2eb53a-3d0f-4bb3-be36-f8024f2a1d4d.jpg?1592752246"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Detonate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/237eedf5-8a8f-4668-a911-e2bf66f8221e.jpg?1562138293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/237eedf5-8a8f-4668-a911-e2bf66f8221e.jpg?1562138293"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Detonate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffd7eb90-ae95-49df-898a-9510187bce1c.jpg?1562949167", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffd7eb90-ae95-49df-898a-9510187bce1c.jpg?1562949167"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devastate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devil's Play", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c80596a4-b464-4b9e-8186-94a1c44838eb.jpg?1562836883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c80596a4-b464-4b9e-8186-94a1c44838eb.jpg?1562836883"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devil's Play", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6dd2f9e-16c2-4d25-98c4-0017ccd42228.jpg?1561758340", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6dd2f9e-16c2-4d25-98c4-0017ccd42228.jpg?1561758340"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Direct Current", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/166b0d75-824c-4c04-833b-7f7c69569a18.jpg?1572893128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/166b0d75-824c-4c04-833b-7f7c69569a18.jpg?1572893128"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disintegrate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/8712c49e-f171-4669-bed9-87575a37af11.jpg?1559591574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/8712c49e-f171-4669-bed9-87575a37af11.jpg?1559591574"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Disintegrate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/93ca09e6-2f23-4457-80ab-c7806112888b.jpg?1562546639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/93ca09e6-2f23-4457-80ab-c7806112888b.jpg?1562546639"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Double Deal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed8b3def-30ee-4dd2-9a25-ecf7d5663f96.jpg?1562799187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed8b3def-30ee-4dd2-9a25-ecf7d5663f96.jpg?1562799187"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Draconic Roar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cf5591c-46e3-4904-8b4e-4f1f84d3118f.jpg?1562787954", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cf5591c-46e3-4904-8b4e-4f1f84d3118f.jpg?1562787954"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dragon's Approach", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cb504a0-1dfb-49d0-84c3-7bd318d55481.jpg?1624591696", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cb504a0-1dfb-49d0-84c3-7bd318d55481.jpg?1624591696"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electrodominance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5c63877b-cdab-4ce4-a1c0-c088eb62a57a.jpg?1584830858", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5c63877b-cdab-4ce4-a1c0-c088eb62a57a.jpg?1584830858"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Electrostatic Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36ba1ac9-ebb9-449d-bd3b-716631b112fb.jpg?1645416206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36ba1ac9-ebb9-449d-bd3b-716631b112fb.jpg?1645416206"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Ember Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a9eb72b-9ae2-4b64-bbb9-187446b5fd2f.jpg?1562630295", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a9eb72b-9ae2-4b64-bbb9-187446b5fd2f.jpg?1562630295"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Energy Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/711f4cff-0256-44b2-a2fe-1cae6e9edb2b.jpg?1562719783", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/711f4cff-0256-44b2-a2fe-1cae6e9edb2b.jpg?1562719783"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Backlash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a98609dc-ea90-4c7e-a191-5e5d0ba16847.jpg?1562791298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a98609dc-ea90-4c7e-a191-5e5d0ba16847.jpg?1562791298"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eternal Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d646feea-3c20-4737-8d20-ffad42258ced.jpg?1562946085", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d646feea-3c20-4737-8d20-ffad42258ced.jpg?1562946085"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Exploding Borders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f247aaaf-4d65-4dfc-bab2-3c1331762647.jpg?1562804623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f247aaaf-4d65-4dfc-bab2-3c1331762647.jpg?1562804623"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Explosive Impact", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3a3e2b45-b086-4ffd-aa1a-1d03046e0d61.jpg?1562785002", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3a3e2b45-b086-4ffd-aa1a-1d03046e0d61.jpg?1562785002"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Explosive Singularity", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6cdd822-44a1-4d58-9de4-69fc56eae255.jpg?1654567601", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6cdd822-44a1-4d58-9de4-69fc56eae255.jpg?1654567601"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Explosive Singularity", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1d47e98-daae-42f7-9581-1269d57bd16e.jpg?1654570003", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1d47e98-daae-42f7-9581-1269d57bd16e.jpg?1654570003"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Explosive Welcome", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/122c01e6-38a6-456e-971e-9004df85ac1c.jpg?1624591777", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/122c01e6-38a6-456e-971e-9004df85ac1c.jpg?1624591777"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Exquisite Firecraft", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42eca98e-a164-4f70-a0b0-7a604863f30b.jpg?1562016890", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42eca98e-a164-4f70-a0b0-7a604863f30b.jpg?1562016890"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Exquisite Firecraft", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0be814f7-3c35-4b82-9fda-b8750a77cb9b.jpg?1562542837", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0be814f7-3c35-4b82-9fda-b8750a77cb9b.jpg?1562542837"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Face to Face", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64c93900-1af7-4c6b-a844-055bb7e27ddb.jpg?1562488406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64c93900-1af7-4c6b-a844-055bb7e27ddb.jpg?1562488406"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Fanning the Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79075361-e6ee-4cc9-990b-88fef27bbb1c.jpg?1562596865", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79075361-e6ee-4cc9-990b-88fef27bbb1c.jpg?1562596865"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Farideh's Fireball", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57a46987-f05a-4b83-af56-f18000874e65.jpg?1627706196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57a46987-f05a-4b83-af56-f18000874e65.jpg?1627706196"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fateful End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56455067-92c0-45b5-ac2e-525c35b41215.jpg?1581480134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56455067-92c0-45b5-ac2e-525c35b41215.jpg?1581480134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fault Line", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Fiery Gambit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a91376ed-5868-4887-8389-5ef5b9471786.jpg?1562153660", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a91376ed-5868-4887-8389-5ef5b9471786.jpg?1562153660"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Temper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61caf82d-e077-4931-a6ad-09fa7f04b36f.jpg?1576384730", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61caf82d-e077-4931-a6ad-09fa7f04b36f.jpg?1576384730"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Fiery Temper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/918e46b7-cbca-4acf-8e83-94b5fcadcc49.jpg?1562630935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/918e46b7-cbca-4acf-8e83-94b5fcadcc49.jpg?1562630935"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Temper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73493d43-7952-4202-818d-a1a05788af6f.jpg?1562636814", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73493d43-7952-4202-818d-a1a05788af6f.jpg?1562636814"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Fiery Temper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d377a7b9-5c25-4017-84a8-ae368eceba50.jpg?1561758137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d377a7b9-5c25-4017-84a8-ae368eceba50.jpg?1561758137"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Fire Ambush", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4dd8bdbd-99c9-4fa7-936a-acc7f4238507.jpg?1562256089", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4dd8bdbd-99c9-4fa7-936a-acc7f4238507.jpg?1562256089"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Fireblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44ab6601-409b-416f-a26c-b995e08fe6f3.jpg?1562908902", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44ab6601-409b-416f-a26c-b995e08fe6f3.jpg?1562908902"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Fireblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b1eb5b2c-1f02-48a6-a287-88eb189d6780.jpg?1562278616", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b1eb5b2c-1f02-48a6-a287-88eb189d6780.jpg?1562278616"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Firebolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90aae741-88af-4d21-a230-9a2592acdc87.jpg?1580014536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90aae741-88af-4d21-a230-9a2592acdc87.jpg?1580014536"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Firebolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5e45005-dd81-4d80-b043-02f719aca929.jpg?1562934963", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5e45005-dd81-4d80-b043-02f719aca929.jpg?1562934963"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Fires of Undeath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d94aaa4-c2fd-4714-9198-8415158b9c4d.jpg?1562920799", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d94aaa4-c2fd-4714-9198-8415158b9c4d.jpg?1562920799"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fire Tempest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "First Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6e5e360-ed47-40c1-8ad7-57645c2854ca.jpg?1562880074", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6e5e360-ed47-40c1-8ad7-57645c2854ca.jpg?1562880074"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flamebreak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64bbd438-7df2-4d7b-88ad-4531ebaf3931.jpg?1562913643", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64bbd438-7df2-4d7b-88ad-4531ebaf3931.jpg?1562913643"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Jab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/06c2b6b2-485e-41e6-b106-4f6f402e0ec3.jpg?1562896430", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/06c2b6b2-485e-41e6-b106-4f6f402e0ec3.jpg?1562896430"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Javelin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a567b570-81e4-4068-929c-9ce406fe7474.jpg?1562834196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a567b570-81e4-4068-929c-9ce406fe7474.jpg?1562834196"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Javelin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/407858c8-316d-47a7-8234-c490a0bc87a6.jpg?1561756991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/407858c8-316d-47a7-8234-c490a0bc87a6.jpg?1561756991"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Flame Jet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a511f9df-b53b-4fea-87cd-9f18f6833f92.jpg?1562444727", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a511f9df-b53b-4fea-87cd-9f18f6833f92.jpg?1562444727"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Lash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac44e3cb-cc69-4222-87bc-ffa54b7ab34a.jpg?1562741297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac44e3cb-cc69-4222-87bc-ffa54b7ab34a.jpg?1562741297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Rift", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e63ed449-d249-4639-85d2-f8fe75496d5c.jpg?1626100460", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e63ed449-d249-4639-85d2-f8fe75496d5c.jpg?1626100460"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Flame Rift", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/7717eeb9-c457-4a65-93a0-e91c7f6a1970.jpg?1562630580", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/7717eeb9-c457-4a65-93a0-e91c7f6a1970.jpg?1562630580"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flames of the Blood Hand", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a79701b4-d220-4c3e-b96c-7a77a22ba899.jpg?1651124386", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a79701b4-d220-4c3e-b96c-7a77a22ba899.jpg?1651124386"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Spill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b3090004-d7dd-47bc-92e5-977be4fd9ae5.jpg?1591227197", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b3090004-d7dd-47bc-92e5-977be4fd9ae5.jpg?1591227197"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flaming Gambit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb7fd9b7-c394-4ab3-b945-b4aab694eb6a.jpg?1562632851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb7fd9b7-c394-4ab3-b945-b4aab694eb6a.jpg?1562632851"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Flare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abc046c2-be9b-4f93-ac7d-e7dea6c4df9a.jpg?1562593271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abc046c2-be9b-4f93-ac7d-e7dea6c4df9a.jpg?1562593271"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Flare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1bd7755f-7ca5-4948-8baf-976823906891.jpg?1617148194", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1bd7755f-7ca5-4948-8baf-976823906891.jpg?1617148194"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Flare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5350236-7bd2-462d-9768-50087626c764.jpg?1562934818", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5350236-7bd2-462d-9768-50087626c764.jpg?1562934818"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Foundry Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c54b7c6-f94c-4349-8725-319c54240409.jpg?1626098329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c54b7c6-f94c-4349-8725-319c54240409.jpg?1626098329"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Friendly Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac4272ca-bb15-415c-a589-a472953a0dd9.jpg?1562828722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac4272ca-bb15-415c-a589-a472953a0dd9.jpg?1562828722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Galvanic Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5881bbc-8600-464d-9dcd-5a7780918d1d.jpg?1562825173", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5881bbc-8600-464d-9dcd-5a7780918d1d.jpg?1562825173"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geistblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19b1dfd9-b717-4c23-b8e5-a6ec835b278a.jpg?1576384765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19b1dfd9-b717-4c23-b8e5-a6ec835b278a.jpg?1576384765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geistflame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b856f31-ac80-4338-95a5-3f8acda74cfe.jpg?1562826976", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b856f31-ac80-4338-95a5-3f8acda74cfe.jpg?1562826976"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghitu Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/78827acd-a526-411b-bd22-ab9b538c75dd.jpg?1562919168", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/78827acd-a526-411b-bd22-ab9b538c75dd.jpg?1562919168"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghostfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a60475e5-0d37-4af0-b717-da4c8dea45ac.jpg?1562928542", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a60475e5-0d37-4af0-b717-da4c8dea45ac.jpg?1562928542"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Giant's Ire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/046fa2db-4c73-401a-b9a4-b039554be625.jpg?1562336735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/046fa2db-4c73-401a-b9a4-b039554be625.jpg?1562336735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glacial Ray", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/04c713fd-df47-4b35-bd37-ab65d853bdc8.jpg?1562271537", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/04c713fd-df47-4b35-bd37-ab65d853bdc8.jpg?1562271537"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Goblin Barrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/4849db5d-cd41-49f6-acd5-697cdc8263f6.jpg?1562735067", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/4849db5d-cd41-49f6-acd5-697cdc8263f6.jpg?1562735067"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goblin Grenade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/394cc2aa-0318-4ccd-a550-99a7eac933c3.jpg?1562639104", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/394cc2aa-0318-4ccd-a550-99a7eac933c3.jpg?1562639104"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Goblin Grenade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/8837eaba-9602-4f63-9897-85583fcdcf51.jpg?1562920228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/8837eaba-9602-4f63-9897-85583fcdcf51.jpg?1562920228"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goblin Grenade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dee262da-3002-4c08-8043-4e40e1b46822.jpg?1562936623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dee262da-3002-4c08-8043-4e40e1b46822.jpg?1562936623"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goblin Grenade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1befdfc7-a1e3-4a2a-ad68-7d0fee170f3f.jpg?1562900237", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1befdfc7-a1e3-4a2a-ad68-7d0fee170f3f.jpg?1562900237"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grapeshot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8cd49f85-7dbd-4cb6-b916-2adee29bb745.jpg?1561967853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8cd49f85-7dbd-4cb6-b916-2adee29bb745.jpg?1561967853"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Grapeshot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4ee33cb6-768e-44a0-b6f4-b8638aa84330.jpg?1562911525", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4ee33cb6-768e-44a0-b6f4-b8638aa84330.jpg?1562911525"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grapeshot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b99b45df-9602-4037-a695-09decb5f21d7.jpg?1623890288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b99b45df-9602-4037-a695-09decb5f21d7.jpg?1623890288"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Guerrilla Tactics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9bf3bac0-6e63-4bd3-bbd6-547f46c2d126.jpg?1562552299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9bf3bac0-6e63-4bd3-bbd6-547f46c2d126.jpg?1562552299"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Guerrilla Tactics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63535f0e-dc14-420e-bcb7-b5ef8fafb93f.jpg?1562915110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63535f0e-dc14-420e-bcb7-b5ef8fafb93f.jpg?1562915110"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Guerrilla Tactics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51811f2a-7002-4ba7-98d8-5b09d887975c.jpg?1562768705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51811f2a-7002-4ba7-98d8-5b09d887975c.jpg?1562768705"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guerrilla Tactics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c005ca3-0508-4ac2-afec-3d4a27334c31.jpg?1562768254", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c005ca3-0508-4ac2-afec-3d4a27334c31.jpg?1562768254"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gut Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a54a2a30-b96a-49c7-9151-1f4b0d4a4413.jpg?1562880417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a54a2a30-b96a-49c7-9151-1f4b0d4a4413.jpg?1562880417"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hammer of Bogardan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7285f52-5df0-4f90-9cf7-a57295d90fd4.jpg?1562722857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7285f52-5df0-4f90-9cf7-a57295d90fd4.jpg?1562722857"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hanabi Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/881fecf4-8c14-4614-84bd-c1a3dcdbb5ff.jpg?1562762458", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/881fecf4-8c14-4614-84bd-c1a3dcdbb5ff.jpg?1562762458"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Heartfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7db219ea-2ed1-4a86-955c-d61ecedbc019.jpg?1557576716", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7db219ea-2ed1-4a86-955c-d61ecedbc019.jpg?1557576716"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hidetsugu's Second Rite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e48eb77-3bd7-444a-9262-799cc706c05a.jpg?1562493025", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e48eb77-3bd7-444a-9262-799cc706c05a.jpg?1562493025"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hungry Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4ca23676-f36f-4266-ba4f-5e9ebf3adb57.jpg?1592419490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4ca23676-f36f-4266-ba4f-5e9ebf3adb57.jpg?1592419490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Igneous Inspiration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/5781ad7b-dc1b-4cc1-9e72-6e714b9ba1de.jpg?1624591976", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/5781ad7b-dc1b-4cc1-9e72-6e714b9ba1de.jpg?1624591976"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Illuminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ceef2761-7301-42de-8f54-49b8cd1e457b.jpg?1562943833", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ceef2761-7301-42de-8f54-49b8cd1e457b.jpg?1562943833"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Improvised Weaponry", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29d5fd00-c616-4079-a91e-4da0bcaf9120.jpg?1627706453", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29d5fd00-c616-4079-a91e-4da0bcaf9120.jpg?1627706453"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incendiary Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incendiary Flow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cf464f61-8a7f-493b-a80f-2f2b0ebd8bf6.jpg?1576384613", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cf464f61-8a7f-493b-a80f-2f2b0ebd8bf6.jpg?1576384613"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/burn2.json b/web/public/mtg/jsons/burn2.json new file mode 100644 index 00000000..c1116e11 --- /dev/null +++ b/web/public/mtg/jsons/burn2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Incendiary Flow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae93a313-c265-435f-b745-7b7a7ed6208e.jpg?1562636873", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae93a313-c265-435f-b745-7b7a7ed6208e.jpg?1562636873"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/8503210d-be78-4271-a050-53caa94f735d.jpg?1562844302", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/8503210d-be78-4271-a050-53caa94f735d.jpg?1562844302"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/723fb62e-735a-4ca6-9d38-f1c3944fe69a.jpg?1562549678", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/723fb62e-735a-4ca6-9d38-f1c3944fe69a.jpg?1562549678"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa0f7e1f-bcb5-414f-a2e9-6a158fec2ff5.jpg?1562593262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa0f7e1f-bcb5-414f-a2e9-6a158fec2ff5.jpg?1562593262"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/409b2be8-5bb6-45e0-ab87-ca73b4e3a396.jpg?1562718795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/409b2be8-5bb6-45e0-ab87-ca73b4e3a396.jpg?1562718795"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c3f00af-010d-4485-b8b7-47400d99c496.jpg?1562924091", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c3f00af-010d-4485-b8b7-47400d99c496.jpg?1562924091"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incinerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28b0495d-0c3f-4491-8331-4cbabbd6eac5.jpg?1561756819", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28b0495d-0c3f-4491-8331-4cbabbd6eac5.jpg?1561756819"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Inescapable Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46651efd-0906-4350-a1b8-52e3f8aff45d.jpg?1572893201", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46651efd-0906-4350-a1b8-52e3f8aff45d.jpg?1572893201"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Inferno Jet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c6a43fe-369d-4943-a825-570eb3cceba4.jpg?1562788752", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c6a43fe-369d-4943-a825-570eb3cceba4.jpg?1562788752"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inspired Ultimatum", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd64f064-8f05-41ef-b95b-1b723137f846.jpg?1591228071", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd64f064-8f05-41ef-b95b-1b723137f846.jpg?1591228071"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Invoke the Firemind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58d8e41a-5990-4ceb-9d41-76632faa7883.jpg?1593272700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58d8e41a-5990-4ceb-9d41-76632faa7883.jpg?1593272700"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ionize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f161f7d2-eaa1-4931-93f9-befa8b5df821.jpg?1572893679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f161f7d2-eaa1-4931-93f9-befa8b5df821.jpg?1572893679"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Izzet Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61289196-a56b-4d24-b340-9cf067c77f45.jpg?1592713417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61289196-a56b-4d24-b340-9cf067c77f45.jpg?1592713417"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Izzet Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8e84a97-8e40-42fa-a114-df90e820ede6.jpg?1562497263", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8e84a97-8e40-42fa-a114-df90e820ede6.jpg?1562497263"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Jeskai Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca268705-ef04-4bf1-8a5d-866bb3e5bb61.jpg?1562793488", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca268705-ef04-4bf1-8a5d-866bb3e5bb61.jpg?1562793488"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaervek's Purge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaervek's Torch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0a1624ab-e50e-48a3-acf7-457069914616.jpg?1562717831", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0a1624ab-e50e-48a3-acf7-457069914616.jpg?1562717831"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaleidoscorch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a5f07603-fd79-437a-9b12-495fc5a39b68.jpg?1626096801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a5f07603-fd79-437a-9b12-495fc5a39b68.jpg?1626096801"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Kamahl's Sledge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/38c55518-7bdf-4a42-ae30-cd6525557a59.jpg?1562629270", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/38c55518-7bdf-4a42-ae30-cd6525557a59.jpg?1562629270"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kami's Flare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bef5d58e-b490-4682-9a44-12cd61a94c0f.jpg?1654567705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bef5d58e-b490-4682-9a44-12cd61a94c0f.jpg?1654567705"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kindle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/930745eb-b038-4b55-97f3-bf8d99b54d32.jpg?1562055431", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/930745eb-b038-4b55-97f3-bf8d99b54d32.jpg?1562055431"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kolaghan's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c884e1e-fecb-4330-b3de-5fc2a60f7173.jpg?1562788780", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c884e1e-fecb-4330-b3de-5fc2a60f7173.jpg?1562788780"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kolaghan's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e8bdd10-0bdc-4339-bd84-b540606438d6.jpg?1656000872", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e8bdd10-0bdc-4339-bd84-b540606438d6.jpg?1656000872"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Lash Out", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de2c0c8b-5442-44fb-9686-d3dff5742501.jpg?1562371092", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de2c0c8b-5442-44fb-9686-d3dff5742501.jpg?1562371092"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Axe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/807e5102-1fab-4ff4-aad8-94defbbb8a6b.jpg?1562241656", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/807e5102-1fab-4ff4-aad8-94defbbb8a6b.jpg?1562241656"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Lava Axe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e11ec278-46f5-4970-ad0b-f6718c73de6c.jpg?1562864233", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e11ec278-46f5-4970-ad0b-f6718c73de6c.jpg?1562864233"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Lava Axe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe6cff90-ecec-4610-82ea-0f2a109959cf.jpg?1562955255", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe6cff90-ecec-4610-82ea-0f2a109959cf.jpg?1562955255"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Lava Axe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2bebbad-76aa-4388-891a-583e8af9509d.jpg?1562448334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2bebbad-76aa-4388-891a-583e8af9509d.jpg?1562448334"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Lava Blister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd0e9e53-2710-4c2a-a8e4-48f25375ebc7.jpg?1562933365", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd0e9e53-2710-4c2a-a8e4-48f25375ebc7.jpg?1562933365"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79dc0e20-5790-4927-8432-cf0e9b7381d4.jpg?1562917534", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79dc0e20-5790-4927-8432-cf0e9b7381d4.jpg?1562917534"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Dart", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b16dd041-451d-4914-8c46-aa315a90d802.jpg?1562201890", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b16dd041-451d-4914-8c46-aa315a90d802.jpg?1562201890"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Lava Dart", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/865bb1d3-5b7d-40e9-87cc-96be9524a105.jpg?1562630775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/865bb1d3-5b7d-40e9-87cc-96be9524a105.jpg?1562630775"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Lavalanche", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Spike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79c21c1f-eaa4-454d-a1c7-b41466d0a428.jpg?1547517298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79c21c1f-eaa4-454d-a1c7-b41466d0a428.jpg?1547517298"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Lava Spike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83e3c502-9e3c-41db-806c-538243dc0453.jpg?1562241728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83e3c502-9e3c-41db-806c-538243dc0453.jpg?1562241728"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Lightning Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63fec3f9-d399-48e6-84b6-c8410c24c382.jpg?1562054251", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63fec3f9-d399-48e6-84b6-c8410c24c382.jpg?1562054251"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae5f9fb1-5a55-4db3-98a1-2628e3598c18.jpg?1648155765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae5f9fb1-5a55-4db3-98a1-2628e3598c18.jpg?1648155765"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435589bb-27c6-4a6d-9d63-394d5092b9d8.jpg?1561978182", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435589bb-27c6-4a6d-9d63-394d5092b9d8.jpg?1561978182"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d573ef03-4730-45aa-93dd-e45ac1dbaf4a.jpg?1559591645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d573ef03-4730-45aa-93dd-e45ac1dbaf4a.jpg?1559591645"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8c8390f-4072-454f-8dc4-174919187a47.jpg?1655641560", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8c8390f-4072-454f-8dc4-174919187a47.jpg?1655641560"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c69f668b-cf28-495a-bbe1-24e9d0089fa1.jpg?1648155788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c69f668b-cf28-495a-bbe1-24e9d0089fa1.jpg?1648155788"}, "reprint": true, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27740ea5-79c8-420f-bc49-6d5eac58dac5.jpg?1657119952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27740ea5-79c8-420f-bc49-6d5eac58dac5.jpg?1657119952"}, "flavor_name": "Hadoken", "reprint": true, "digital": false, "set_type": "box"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4eaac4fd-95f5-4f38-b593-0101e79a20f9.jpg?1623945607", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4eaac4fd-95f5-4f38-b593-0101e79a20f9.jpg?1623945607"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45184cd7-b037-4a85-a063-e622ca928d17.jpg?1599352446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45184cd7-b037-4a85-a063-e622ca928d17.jpg?1599352446"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6fb94c1b-8002-4d79-add0-c4dfef9019ee.jpg?1599352358", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6fb94c1b-8002-4d79-add0-c4dfef9019ee.jpg?1599352358"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6ab06973-6440-4b12-8947-8c412500fa41.jpg?1599352361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6ab06973-6440-4b12-8947-8c412500fa41.jpg?1599352361"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3eb3895-b64c-46ab-b704-3c46963920ba.jpg?1599352414", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3eb3895-b64c-46ab-b704-3c46963920ba.jpg?1599352414"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ff204024-20a5-4bb9-82b6-f6b4337efd60.jpg?1552226335", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ff204024-20a5-4bb9-82b6-f6b4337efd60.jpg?1552226335"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Lightning Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28708c8c-4336-4d04-b43a-59a31471a9f6.jpg?1561756817", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28708c8c-4336-4d04-b43a-59a31471a9f6.jpg?1561756817"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Lightning Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/613789fe-fac1-4200-b0a1-c84d1fa27cff.jpg?1562917870", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/613789fe-fac1-4200-b0a1-c84d1fa27cff.jpg?1562917870"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Lightning Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b2ecf55-c1cc-4b28-b7ce-e1b25305155e.jpg?1598917140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b2ecf55-c1cc-4b28-b7ce-e1b25305155e.jpg?1598917140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/227ac87a-7196-40d0-ab00-98ebafcca09a.jpg?1624065725", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/227ac87a-7196-40d0-ab00-98ebafcca09a.jpg?1624065725"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Lightning Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4ec9e67b-1b4e-4e4e-9758-be697d308f16.jpg?1561757108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4ec9e67b-1b4e-4e4e-9758-be697d308f16.jpg?1561757108"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Lightning Javelin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1ccaeed-9670-4432-8a45-d5c06119fa9f.jpg?1562040115", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1ccaeed-9670-4432-8a45-d5c06119fa9f.jpg?1562040115"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Lightning Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c9c0388e-a04c-4757-a06d-8e8046f5a783.jpg?1593275279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c9c0388e-a04c-4757-a06d-8e8046f5a783.jpg?1593275279"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0f55dee-7e39-4183-8e74-844d9c299bf5.jpg?1562566447", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0f55dee-7e39-4183-8e74-844d9c299bf5.jpg?1562566447"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Lightning Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbb03f2e-2b92-4aa1-afae-301ed5d151d3.jpg?1562827848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbb03f2e-2b92-4aa1-afae-301ed5d151d3.jpg?1562827848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Surge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/0452d78d-eafc-4ccb-a478-d1f46bcefffe.jpg?1562628459", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/0452d78d-eafc-4ccb-a478-d1f46bcefffe.jpg?1562628459"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Light Up the Night", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7de68154-3b82-4a94-98a6-cfc49d359e4e.jpg?1636223152", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7de68154-3b82-4a94-98a6-cfc49d359e4e.jpg?1636223152"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lorehold Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4f0885f-1049-4a19-853d-f4e6d4bec29e.jpg?1627429447", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4f0885f-1049-4a19-853d-f4e6d4bec29e.jpg?1627429447"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lunge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9e43349-429c-43f7-b808-c4bf37370a9f.jpg?1562383530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9e43349-429c-43f7-b808-c4bf37370a9f.jpg?1562383530"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Magma Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9752bc3-0bdf-4657-8750-73c8cbc8e83f.jpg?1562940942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9752bc3-0bdf-4657-8750-73c8cbc8e83f.jpg?1562940942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Magma Jet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8af1c5b0-973d-467e-a797-51ca75c183c1.jpg?1593813497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8af1c5b0-973d-467e-a797-51ca75c183c1.jpg?1593813497"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Magma Jet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51ea1728-08aa-4553-90b2-919c70712ed5.jpg?1562877009", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51ea1728-08aa-4553-90b2-919c70712ed5.jpg?1562877009"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Magma Jet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/be7552ac-4546-492d-8d11-d6678a04b9c3.jpg?1562640021", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/be7552ac-4546-492d-8d11-d6678a04b9c3.jpg?1562640021"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Make Mischief", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2049072-5901-4edd-8305-ce55f256bca5.jpg?1576384624", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2049072-5901-4edd-8305-ce55f256bca5.jpg?1576384624"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Melt Terrain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d94a1d1-6d24-46e1-9568-42e1a810ad31.jpg?1562815251", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d94a1d1-6d24-46e1-9568-42e1a810ad31.jpg?1562815251"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mindblaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59418766-5567-4ec4-af1f-1cb2db2958d0.jpg?1562760146", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59418766-5567-4ec4-af1f-1cb2db2958d0.jpg?1562760146"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mindswipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/557e8303-a021-4257-b41a-7d25f04618c8.jpg?1562786781", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/557e8303-a021-4257-b41a-7d25f04618c8.jpg?1562786781"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Misfortune", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b14cc32a-eb4f-4690-aceb-160780743ebe.jpg?1562770145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b14cc32a-eb4f-4690-aceb-160780743ebe.jpg?1562770145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molten Disaster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molten Influence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c2b326b-d177-4a03-a0a3-fe2c2d4af272.jpg?1562908953", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c2b326b-d177-4a03-a0a3-fe2c2d4af272.jpg?1562908953"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molten Rain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ecdd414b-3d9d-4347-acce-289209d09fc4.jpg?1593813519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ecdd414b-3d9d-4347-acce-289209d09fc4.jpg?1593813519"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Molten Rain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f888b4d4-31f9-4322-8225-4d7e7a9f4dd5.jpg?1562163535", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f888b4d4-31f9-4322-8225-4d7e7a9f4dd5.jpg?1562163535"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Moonrager's Slash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb4f266b-c41c-4047-ae6f-b2226c7459e8.jpg?1636223196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb4f266b-c41c-4047-ae6f-b2226c7459e8.jpg?1636223196"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Needle Drop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3f89bcf-46f8-4598-a949-7f10134606aa.jpg?1562369628", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3f89bcf-46f8-4598-a949-7f10134606aa.jpg?1562369628"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Neonate's Rush", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dee17e12-e08f-4449-9f49-05f20e0d1670.jpg?1636223275", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dee17e12-e08f-4449-9f49-05f20e0d1670.jpg?1636223275"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nerf War", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2eb08fc5-29a4-4911-ac94-dc5ff2fc2ace.jpg?1561756860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2eb08fc5-29a4-4911-ac94-dc5ff2fc2ace.jpg?1561756860"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Open Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/448f9fb5-ffb5-4325-9f81-ce8782e5f9e9.jpg?1562797508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/448f9fb5-ffb5-4325-9f81-ce8782e5f9e9.jpg?1562797508"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Orcish Cannonade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0afae574-aa96-4500-9882-a4b10337b6f5.jpg?1619397326", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0afae574-aa96-4500-9882-a4b10337b6f5.jpg?1619397326"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Orcish Cannonade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e40f99c-9608-4463-8c6f-c6e142f0d716.jpg?1562911399", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e40f99c-9608-4463-8c6f-c6e142f0d716.jpg?1562911399"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parallectric Feedback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/891f1d29-377a-4f71-917f-ff10e785caee.jpg?1593272344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/891f1d29-377a-4f71-917f-ff10e785caee.jpg?1593272344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3ab8065-cecc-4b19-be93-7cf791a93e62.jpg?1562864229", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3ab8065-cecc-4b19-be93-7cf791a93e62.jpg?1562864229"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pass the Torch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5dc05455-4ebd-46f8-94cf-14f0d5420037.jpg?1654043945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5dc05455-4ebd-46f8-94cf-14f0d5420037.jpg?1654043945"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Peak Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed0a00f7-aee0-4ab2-bab6-bc0949176a7a.jpg?1562836713", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed0a00f7-aee0-4ab2-bab6-bc0949176a7a.jpg?1562836713"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pigment Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d285a7a1-bb7e-4a78-a49f-c2add62b829a.jpg?1624592111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d285a7a1-bb7e-4a78-a49f-c2add62b829a.jpg?1624592111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pillar of Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c983e879-d9d2-47cc-9958-506711ca80cd.jpg?1592709165", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c983e879-d9d2-47cc-9958-506711ca80cd.jpg?1592709165"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pillar of Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c39677b8-9a43-4e62-a83a-4a9d6372310b.jpg?1562640029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c39677b8-9a43-4e62-a83a-4a9d6372310b.jpg?1562640029"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Play with Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42901bec-a8d0-46a3-a710-bfb7bd87f155.jpg?1640721639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42901bec-a8d0-46a3-a710-bfb7bd87f155.jpg?1640721639"}, "reprint": false, "frame_effects": ["inverted"], "digital": false, "set_type": "expansion"}, {"name": "Poison the Well", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb86eeec-d50f-4823-86bd-35437926a6e4.jpg?1562835997", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb86eeec-d50f-4823-86bd-35437926a6e4.jpg?1562835997"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Precision Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a59b4e5b-e9e0-4507-b9e7-8fba7e3a54f9.jpg?1572894271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a59b4e5b-e9e0-4507-b9e7-8fba7e3a54f9.jpg?1572894271"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prismari Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/866b7fd4-86e3-4b42-b1ea-33bad0db1f9f.jpg?1627429955", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/866b7fd4-86e3-4b42-b1ea-33bad0db1f9f.jpg?1627429955"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prophetic Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/101163c0-cd2f-4e1a-84b3-f64fc748807d.jpg?1592713462", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/101163c0-cd2f-4e1a-84b3-f64fc748807d.jpg?1592713462"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Prophetic Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79f74291-c452-4a60-bf5f-73efad6583d4.jpg?1562923762", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79f74291-c452-4a60-bf5f-73efad6583d4.jpg?1562923762"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Provoke the Trolls", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/2727b05a-0c86-4c59-b7b4-425bdd8e775d.jpg?1631049503", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/2727b05a-0c86-4c59-b7b4-425bdd8e775d.jpg?1631049503"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pulse of the Forge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea3ed9c8-b552-4a9a-b77a-8b148638b4f0.jpg?1562640294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea3ed9c8-b552-4a9a-b77a-8b148638b4f0.jpg?1562640294"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Puncture Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bf90b4d-98cf-4953-b6ae-c41d21ab559b.jpg?1562907480", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bf90b4d-98cf-4953-b6ae-c41d21ab559b.jpg?1562907480"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Punishing Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0da4409b-fe3f-4500-bf4b-890593f7d313.jpg?1562897775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0da4409b-fe3f-4500-bf4b-890593f7d313.jpg?1562897775"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Punishing Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56e76f1c-5a07-455a-a3df-4c45b5b25b82.jpg?1562612350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56e76f1c-5a07-455a-a3df-4c45b5b25b82.jpg?1562612350"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Punish the Enemy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/4179a72b-8482-46ec-9815-f5d6d94b5aa5.jpg?1562907014", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/4179a72b-8482-46ec-9815-f5d6d94b5aa5.jpg?1562907014"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyromatics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c22c9dab-e8d5-48b3-8fd2-9f4138ee0c7c.jpg?1593272350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c22c9dab-e8d5-48b3-8fd2-9f4138ee0c7c.jpg?1593272350"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quenchable Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee1c0ded-2a80-4ed4-b9fc-3a18bf5c3239.jpg?1562804519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee1c0ded-2a80-4ed4-b9fc-3a18bf5c3239.jpg?1562804519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rain of Embers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakdos Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0fcd4394-d22d-4eec-ad73-ffaf10ad60de.jpg?1562782720", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0fcd4394-d22d-4eec-ad73-ffaf10ad60de.jpg?1562782720"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakdos's Return", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d72981c0-1632-4d64-9341-2a76047d9b36.jpg?1562793869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d72981c0-1632-4d64-9341-2a76047d9b36.jpg?1562793869"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ral's Outburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6be3dd3e-50d2-4729-9caa-b2cd984f4c97.jpg?1557577237", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6be3dd3e-50d2-4729-9caa-b2cd984f4c97.jpg?1557577237"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravaging Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d9404b2-f0ea-4a31-bc7b-6748574c57d3.jpg?1562021972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d9404b2-f0ea-4a31-bc7b-6748574c57d3.jpg?1562021972"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Reality Hemorrhage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c044168d-cb08-493d-98c1-b66b6149fe5a.jpg?1562933647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c044168d-cb08-493d-98c1-b66b6149fe5a.jpg?1562933647"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Reckless Abandon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f335d43-cacb-40ad-93c1-9a861e9f66c7.jpg?1562444699", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f335d43-cacb-40ad-93c1-9a861e9f66c7.jpg?1562444699"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Red Sun's Zenith", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/373eb109-0e30-41c1-b2df-6bc78d968890.jpg?1562610602", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/373eb109-0e30-41c1-b2df-6bc78d968890.jpg?1562610602"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rekindled Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/131c6377-4ed4-4a76-a9cb-be7ad17d76fd.jpg?1562899037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/131c6377-4ed4-4a76-a9cb-be7ad17d76fd.jpg?1562899037"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Release the Ants", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b6f1afb-2451-4611-ac3e-3513a4651719.jpg?1562877157", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b6f1afb-2451-4611-ac3e-3513a4651719.jpg?1562877157"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rending Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51332c31-41df-4379-aa63-6a734a4df618.jpg?1643591905", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51332c31-41df-4379-aa63-6a734a4df618.jpg?1643591905"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Repeating Barrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba90a2d6-8292-4ff1-91d0-b30ae9775f12.jpg?1562562987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba90a2d6-8292-4ff1-91d0-b30ae9775f12.jpg?1562562987"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Resounding Thunder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/680b7955-d939-4195-aba8-b46a8c925616.jpg?1562704894", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/680b7955-d939-4195-aba8-b46a8c925616.jpg?1562704894"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhystic Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21ce365e-3002-42e9-aeb5-1b845408271e.jpg?1562901251", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21ce365e-3002-42e9-aeb5-1b845408271e.jpg?1562901251"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rift Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88dde96e-6824-4d26-9fb5-86b9f3c50959.jpg?1562923901", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88dde96e-6824-4d26-9fb5-86b9f3c50959.jpg?1562923901"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rift Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25176fe7-a5a0-44d2-9619-193063c55902.jpg?1561929531", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25176fe7-a5a0-44d2-9619-193063c55902.jpg?1561929531"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Risk Factor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4eda89d9-9bd1-4a55-ac02-f9a0625d8e5b.jpg?1572893240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4eda89d9-9bd1-4a55-ac02-f9a0625d8e5b.jpg?1572893240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Roil Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86572747-8faa-4242-b059-07d11e6be1cd.jpg?1604197631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86572747-8faa-4242-b059-07d11e6be1cd.jpg?1604197631"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Roiling Terrain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87d3a425-01d1-4001-92f9-8e297dd862b7.jpg?1562291349", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87d3a425-01d1-4001-92f9-8e297dd862b7.jpg?1562291349"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rolling Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Sacred Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0b1dcd7-6dd9-4134-bc6c-9dc7754006a2.jpg?1636684880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0b1dcd7-6dd9-4134-bc6c-9dc7754006a2.jpg?1636684880"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sarkhan's Catharsis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f4b6f26-c66b-4048-9503-af0a886ef14f.jpg?1557576811", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f4b6f26-c66b-4048-9503-af0a886ef14f.jpg?1557576811"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sarkhan's Dragonfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b96a7320-089a-4a7e-813f-49ca1620df76.jpg?1562303870", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b96a7320-089a-4a7e-813f-49ca1620df76.jpg?1562303870"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Sarkhan's Rage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/4787924f-3186-4e18-b53c-dd67c5f42220.jpg?1562785591", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/4787924f-3186-4e18-b53c-dd67c5f42220.jpg?1562785591"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saut\u00e9", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85cbebbb-7ea4-4140-933f-186cad08697d.jpg?1562488867", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85cbebbb-7ea4-4140-933f-186cad08697d.jpg?1562488867"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Scent of Cinder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c030eca0-bc5f-403b-8600-1f295fc85fee.jpg?1562445189", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c030eca0-bc5f-403b-8600-1f295fc85fee.jpg?1562445189"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scent of Cinder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d083fcdc-1e1f-4ad3-82d1-11f0b84dd74d.jpg?1562934001", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d083fcdc-1e1f-4ad3-82d1-11f0b84dd74d.jpg?1562934001"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Scorching Lava", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a85437f-052e-494c-a9ee-265c4624a409.jpg?1562903659", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a85437f-052e-494c-a9ee-265c4624a409.jpg?1562903659"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scorching Missile", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/0672960b-4cb5-4ed6-ba3c-6b97290e0330.jpg?1562896294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0672960b-4cb5-4ed6-ba3c-6b97290e0330.jpg?1562896294"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Scorching Spear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e4817bd-68e8-4a85-983a-ee6dda2bbf33.jpg?1562447352", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e4817bd-68e8-4a85-983a-ee6dda2bbf33.jpg?1562447352"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Searing Barrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2f11135-e9ce-4e4c-bea7-72a46d326e40.jpg?1572490453", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2f11135-e9ce-4e4c-bea7-72a46d326e40.jpg?1572490453"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/301f13dd-39b8-4a93-9c05-3dc4fa1f1c75.jpg?1562284687", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/301f13dd-39b8-4a93-9c05-3dc4fa1f1c75.jpg?1562284687"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/974c599b-170e-4948-b741-47f61c769b6e.jpg?1561757630", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/974c599b-170e-4948-b741-47f61c769b6e.jpg?1561757630"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Searing Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb43fd07-d281-447d-88bf-c53498c2cf20.jpg?1593092367", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb43fd07-d281-447d-88bf-c53498c2cf20.jpg?1593092367"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Flesh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d83db110-42e7-4823-a686-b83205faf503.jpg?1562946564", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d83db110-42e7-4823-a686-b83205faf503.jpg?1562946564"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Spear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/11a94b7c-0216-473c-87a6-71e5a64d7799.jpg?1562550529", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/11a94b7c-0216-473c-87a6-71e5a64d7799.jpg?1562550529"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Searing Spear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e574f1f8-ca61-43b4-8230-2636709a3855.jpg?1562497262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e574f1f8-ca61-43b4-8230-2636709a3855.jpg?1562497262"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Searing Touch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9091667-d5a8-4978-9023-032ff65f9642.jpg?1562057345", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9091667-d5a8-4978-9023-032ff65f9642.jpg?1562057345"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Wind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b761f97-3690-497a-b6ab-c71f61b8e841.jpg?1562917793", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b761f97-3690-497a-b6ab-c71f61b8e841.jpg?1562917793"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seismic Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shard Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43db4810-078e-487a-afef-57cbc1db0cc7.jpg?1562878159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43db4810-078e-487a-afef-57cbc1db0cc7.jpg?1562878159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/00365412-41db-427c-9109-8f69c17c326d.jpg?1576381909", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/00365412-41db-427c-9109-8f69c17c326d.jpg?1576381909"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/334ad39a-4088-4530-8f3c-d34e7cc99fae.jpg?1562545881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/334ad39a-4088-4530-8f3c-d34e7cc99fae.jpg?1562545881"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83c92b5d-103c-4719-a850-690a7010291a.jpg?1562926054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83c92b5d-103c-4719-a850-690a7010291a.jpg?1562926054"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea653772-a5fe-4416-bef3-fd41133371db.jpg?1562250385", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea653772-a5fe-4416-bef3-fd41133371db.jpg?1562250385"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9b2ff2a-6dfe-4635-8da2-22d525e82b94.jpg?1562597849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9b2ff2a-6dfe-4635-8da2-22d525e82b94.jpg?1562597849"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/60eeb025-704c-4a82-90b2-f91202ae30d9.jpg?1623945691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60eeb025-704c-4a82-90b2-f91202ae30d9.jpg?1623945691"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Shower of Sparks", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54428999-a83d-40a5-9753-dfefdf705a9e.jpg?1562912591", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54428999-a83d-40a5-9753-dfefdf705a9e.jpg?1562912591"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shrapnel Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/056affab-4e2a-4b68-b864-d879becd3c45.jpg?1562134669", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/056affab-4e2a-4b68-b864-d879becd3c45.jpg?1562134669"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shrapnel Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8baa9d1-b45d-4947-9eb6-7f580c83a2c3.jpg?1562164853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8baa9d1-b45d-4947-9eb6-7f580c83a2c3.jpg?1562164853"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sizzle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfdfe2a9-1323-4f15-b2ce-d8dd404b914d.jpg?1587913602", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfdfe2a9-1323-4f15-b2ce-d8dd404b914d.jpg?1587913602"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Sizzle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f1ca1eee-d97d-48c6-84f1-7d1f972c3ca9.jpg?1562383987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f1ca1eee-d97d-48c6-84f1-7d1f972c3ca9.jpg?1562383987"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skewer the Critics", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97295660-6bea-46ae-9a3b-0fc6abba407f.jpg?1584831035", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97295660-6bea-46ae-9a3b-0fc6abba407f.jpg?1584831035"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skullcrack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/8068a146-f6fe-46f3-a42e-822fbc3502e6.jpg?1561833692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/8068a146-f6fe-46f3-a42e-822fbc3502e6.jpg?1561833692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skull Rend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c8efb23-bac0-41d2-b4ee-27a6b1fe3134.jpg?1562783397", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c8efb23-bac0-41d2-b4ee-27a6b1fe3134.jpg?1562783397"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skullscorch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88f1343c-77bf-4f44-8226-fdfb2c2c7015.jpg?1562630775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88f1343c-77bf-4f44-8226-fdfb2c2c7015.jpg?1562630775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slagstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaying Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83b5b110-c430-4ffe-9fc1-8e6987f52d1e.jpg?1572490469", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83b5b110-c430-4ffe-9fc1-8e6987f52d1e.jpg?1572490469"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smash to Smithereens", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7eda1524-44dd-4f70-ac21-bac51578860e.jpg?1562832260", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7eda1524-44dd-4f70-ac21-bac51578860e.jpg?1562832260"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smash to Smithereens", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4daccff6-8395-4b11-a4ce-3576aa38bc09.jpg?1562636800", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4daccff6-8395-4b11-a4ce-3576aa38bc09.jpg?1562636800"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Smoke Spirits' Aid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e492a245-46ba-438e-8d81-4626faa49bff.jpg?1651655377", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e492a245-46ba-438e-8d81-4626faa49bff.jpg?1651655377"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Solar Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b36fc40c-6a68-4192-91d9-2031c7d32e05.jpg?1562937333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b36fc40c-6a68-4192-91d9-2031c7d32e05.jpg?1562937333"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sonic Assault", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc61a398-cf16-415b-b3cf-897217dc7cc9.jpg?1572893813", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc61a398-cf16-415b-b3cf-897217dc7cc9.jpg?1572893813"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sonic Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05530d5a-dcb6-403e-9e35-224c7b5cf615.jpg?1562086889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05530d5a-dcb6-403e-9e35-224c7b5cf615.jpg?1562086889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sonic Seizure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98eb9371-aa20-4790-baf8-a1ad95de39de.jpg?1562631090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98eb9371-aa20-4790-baf8-a1ad95de39de.jpg?1562631090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Jolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6ee479c2-a115-450b-bc2e-b03d23b82f2d.jpg?1562819617", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6ee479c2-a115-450b-bc2e-b03d23b82f2d.jpg?1562819617"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Spray", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f60d8716-4297-484c-8e02-c30ce2773a65.jpg?1562536945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f60d8716-4297-484c-8e02-c30ce2773a65.jpg?1562536945"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spawning Breath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90ec1540-e8cb-4edc-a3b3-f71423cb46fc.jpg?1562706335", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90ec1540-e8cb-4edc-a3b3-f71423cb46fc.jpg?1562706335"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/burn3.json b/web/public/mtg/jsons/burn3.json new file mode 100644 index 00000000..aa261ae5 --- /dev/null +++ b/web/public/mtg/jsons/burn3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Staggershock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75624ab3-ddbd-4fe8-8a07-7d1f78ec8a84.jpg?1562705194", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75624ab3-ddbd-4fe8-8a07-7d1f78ec8a84.jpg?1562705194"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Starfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13921f0d-f163-4275-b025-045c1ccd99e5.jpg?1593096122", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13921f0d-f163-4275-b025-045c1ccd99e5.jpg?1593096122"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Start from Scratch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55c99486-ae64-4293-81fb-a4b02e8fcae6.jpg?1637082383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55c99486-ae64-4293-81fb-a4b02e8fcae6.jpg?1637082383"}, "reprint": false, "frame_effects": ["lesson"], "digital": false, "set_type": "expansion"}, {"name": "Steam Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stoke the Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d94c000-52e0-4215-83af-6351dc43e636.jpg?1562783509", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d94c000-52e0-4215-83af-6351dc43e636.jpg?1562783509"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Stoke the Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5a1071a-c50c-439f-8387-5b2c143e24e4.jpg?1562640134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5a1071a-c50c-439f-8387-5b2c143e24e4.jpg?1562640134"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Stomping Slabs", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/820f1acf-7f0c-4ee5-9f18-b5627aac7c81.jpg?1562879653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/820f1acf-7f0c-4ee5-9f18-b5627aac7c81.jpg?1562879653"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Strategy, Schmategy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Structural Collapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d10da484-db67-4afc-90ef-6caf7d2e3a75.jpg?1561847167", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d10da484-db67-4afc-90ef-6caf7d2e3a75.jpg?1561847167"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Structural Distortion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7895890-a774-4c7c-9f15-78b8aadfd9ef.jpg?1576384931", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7895890-a774-4c7c-9f15-78b8aadfd9ef.jpg?1576384931"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sudden Shock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9fcc7ad0-1348-44e9-9782-e9b7fd032fa4.jpg?1606761799", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9fcc7ad0-1348-44e9-9782-e9b7fd032fa4.jpg?1606761799"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sulfurous Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Surging Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/156994b5-a0f2-4d02-9bda-882e80d9905c.jpg?1561756701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/156994b5-a0f2-4d02-9bda-882e80d9905c.jpg?1561756701"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Tarfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d13a898e-6a97-4fd9-980e-3bfd8d755386.jpg?1562369172", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d13a898e-6a97-4fd9-980e-3bfd8d755386.jpg?1562369172"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderblade Charge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88a85be1-9de5-4f96-9fd1-15f3f17c4bea.jpg?1562922621", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88a85be1-9de5-4f96-9fd1-15f3f17c4bea.jpg?1562922621"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderbolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/5845a5bc-6b7d-4bbb-80b3-a0f877b95553.jpg?1592709223", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/5845a5bc-6b7d-4bbb-80b3-a0f877b95553.jpg?1592709223"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Thunderbolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/0/a0a4b641-2eb3-482b-91a1-236ebe2a7a41.jpg?1562802418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/0/a0a4b641-2eb3-482b-91a1-236ebe2a7a41.jpg?1562802418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderous Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daa39826-7f89-41cb-a7fe-7f7be817d5cd.jpg?1592709229", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daa39826-7f89-41cb-a7fe-7f7be817d5cd.jpg?1592709229"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Titan's Revenge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b1b0f9ca-b752-4dd6-982b-06bb3a27ddbc.jpg?1562880793", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b1b0f9ca-b752-4dd6-982b-06bb3a27ddbc.jpg?1562880793"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Touch of the Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/006ead4a-dc57-4856-8e13-235ba55483e6.jpg?1562895118", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/006ead4a-dc57-4856-8e13-235ba55483e6.jpg?1562895118"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Traitor's Roar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/751e2700-6425-45b8-b026-8c78098f08b2.jpg?1562831801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/751e2700-6425-45b8-b026-8c78098f08b2.jpg?1562831801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tribal Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07fafa53-1e22-43f5-abf3-bbab8130f84d.jpg?1561966002", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07fafa53-1e22-43f5-abf3-bbab8130f84d.jpg?1561966002"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Tribal Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9b32531e-c759-4603-abd0-1724e8df70db.jpg?1562926326", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9b32531e-c759-4603-abd0-1724e8df70db.jpg?1562926326"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unfriendly Fire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a61b274-0499-4cb6-a2e4-f5e18ad7fd2d.jpg?1562558512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a61b274-0499-4cb6-a2e4-f5e18ad7fd2d.jpg?1562558512"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Urza's Rage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/774c52e2-b0d1-4b70-b6d1-bf98f6298603.jpg?1562917055", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/774c52e2-b0d1-4b70-b6d1-bf98f6298603.jpg?1562917055"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Urza's Rage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61a25a35-3ae4-471e-adcd-d8baf2f77b68.jpg?1562914759", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61a25a35-3ae4-471e-adcd-d8baf2f77b68.jpg?1562914759"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Urza's Rage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d80e9897-d84c-4992-9e8e-3a00f377c7e5.jpg?1623945800", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d80e9897-d84c-4992-9e8e-3a00f377c7e5.jpg?1623945800"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Volcanic Geyser", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df5bab70-3c28-48db-9ed3-64706f64f4fa.jpg?1562560984", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df5bab70-3c28-48db-9ed3-64706f64f4fa.jpg?1562560984"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Volcanic Geyser", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/911787db-9023-46f8-9501-3ad26b6ca51d.jpg?1562720483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/911787db-9023-46f8-9501-3ad26b6ca51d.jpg?1562720483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Hammer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8d93606-4864-4a5f-bcbf-8638211e979d.jpg?1562251759", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8d93606-4864-4a5f-bcbf-8638211e979d.jpg?1562251759"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Volcanic Hammer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58c0489d-b073-4ad4-b044-447fcc865b6c.jpg?1562915903", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58c0489d-b073-4ad4-b044-447fcc865b6c.jpg?1562915903"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Volcanic Hammer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/9563d7c1-4ed1-4919-b0b8-ea1ec9d4bbf6.jpg?1562447337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/9563d7c1-4ed1-4919-b0b8-ea1ec9d4bbf6.jpg?1562447337"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Volcanic Spray", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Volt Charge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa88011c-a19d-4faa-8da6-86b9980cd571.jpg?1562880613", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa88011c-a19d-4faa-8da6-86b9980cd571.jpg?1562880613"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Warleader's Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81e474ac-54f7-43f9-8af9-2f1adf258b15.jpg?1562919089", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81e474ac-54f7-43f9-8af9-2f1adf258b15.jpg?1562919089"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Warleader's Helix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fcc1dd23-90fa-4aa4-b0a9-7a92991ad7ec.jpg?1562640152", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fcc1dd23-90fa-4aa4-b0a9-7a92991ad7ec.jpg?1562640152"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Weight of Spires", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5f26a87-4562-450c-800b-7d4acc1ae17b.jpg?1593273313", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5f26a87-4562-450c-800b-7d4acc1ae17b.jpg?1593273313"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wild Slash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/6975490f-7679-48b3-ba34-04dec97a29c2.jpg?1562826120", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/6975490f-7679-48b3-ba34-04dec97a29c2.jpg?1562826120"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Winter Sky", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wizard's Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59bf371a-164c-4db8-9207-197c2e7c3c10.jpg?1562736134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59bf371a-164c-4db8-9207-197c2e7c3c10.jpg?1562736134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Word of Blasting", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c5362ead-9162-4160-bfa9-432f7d0e222d.jpg?1562383027", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c5362ead-9162-4160-bfa9-432f7d0e222d.jpg?1562383027"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Word of Blasting", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46b383c8-d604-4131-a869-9e9d13e30b94.jpg?1562907917", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46b383c8-d604-4131-a869-9e9d13e30b94.jpg?1562907917"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yamabushi's Flame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1a9bacba-55c4-4b92-bdd9-01b6035ed1b2.jpg?1562757952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1a9bacba-55c4-4b92-bdd9-01b6035ed1b2.jpg?1562757952"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/7502ce01-b762-40fe-a064-c7b20b08a722.jpg?1562918451", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/7502ce01-b762-40fe-a064-c7b20b08a722.jpg?1562918451"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zenith Flare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0efac1ed-3f01-487c-86be-8239568b4425.jpg?1591228324", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0efac1ed-3f01-487c-86be-8239568b4425.jpg?1591228324"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/counterspell1.json b/web/public/mtg/jsons/counterspell1.json new file mode 100644 index 00000000..abee75b2 --- /dev/null +++ b/web/public/mtg/jsons/counterspell1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Abjure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fbad9449-d09c-4fd0-b2ad-2aa3a29e03bf.jpg?1562804357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fbad9449-d09c-4fd0-b2ad-2aa3a29e03bf.jpg?1562804357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Absorb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e8a43c1-42d1-45ef-8a63-4b87775a6e88.jpg?1584831352", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e8a43c1-42d1-45ef-8a63-4b87775a6e88.jpg?1584831352"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Absorb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d6a0f3e-457f-41f5-be26-5fb249874f1a.jpg?1562913952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d6a0f3e-457f-41f5-be26-5fb249874f1a.jpg?1562913952"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Absorb Energy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfdca67d-9a97-4ddc-8d50-26a48ad2e4b7.jpg?1645416627", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfdca67d-9a97-4ddc-8d50-26a48ad2e4b7.jpg?1645416627"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Abstruse Interference", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/249a7be3-311e-4ce6-97dc-97242463ae23.jpg?1562902357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/249a7be3-311e-4ce6-97dc-97242463ae23.jpg?1562902357"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Access Denied", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/1642df77-6fe8-47cf-b750-ca4dd9b331ba.jpg?1651655225", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/1642df77-6fe8-47cf-b750-ca4dd9b331ba.jpg?1651655225"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Admiral's Order", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/80dc0310-afd9-49b4-b58f-a0e91120c38c.jpg?1555039852", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/80dc0310-afd9-49b4-b58f-a0e91120c38c.jpg?1555039852"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aether Gust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/783da808-6698-4e55-9fac-430a6effe2b1.jpg?1592516251", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/783da808-6698-4e55-9fac-430a6effe2b1.jpg?1592516251"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Aether Gust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bcc1aa91-ec97-4fe8-b4b1-a213f050f956.jpg?1645141636", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bcc1aa91-ec97-4fe8-b4b1-a213f050f956.jpg?1645141636"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Annul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b1d4a59-11a0-4a55-8ac0-07377a9e6dc8.jpg?1631046631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b1d4a59-11a0-4a55-8ac0-07377a9e6dc8.jpg?1631046631"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Annul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e71b6ad-4b81-4277-8512-0a3f2266cd23.jpg?1562818788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e71b6ad-4b81-4277-8512-0a3f2266cd23.jpg?1562818788"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Annul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8ba18ec8-e82f-41be-9ed8-b1a4ae9b7426.jpg?1562150464", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8ba18ec8-e82f-41be-9ed8-b1a4ae9b7426.jpg?1562150464"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Annul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f8c73ff-be92-41ca-93a7-76f9823adb38.jpg?1562908208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f8c73ff-be92-41ca-93a7-76f9823adb38.jpg?1562908208"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "An Offer You Can't Refuse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d349f3-5be2-4b1f-a4c3-ba94822cf0cf.jpg?1649394290", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d349f3-5be2-4b1f-a4c3-ba94822cf0cf.jpg?1649394290"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anticognition", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db99b872-77c7-4471-9c44-a36d4ff5d33f.jpg?1604193539", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db99b872-77c7-4471-9c44-a36d4ff5d33f.jpg?1604193539"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcane Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d1ffeb1-6c31-45f7-8140-913c397022a3.jpg?1562439019", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d1ffeb1-6c31-45f7-8140-913c397022a3.jpg?1562439019"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Arcane Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab175817-da6a-4ae7-a016-c3bfb087eae0.jpg?1562931100", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab175817-da6a-4ae7-a016-c3bfb087eae0.jpg?1562931100"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Arcane Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0c5728e-43e7-417a-ba18-5038345cec67.jpg?1562770144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0c5728e-43e7-417a-ba18-5038345cec67.jpg?1562770144"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcane Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/415a3104-90e6-4235-b67f-69337c7fe714.jpg?1562768258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/415a3104-90e6-4235-b67f-69337c7fe714.jpg?1562768258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Archmage's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57b852b6-4388-4a41-a5c0-bba37a5c1451.jpg?1562201300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57b852b6-4388-4a41-a5c0-bba37a5c1451.jpg?1562201300"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Archmage's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d378f4f8-ff9f-4389-86c8-23c5c4990b4c.jpg?1657849868", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d378f4f8-ff9f-4389-86c8-23c5c4990b4c.jpg?1657849868"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Artifact Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/1506d99d-7b2e-4101-84a5-c950dadb263a.jpg?1562899411", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/1506d99d-7b2e-4101-84a5-c950dadb263a.jpg?1562899411"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assert Authority", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc339ed7-e1d4-4fe9-a4c4-b030d3e74c00.jpg?1562163986", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc339ed7-e1d4-4fe9-a4c4-b030d3e74c00.jpg?1562163986"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Avoid Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92f1509e-6ed5-4009-a031-ea84b43cbd1b.jpg?1562859699", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92f1509e-6ed5-4009-a031-ea84b43cbd1b.jpg?1562859699"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bane's Contingency", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19f81099-f657-4f7d-84ad-f472ae87d9c5.jpg?1653844052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19f81099-f657-4f7d-84ad-f472ae87d9c5.jpg?1653844052"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Bant Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65b65c87-b084-44aa-b841-411a3c73e234.jpg?1562704776", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65b65c87-b084-44aa-b841-411a3c73e234.jpg?1562704776"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bar the Gate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9b1e53f-1384-4860-9944-e68922afc65c.jpg?1627702860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9b1e53f-1384-4860-9944-e68922afc65c.jpg?1627702860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cfa51783-9ef8-4e51-ba0d-ce8439d83bdf.jpg?1562936749", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cfa51783-9ef8-4e51-ba0d-ce8439d83bdf.jpg?1562936749"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bind to Secrecy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/bab838e0-cfc5-4eeb-920d-bfbe462a1e31.jpg?1655963915", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/bab838e0-cfc5-4eeb-920d-bfbe462a1e31.jpg?1655963915"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Blue Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f51f88f-f662-4572-a371-9a77718ed079.jpg?1562434032", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f51f88f-f662-4572-a371-9a77718ed079.jpg?1562434032"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Blue Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20d666ef-39bf-4fbf-8201-5f1056539da2.jpg?1559591462", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20d666ef-39bf-4fbf-8201-5f1056539da2.jpg?1559591462"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Blue Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/6582b980-3e4b-422a-9a6c-1927ae966d7e.jpg?1561757308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/6582b980-3e4b-422a-9a6c-1927ae966d7e.jpg?1561757308"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Blue Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a671237a-f895-4bbc-b6bd-b0eed4502ec5.jpg?1562547160", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a671237a-f895-4bbc-b6bd-b0eed4502ec5.jpg?1562547160"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Bone to Ash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4a75cef-9551-45e2-b1ff-80662c76ec20.jpg?1562941461", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4a75cef-9551-45e2-b1ff-80662c76ec20.jpg?1562941461"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broken Ambitions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/8052d90b-bc49-4a9e-9211-159a54aa2bcd.jpg?1562355294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/8052d90b-bc49-4a9e-9211-159a54aa2bcd.jpg?1562355294"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broken Concentration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/252eef1f-0a62-420d-aad8-e3d7f1e07c1b.jpg?1576383988", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/252eef1f-0a62-420d-aad8-e3d7f1e07c1b.jpg?1576383988"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brokers Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/657ff5fc-1a95-46f9-85f7-fc1ad757c8c4.jpg?1650506185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/657ff5fc-1a95-46f9-85f7-fc1ad757c8c4.jpg?1650506185"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Brutal Expulsion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cd0e11a-0398-431b-b523-9d3c8a0155cb.jpg?1562132495", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cd0e11a-0398-431b-b523-9d3c8a0155cb.jpg?1562132495"}, "reprint": true, "frame_effects": ["devoid"], "digital": false, "set_type": "promo"}, {"name": "Burnout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a8f5a18-e490-4010-ac1c-c74a5f2dcbda.jpg?1562768717", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a8f5a18-e490-4010-ac1c-c74a5f2dcbda.jpg?1562768717"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Calculated Dismissal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c42ab35-6050-42b2-9c3c-3252f2e69442.jpg?1562012331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c42ab35-6050-42b2-9c3c-3252f2e69442.jpg?1562012331"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cf6e5ad6-ffe2-4588-b357-c415c33fbc11.jpg?1562564222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cf6e5ad6-ffe2-4588-b357-c415c33fbc11.jpg?1562564222"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7258e651-868a-4f63-9454-6c6c95d25387.jpg?1543674894", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7258e651-868a-4f63-9454-6c6c95d25387.jpg?1543674894"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f540dcb-8d0b-4d33-8c0d-893fa5db54eb.jpg?1562791164", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f540dcb-8d0b-4d33-8c0d-893fa5db54eb.jpg?1562791164"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/d/fd994a26-65ff-43be-8d52-476e887d3ed2.jpg?1562795930", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/d/fd994a26-65ff-43be-8d52-476e887d3ed2.jpg?1562795930"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e557f54-3d9d-4610-a0d0-5874feacc76e.jpg?1562614848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e557f54-3d9d-4610-a0d0-5874feacc76e.jpg?1562614848"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/479f56c2-8256-4325-909a-bf460505dbc5.jpg?1562703421", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/479f56c2-8256-4325-909a-bf460505dbc5.jpg?1562703421"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4e175f7-f649-451b-9ee5-ad1140b2e8a7.jpg?1562933181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4e175f7-f649-451b-9ee5-ad1140b2e8a7.jpg?1562933181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cancel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc4d6368-03dc-488a-9a6b-07a549a87572.jpg?1561757939", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc4d6368-03dc-488a-9a6b-07a549a87572.jpg?1561757939"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Censor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cb4e315-1a77-479a-9f15-fb23575de805.jpg?1543674908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cb4e315-1a77-479a-9f15-fb23575de805.jpg?1543674908"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ceremonious Rejection", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08c5ed8e-4804-4042-8a1d-ad24c6846816.jpg?1576381129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08c5ed8e-4804-4042-8a1d-ad24c6846816.jpg?1576381129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Circular Logic", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd9198d6-201d-4175-8f70-eef92d7d5bb5.jpg?1562632085", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd9198d6-201d-4175-8f70-eef92d7d5bb5.jpg?1562632085"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clash of Wills", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/665ee42f-8d76-4f8b-9dd3-7455a90f0da7.jpg?1562023499", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/665ee42f-8d76-4f8b-9dd3-7455a90f0da7.jpg?1562023499"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Clash of Wills", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c67ab53-9489-4658-859e-9dd8a6e0f20d.jpg?1562636752", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c67ab53-9489-4658-859e-9dd8a6e0f20d.jpg?1562636752"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Complicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33f69670-e494-42b8-9148-fe105ec61aa0.jpg?1562907165", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f69670-e494-42b8-9148-fe105ec61aa0.jpg?1562907165"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Concerted Defense", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/235c108d-3902-4c2e-919c-a5449cd2dc3c.jpg?1604193820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/235c108d-3902-4c2e-919c-a5449cd2dc3c.jpg?1604193820"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Condescend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5ba16c0f-dd42-4a2a-8f08-bc8c8478952b.jpg?1562849378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5ba16c0f-dd42-4a2a-8f08-bc8c8478952b.jpg?1562849378"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Condescend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8303b80-e29a-46b8-90b0-c0cfe551b435.jpg?1562880436", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8303b80-e29a-46b8-90b0-c0cfe551b435.jpg?1562880436"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Confirm Suspicions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cf7fcbc2-1034-442d-9f2a-7d79ea40ac3d.jpg?1576384007", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cf7fcbc2-1034-442d-9f2a-7d79ea40ac3d.jpg?1576384007"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Confound", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f3b7d39-ce98-48e2-b2bf-0d55b4d3102b.jpg?1562911605", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f3b7d39-ce98-48e2-b2bf-0d55b4d3102b.jpg?1562911605"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Contradict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/0/a0b3d4ff-09d1-4d9f-8c83-cdfbd7bb1079.jpg?1562790758", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/0/a0b3d4ff-09d1-4d9f-8c83-cdfbd7bb1079.jpg?1562790758"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Controvert", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0e670f6b-d16e-47fc-a5b7-7ca0d8763644.jpg?1593274904", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0e670f6b-d16e-47fc-a5b7-7ca0d8763644.jpg?1593274904"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Convolute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3fd8e607-8179-4ae8-ba7f-f5f22649dc18.jpg?1591230479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3fd8e607-8179-4ae8-ba7f-f5f22649dc18.jpg?1591230479"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Convolute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e17cf756-ec41-4934-8906-4276277c1470.jpg?1576384056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e17cf756-ec41-4934-8906-4276277c1470.jpg?1576384056"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Convolute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fac88052-96a3-4a4d-95a2-c5a652fcb275.jpg?1598914075", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fac88052-96a3-4a4d-95a2-c5a652fcb275.jpg?1598914075"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corrupted Resolve", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28432161-023b-4a98-b92a-55dc6d936cd1.jpg?1562876198", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28432161-023b-4a98-b92a-55dc6d936cd1.jpg?1562876198"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Counterbore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f4228b80-d87d-4ebe-ae92-04e4a7d0dc43.jpg?1562838120", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f4228b80-d87d-4ebe-ae92-04e4a7d0dc43.jpg?1562838120"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Counterflux", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94e4b773-40a4-4272-85dd-f728ada22748.jpg?1562790128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94e4b773-40a4-4272-85dd-f728ada22748.jpg?1562790128"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Counterflux", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e864fd80-baee-468e-9dc3-e650cc203b23.jpg?1657120160", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e864fd80-baee-468e-9dc3-e650cc203b23.jpg?1657120160"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Counterlash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3ec2c57-8e67-472d-8f2e-0492d311f130.jpg?1562945498", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3ec2c57-8e67-472d-8f2e-0492d311f130.jpg?1562945498"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Countermand", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07815e32-0b64-4c2b-84e6-a72336c45cf5.jpg?1593095401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07815e32-0b64-4c2b-84e6-a72336c45cf5.jpg?1593095401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c9a7cb0-5bff-48ff-b620-2838816ac9b5.jpg?1580013910", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c9a7cb0-5bff-48ff-b620-2838816ac9b5.jpg?1580013910"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29bb1b85-9444-4bfa-b622-092a6873631c.jpg?1562234566", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29bb1b85-9444-4bfa-b622-092a6873631c.jpg?1562234566"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bd03c80-7812-4704-9e07-9cf73b49c01f.jpg?1562381815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bd03c80-7812-4704-9e07-9cf73b49c01f.jpg?1562381815"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/dacdd380-71cf-4832-bd02-3697501325f3.jpg?1562056885", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/dacdd380-71cf-4832-bd02-3697501325f3.jpg?1562056885"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b975289d-d8b8-46b4-8c60-d6ed4b594519.jpg?1562593755", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b975289d-d8b8-46b4-8c60-d6ed4b594519.jpg?1562593755"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/aedbcbaa-40f0-485f-8427-778edc2d2ec0.jpg?1562927522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/aedbcbaa-40f0-485f-8427-778edc2d2ec0.jpg?1562927522"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0df55e3f-14de-46ef-b6b1-616618724d9e.jpg?1559591713", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0df55e3f-14de-46ef-b6b1-616618724d9e.jpg?1559591713"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f35ec9da-f38b-4b7f-9eb5-090ca7755668.jpg?1645141660", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f35ec9da-f38b-4b7f-9eb5-090ca7755668.jpg?1645141660"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c358d75-01ad-4487-8104-425124b96aae.jpg?1628337127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c358d75-01ad-4487-8104-425124b96aae.jpg?1628337127"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "draft_innovation"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffdf9d2a-c163-43df-9a2f-20b8749c86ae.jpg?1631491044", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffdf9d2a-c163-43df-9a2f-20b8749c86ae.jpg?1631491044"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/3126d20f-1082-4ebc-b2fa-b12be3ba1bac.jpg?1562904991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/3126d20f-1082-4ebc-b2fa-b12be3ba1bac.jpg?1562904991"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Counterspell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/7065deea-6117-47d4-9d72-fc67af5bb483.jpg?1561757383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/7065deea-6117-47d4-9d72-fc67af5bb483.jpg?1561757383"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Countersquall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b645d74-420e-45e5-aa82-ba3a8dfdd9a0.jpg?1562905206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b645d74-420e-45e5-aa82-ba3a8dfdd9a0.jpg?1562905206"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Countersquall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec16e216-95e1-41f7-87e0-78b6ac3fe1df.jpg?1562804491", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec16e216-95e1-41f7-87e0-78b6ac3fe1df.jpg?1562804491"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Countervailing Winds", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de1c0ef3-b32c-403a-93cb-29cf05795711.jpg?1562817497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de1c0ef3-b32c-403a-93cb-29cf05795711.jpg?1562817497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crush Dissent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94c70f23-0ca9-425e-a53a-6c09921c0075.jpg?1557576187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94c70f23-0ca9-425e-a53a-6c09921c0075.jpg?1557576187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/30f6fca9-003b-4f6b-9d6e-1e88adda4155.jpg?1562847413", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/30f6fca9-003b-4f6b-9d6e-1e88adda4155.jpg?1562847413"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/829e3d6e-5d7c-4cc4-a7a6-7cbf5a7442ba.jpg?1562355759", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/829e3d6e-5d7c-4cc4-a7a6-7cbf5a7442ba.jpg?1562355759"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2a384c1-a05f-4f00-bd77-f897d9819971.jpg?1562927862", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2a384c1-a05f-4f00-bd77-f897d9819971.jpg?1562927862"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/526607e9-1907-4639-b944-8ee152c81bfb.jpg?1561757137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/526607e9-1907-4639-b944-8ee152c81bfb.jpg?1561757137"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Dash Hopes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/814bcfc0-7539-4ed9-8b51-27e6a3ab9d9a.jpg?1562575740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/814bcfc0-7539-4ed9-8b51-27e6a3ab9d9a.jpg?1562575740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dawn Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a4c9667b-1d94-42eb-ae8e-1ae4755e200a.jpg?1562578420", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a4c9667b-1d94-42eb-ae8e-1ae4755e200a.jpg?1562578420"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Daze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f05e9a3e-8a35-4687-85cb-e31b3927a5e2.jpg?1580013916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f05e9a3e-8a35-4687-85cb-e31b3927a5e2.jpg?1580013916"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Daze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d03bff25-0d5e-4dcf-8d75-6df846afea3b.jpg?1562632115", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d03bff25-0d5e-4dcf-8d75-6df846afea3b.jpg?1562632115"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Daze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9b037f1-3298-4ba8-92a8-0843f6e497d7.jpg?1562929191", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9b037f1-3298-4ba8-92a8-0843f6e497d7.jpg?1562929191"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Decisive Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b2e9d132-95f7-4ee7-9c91-be19e4ad7a5d.jpg?1627428577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b2e9d132-95f7-4ee7-9c91-be19e4ad7a5d.jpg?1627428577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Delay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/3906d538-f1ca-4799-b91c-2e0d2934f241.jpg?1619393997", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/3906d538-f1ca-4799-b91c-2e0d2934f241.jpg?1619393997"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Delay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e821d337-4bc5-4401-ac9b-34adf4012b73.jpg?1562941573", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e821d337-4bc5-4401-ac9b-34adf4012b73.jpg?1562941573"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Denied!", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1285c125-e145-4565-a029-352ac6adf688.jpg?1562799062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1285c125-e145-4565-a029-352ac6adf688.jpg?1562799062"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Deny Existence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16a14eeb-1c85-4029-a047-39a4efef3f74.jpg?1576384025", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16a14eeb-1c85-4029-a047-39a4efef3f74.jpg?1576384025"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deny the Divine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1200f68a-a8ea-4777-b6b0-de48b2203fd1.jpg?1588900840", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1200f68a-a8ea-4777-b6b0-de48b2203fd1.jpg?1588900840"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deprive", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2efecdd9-bd3a-4b79-92da-6485589d5bde.jpg?1562702470", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2efecdd9-bd3a-4b79-92da-6485589d5bde.jpg?1562702470"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desertion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/a/9a2a1779-af08-4a9a-aba4-e6892ce2332c.jpg?1562278155", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/a/9a2a1779-af08-4a9a-aba4-e6892ce2332c.jpg?1562278155"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devious Cover-Up", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/648281fe-89fb-4d8d-b944-3af28fb044f6.jpg?1634348751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/648281fe-89fb-4d8d-b944-3af28fb044f6.jpg?1634348751"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Devious Cover-Up", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21ac6b0a-b1a5-439d-b65e-5f04e1826c80.jpg?1636491628", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21ac6b0a-b1a5-439d-b65e-5f04e1826c80.jpg?1636491628"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Didn't Say Please", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77500b53-0852-4d6a-bfe3-b1e8ef5a12cd.jpg?1572489858", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77500b53-0852-4d6a-bfe3-b1e8ef5a12cd.jpg?1572489858"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Disallow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25f05814-a5a5-460f-9d29-0ab03efecf4c.jpg?1576381471", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25f05814-a5a5-460f-9d29-0ab03efecf4c.jpg?1576381471"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disappearing Act", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/a/9a4a6d56-9bed-444c-aae8-383c315779a0.jpg?1576381158", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/a/9a4a6d56-9bed-444c-aae8-383c315779a0.jpg?1576381158"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Discombobulate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cef584c5-6e2d-419b-9c11-a1b6c9c9ab2a.jpg?1562943839", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cef584c5-6e2d-419b-9c11-a1b6c9c9ab2a.jpg?1562943839"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Discontinuity", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b33ba0a8-04e9-4df6-af20-a3ca4470cdcc.jpg?1594735451", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b33ba0a8-04e9-4df6-af20-a3ca4470cdcc.jpg?1594735451"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Disdainful Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/492aa24c-61c4-48bc-b7b7-f423be2662da.jpg?1649881231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/492aa24c-61c4-48bc-b7b7-f423be2662da.jpg?1649881231"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Disdainful Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/7691ac89-f8ba-493e-aa11-5674a783dffb.jpg?1631047007", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/7691ac89-f8ba-493e-aa11-5674a783dffb.jpg?1631047007"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Disdainful Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0193dfa3-8409-44be-b4be-6c3cad42d4a4.jpg?1572892724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0193dfa3-8409-44be-b4be-6c3cad42d4a4.jpg?1572892724"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Disdainful Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/8/180425c9-1898-48d4-9932-ddfb1a28e6b0.jpg?1562783110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/8/180425c9-1898-48d4-9932-ddfb1a28e6b0.jpg?1562783110"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disdainful Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/3711f61d-6381-4c92-a3f5-6deed29aae47.jpg?1562639749", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/3711f61d-6381-4c92-a3f5-6deed29aae47.jpg?1562639749"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Dismal Failure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35786a7a-faa6-457d-9b92-da560b93a43a.jpg?1562569290", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35786a7a-faa6-457d-9b92-da560b93a43a.jpg?1562569290"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dismiss", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e55d6be-7682-4786-9872-e847afd710b0.jpg?1562052798", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e55d6be-7682-4786-9872-e847afd710b0.jpg?1562052798"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dispel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bceab6b3-6b64-4964-a501-ce806a6c13ad.jpg?1562939587", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bceab6b3-6b64-4964-a501-ce806a6c13ad.jpg?1562939587"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dispel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08d4a8d7-c136-472f-8146-a1100701ca4f.jpg?1562782227", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08d4a8d7-c136-472f-8146-a1100701ca4f.jpg?1562782227"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dispel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f178d0cc-5dd1-41ab-a2e8-218ece6f2a86.jpg?1562299138", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f178d0cc-5dd1-41ab-a2e8-218ece6f2a86.jpg?1562299138"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dispersal Shield", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c257df6-f275-40db-bfe3-a9291356cdf7.jpg?1562525399", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c257df6-f275-40db-bfe3-a9291356cdf7.jpg?1562525399"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disrupt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c000a02f-6b7e-4925-a938-59e645e980d7.jpg?1562933600", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c000a02f-6b7e-4925-a938-59e645e980d7.jpg?1562933600"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Disrupt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6cc89b0-9acf-452b-ac1a-bc7e90eb32fc.jpg?1562803281", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6cc89b0-9acf-452b-ac1a-bc7e90eb32fc.jpg?1562803281"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disrupting Shoal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/15589745-4c0a-4edf-ad45-3b7fa45e70c5.jpg?1562875608", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/15589745-4c0a-4edf-ad45-3b7fa45e70c5.jpg?1562875608"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disruption Protocol", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/053ab598-06a4-43ae-b9fd-c291bd05642c.jpg?1654566666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/053ab598-06a4-43ae-b9fd-c291bd05642c.jpg?1654566666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dissipate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/4689b3f2-e4b7-448e-b3d4-ab33194aafb2.jpg?1634348774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/4689b3f2-e4b7-448e-b3d4-ab33194aafb2.jpg?1634348774"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dissipate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d778082-bcdb-423a-b16f-57ac0d4dace7.jpg?1562830916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d778082-bcdb-423a-b16f-57ac0d4dace7.jpg?1562830916"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dissipate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36d9271d-6dbf-4640-9222-721a7a3ccc08.jpg?1562718782", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36d9271d-6dbf-4640-9222-721a7a3ccc08.jpg?1562718782"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dissolve", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/992e8119-f933-4e54-bb04-e1cc78f7e87b.jpg?1562821811", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/992e8119-f933-4e54-bb04-e1cc78f7e87b.jpg?1562821811"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dissolve", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2068083-5d53-43c3-af22-79bf617ccf1b.jpg?1562640127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2068083-5d53-43c3-af22-79bf617ccf1b.jpg?1562640127"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Divide by Zero", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/1958d96e-ec44-48ab-80b1-5b01a24ac7b8.jpg?1644607565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/1958d96e-ec44-48ab-80b1-5b01a24ac7b8.jpg?1644607565"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Double Negative", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c7e3c58-3cda-4891-8b3d-33bb21568cf5.jpg?1562640325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c7e3c58-3cda-4891-8b3d-33bb21568cf5.jpg?1562640325"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dovin's Veto", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d60ca98f-7f91-4bbd-9d06-dadb0c1da282.jpg?1570573658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d60ca98f-7f91-4bbd-9d06-dadb0c1da282.jpg?1570573658"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Dream Fracture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daca6a57-38b7-4547-9174-a7f548ea1258.jpg?1653691053", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daca6a57-38b7-4547-9174-a7f548ea1258.jpg?1653691053"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Dream Fracture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cfd71ff-d899-4f5b-b7df-ec47e2840be9.jpg?1562911180", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cfd71ff-d899-4f5b-b7df-ec47e2840be9.jpg?1562911180"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dromar's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/69f752d3-3f42-4275-be09-d257c89da70d.jpg?1562917160", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/69f752d3-3f42-4275-be09-d257c89da70d.jpg?1562917160"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Dromar's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c7a1894c-af4e-4530-960f-2225916be8cb.jpg?1562937176", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c7a1894c-af4e-4530-960f-2225916be8cb.jpg?1562937176"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Endless Detour", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13798c8c-1aa5-4f95-979b-b971e73d715f.jpg?1649942599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13798c8c-1aa5-4f95-979b-b971e73d715f.jpg?1649942599"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Endless Detour", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e55503d2-1b32-43cf-95c6-a4a61047a4dc.jpg?1649942620", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e55503d2-1b32-43cf-95c6-a4a61047a4dc.jpg?1649942620"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Envelop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7ed250e-12d0-4ebc-9410-5711e71c6d1f.jpg?1562632433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7ed250e-12d0-4ebc-9410-5711e71c6d1f.jpg?1562632433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ertai's Meddling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35c7e7fa-1493-4ef8-9cdb-b02b07a1ad85.jpg?1562053736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35c7e7fa-1493-4ef8-9cdb-b02b07a1ad85.jpg?1562053736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ertai's Trickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/544e3575-9fb6-41f7-a4e6-f8460dfae344.jpg?1562912607", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/544e3575-9fb6-41f7-a4e6-f8460dfae344.jpg?1562912607"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Backlash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a98609dc-ea90-4c7e-a191-5e5d0ba16847.jpg?1562791298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a98609dc-ea90-4c7e-a191-5e5d0ba16847.jpg?1562791298"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Capture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f39bf1fa-b530-4353-a683-843466227109.jpg?1654566672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f39bf1fa-b530-4353-a683-843466227109.jpg?1654566672"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Essence Capture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce137910-0f0e-4f94-9b95-6e0eeeba164e.jpg?1584830187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce137910-0f0e-4f94-9b95-6e0eeeba164e.jpg?1584830187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Scatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f79c8a0-291e-4e13-b765-4cf8c726cf30.jpg?1636491405", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f79c8a0-291e-4e13-b765-4cf8c726cf30.jpg?1636491405"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Essence Scatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1e325e1-f1f9-4448-84e3-1fd929b0bc12.jpg?1543674950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1e325e1-f1f9-4448-84e3-1fd929b0bc12.jpg?1543674950"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Essence Scatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c231101e-6620-46fc-a0ad-a53291d12dc2.jpg?1561994248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c231101e-6620-46fc-a0ad-a53291d12dc2.jpg?1561994248"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Evasive Action", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d0b4f29-ada4-41d2-8292-b5af537c6fd2.jpg?1562916923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d0b4f29-ada4-41d2-8292-b5af537c6fd2.jpg?1562916923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Exclude", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1a50f54-6363-41dd-88a7-9f9e820e7d5f.jpg?1562439432", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1a50f54-6363-41dd-88a7-9f9e820e7d5f.jpg?1562439432"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Exclude", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/970864e0-5488-4b6f-9316-3e3b4098770e.jpg?1561951119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/970864e0-5488-4b6f-9316-3e3b4098770e.jpg?1561951119"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Exclude", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/aeb359c8-209c-455f-84b2-970e5678a9fa.jpg?1562930137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/aeb359c8-209c-455f-84b2-970e5678a9fa.jpg?1562930137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinguish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21140417-09f5-4d05-b94c-355fde9b4719.jpg?1562255853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21140417-09f5-4d05-b94c-355fde9b4719.jpg?1562255853"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Extinguish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/641f4e66-b46b-4da3-a053-f3763400d4f5.jpg?1562918557", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/641f4e66-b46b-4da3-a053-f3763400d4f5.jpg?1562918557"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Faerie Trickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/defb9f0b-195e-4aeb-92c1-8f827ad6724b.jpg?1562371108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/defb9f0b-195e-4aeb-92c1-8f827ad6724b.jpg?1562371108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Failed Inspection", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8900f91-cb17-4f99-a5ce-15819369beb8.jpg?1576381199", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8900f91-cb17-4f99-a5ce-15819369beb8.jpg?1576381199"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fall of the Gavel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64f42848-963b-4b16-aeec-66d0f349758b.jpg?1562787318", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64f42848-963b-4b16-aeec-66d0f349758b.jpg?1562787318"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "False Summoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd7d30a8-bc7a-42bc-8d1b-600cbf78ab98.jpg?1562943500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd7d30a8-bc7a-42bc-8d1b-600cbf78ab98.jpg?1562943500"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Familiar's Ruse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55b9be91-f3a1-49ce-8a3e-2ecd30e2e692.jpg?1562348978", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55b9be91-f3a1-49ce-8a3e-2ecd30e2e692.jpg?1562348978"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fervent Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b15428e-946e-490d-93bb-9888bfd3a1df.jpg?1568003997", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b15428e-946e-490d-93bb-9888bfd3a1df.jpg?1568003997"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Fervent Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed13fdb4-f28a-43c9-a69f-bab227806c39.jpg?1562939482", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed13fdb4-f28a-43c9-a69f-bab227806c39.jpg?1562939482"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Fierce Guardianship", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c5ffa83-c88d-4f5d-851e-a642b229d596.jpg?1591319453", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c5ffa83-c88d-4f5d-851e-a642b229d596.jpg?1591319453"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Flaccify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/409cb48a-572a-40df-ae1a-a43feab6bdfd.jpg?1562487932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/409cb48a-572a-40df-ae1a-a43feab6bdfd.jpg?1562487932"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Flash Counter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc14e61f-481a-4bfa-aca0-fb63dc952be6.jpg?1562939250", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc14e61f-481a-4bfa-aca0-fb63dc952be6.jpg?1562939250"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Flash Counter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c3cd450-f1cd-416b-9271-37d95815c089.jpg?1587858200", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c3cd450-f1cd-416b-9271-37d95815c089.jpg?1587858200"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flashfreeze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cefd9955-a195-4855-a00e-3809b96ca92b.jpg?1593274923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cefd9955-a195-4855-a00e-3809b96ca92b.jpg?1593274923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flip the Switch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5cdbe4e3-f030-46fa-ae84-edf261b61706.jpg?1634348893", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5cdbe4e3-f030-46fa-ae84-edf261b61706.jpg?1634348893"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flusterstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e2e09bf-e7c8-4f13-bcee-f9c8cbc57993.jpg?1592713006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e2e09bf-e7c8-4f13-bcee-f9c8cbc57993.jpg?1592713006"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Flusterstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c2077c2-81ce-4ddf-82f0-6fece362d6d7.jpg?1562546827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c2077c2-81ce-4ddf-82f0-6fece362d6d7.jpg?1562546827"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Foil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8b39fd6-9240-4f76-b12c-e7d9aa88f061.jpg?1547516254", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8b39fd6-9240-4f76-b12c-e7d9aa88f061.jpg?1547516254"}, "reprint": true, "digital": false, "set_type": "masters"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/counterspell2.json b/web/public/mtg/jsons/counterspell2.json new file mode 100644 index 00000000..e32ed2c7 --- /dev/null +++ b/web/public/mtg/jsons/counterspell2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Foil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/870fb793-3107-4cb2-ba78-34fbf5c9da2f.jpg?1562920018", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/870fb793-3107-4cb2-ba78-34fbf5c9da2f.jpg?1562920018"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fold into Aether", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/615157d6-0160-417b-b06c-0e253b306c37.jpg?1562877336", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/615157d6-0160-417b-b06c-0e253b306c37.jpg?1562877336"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Forbid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29df5ef7-d679-4543-bdb7-3984155c87e0.jpg?1562087370", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29df5ef7-d679-4543-bdb7-3984155c87e0.jpg?1562087370"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Forbid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14a9cc52-a45b-4cde-8aff-d672b35c3118.jpg?1562899128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14a9cc52-a45b-4cde-8aff-d672b35c3118.jpg?1562899128"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Forceful Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27c75157-2670-4804-8853-a6867c83c40a.jpg?1608909212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27c75157-2670-4804-8853-a6867c83c40a.jpg?1608909212"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Force of Negation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9be371c-c688-44ad-ab71-bd4c9f242d58.jpg?1562201382", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9be371c-c688-44ad-ab71-bd4c9f242d58.jpg?1562201382"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Force of Negation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/5396b405-6fa0-43d7-a8f6-f64154e95e98.jpg?1655823932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/5396b405-6fa0-43d7-a8f6-f64154e95e98.jpg?1655823932"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Force of Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/ebc01ab4-d89a-4d25-bf54-6aed33772f4b.jpg?1580013954", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/ebc01ab4-d89a-4d25-bf54-6aed33772f4b.jpg?1580013954"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Force of Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/a/9a879b60-4381-447d-8a5a-8e0b6a1d49ca.jpg?1562769672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/a/9a879b60-4381-447d-8a5a-8e0b6a1d49ca.jpg?1562769672"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Force of Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec136ce7-bad4-4ebb-ab00-b86de3d209a7.jpg?1599710933", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec136ce7-bad4-4ebb-ab00-b86de3d209a7.jpg?1599710933"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Force of Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/026983a4-03ca-4812-b129-5ea523596942.jpg?1562895460", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/026983a4-03ca-4812-b129-5ea523596942.jpg?1562895460"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Force of Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/53ed5673-728f-4da3-ad18-3bd72032e815.jpg?1562258455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/53ed5673-728f-4da3-ad18-3bd72032e815.jpg?1562258455"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Force Spike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d03d73f-0530-4125-8689-1c43e502e331.jpg?1562233829", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d03d73f-0530-4125-8689-1c43e502e331.jpg?1562233829"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Force Spike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba23d540-8c2d-4a42-b4c0-86f0988bd1ce.jpg?1562593757", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba23d540-8c2d-4a42-b4c0-86f0988bd1ce.jpg?1562593757"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Force Spike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70e64028-ae96-4950-aa6c-9d347409fad3.jpg?1562859654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70e64028-ae96-4950-aa6c-9d347409fad3.jpg?1562859654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Force Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/226555ba-22af-45f1-a3f4-d265f8685dd5.jpg?1587911634", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/226555ba-22af-45f1-a3f4-d265f8685dd5.jpg?1587911634"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frazzle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68b7f705-4d64-4551-8d76-826d91324e9e.jpg?1593271993", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68b7f705-4d64-4551-8d76-826d91324e9e.jpg?1593271993"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frightful Delusion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/38c9ba98-90b4-4c28-9eef-a4fe0913b921.jpg?1562828708", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/38c9ba98-90b4-4c28-9eef-a4fe0913b921.jpg?1562828708"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fuel for the Cause", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/4126e0e5-9b23-496f-8a09-7a35499f9a09.jpg?1562610827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/4126e0e5-9b23-496f-8a09-7a35499f9a09.jpg?1562610827"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gainsay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e658939a-fa5a-4497-b35c-b6fbfa3f6882.jpg?1562835545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e658939a-fa5a-4497-b35c-b6fbfa3f6882.jpg?1562835545"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Gainsay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a70a2092-5048-49c0-9351-a3f882c2f56e.jpg?1562930170", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a70a2092-5048-49c0-9351-a3f882c2f56e.jpg?1562930170"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gale's Redirection", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/f/1f5ddcf8-c87b-4a26-b226-8593f517a74a.jpg?1653353395", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/f/1f5ddcf8-c87b-4a26-b226-8593f517a74a.jpg?1653353395"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Geistlight Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/7302b5da-cac5-4ce7-ad38-2ff4e410891b.jpg?1643587841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/7302b5da-cac5-4ce7-ad38-2ff4e410891b.jpg?1643587841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geist Snatch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6dac5db-ef96-4bd5-aabc-e5ae2b95c8c3.jpg?1592708554", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6dac5db-ef96-4bd5-aabc-e5ae2b95c8c3.jpg?1592708554"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glorious End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/2922b976-7beb-4c68-b39e-1b66d5c6f65e.jpg?1543675588", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/2922b976-7beb-4c68-b39e-1b66d5c6f65e.jpg?1543675588"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grip of Amnesia", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43dc7e2a-5b9b-4f0f-8b2e-a7c7f847e1f1.jpg?1562629609", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43dc7e2a-5b9b-4f0f-8b2e-a7c7f847e1f1.jpg?1562629609"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guttural Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/9121e55e-5070-48cc-b706-92c67ad89254.jpg?1592761849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/9121e55e-5070-48cc-b706-92c67ad89254.jpg?1592761849"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Guttural Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0662ab6-b475-4b8d-ae77-a9b654e611da.jpg?1562837134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0662ab6-b475-4b8d-ae77-a9b654e611da.jpg?1562837134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Halt Order", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fed18af-7301-4d03-ba7c-e94f07f078b3.jpg?1562819574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fed18af-7301-4d03-ba7c-e94f07f078b3.jpg?1562819574"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hinder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc7befed-805b-4a02-a87d-7df3a95db8a0.jpg?1562765119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc7befed-805b-4a02-a87d-7df3a95db8a0.jpg?1562765119"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hinder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/679d6226-7ec1-44f3-ac90-30b123501aa0.jpg?1561757329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/679d6226-7ec1-44f3-ac90-30b123501aa0.jpg?1561757329"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Hindering Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98e43870-4bed-4d76-a633-a6326c736d22.jpg?1562706936", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98e43870-4bed-4d76-a633-a6326c736d22.jpg?1562706936"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hindering Touch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db9735d9-4aac-4175-8ec8-fc9bfd8f2c5c.jpg?1562535667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db9735d9-4aac-4175-8ec8-fc9bfd8f2c5c.jpg?1562535667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hisoka's Defiance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/09fd4d01-1204-46a3-b237-45c37985acac.jpg?1562757466", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/09fd4d01-1204-46a3-b237-45c37985acac.jpg?1562757466"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hornswoggle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/1/b10b8f15-b323-44d8-85a7-ed662a40889d.jpg?1555039907", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/1/b10b8f15-b323-44d8-85a7-ed662a40889d.jpg?1555039907"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Horribly Awry", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cd05532-686e-40dc-858b-8a77a3628c99.jpg?1562912968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cd05532-686e-40dc-858b-8a77a3628c99.jpg?1562912968"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Hydroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c9c9b16-5567-4473-95e6-622292f77336.jpg?1580013995", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c9c9b16-5567-4473-95e6-622292f77336.jpg?1580013995"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Hydroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f62716f0-fde2-49ef-b8a4-c1b03f451194.jpg?1562941220", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f62716f0-fde2-49ef-b8a4-c1b03f451194.jpg?1562941220"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hydroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/222db3a6-c2b1-48fc-9b0c-018ac6ed517b.jpg?1562543501", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/222db3a6-c2b1-48fc-9b0c-018ac6ed517b.jpg?1562543501"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Illumination", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb28f6e5-c9ef-416e-b315-967d857e7600.jpg?1562722393", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb28f6e5-c9ef-416e-b315-967d857e7600.jpg?1562722393"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Induce Paranoia", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc462b75-8b08-47a3-be22-d7b5c062ec5b.jpg?1598914307", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc462b75-8b08-47a3-be22-d7b5c062ec5b.jpg?1598914307"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Insidious Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8eafb2bb-58bf-4c6b-ae8f-91bcea12c7d2.jpg?1576381260", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8eafb2bb-58bf-4c6b-ae8f-91bcea12c7d2.jpg?1576381260"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Interdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/3442c919-73b9-4d29-a014-87293f456325.jpg?1562053290", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/3442c919-73b9-4d29-a014-87293f456325.jpg?1562053290"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Intervene", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b0e3894-5dfe-4d03-9996-eebf96c58168.jpg?1562862808", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b0e3894-5dfe-4d03-9996-eebf96c58168.jpg?1562862808"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Invasive Surgery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e644e38-39bf-40bd-9be1-5eb80f472e81.jpg?1576384110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e644e38-39bf-40bd-9be1-5eb80f472e81.jpg?1576384110"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ionize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f161f7d2-eaa1-4931-93f9-befa8b5df821.jpg?1572893679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f161f7d2-eaa1-4931-93f9-befa8b5df821.jpg?1572893679"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ixidor's Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b713448-853a-41ee-a302-963e9c1c1c65.jpg?1562901464", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b713448-853a-41ee-a302-963e9c1c1c65.jpg?1562901464"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Izzet Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61289196-a56b-4d24-b340-9cf067c77f45.jpg?1592713417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61289196-a56b-4d24-b340-9cf067c77f45.jpg?1592713417"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Izzet Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8e84a97-8e40-42fa-a114-df90e820ede6.jpg?1562497263", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8e84a97-8e40-42fa-a114-df90e820ede6.jpg?1562497263"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Jace's Defeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6b103c1-9b25-4bfe-9081-570977e9fdad.jpg?1562814148", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6b103c1-9b25-4bfe-9081-570977e9fdad.jpg?1562814148"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jaded Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a9ab1f0-4e75-4165-85bc-6f838c221d6a.jpg?1562920093", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a9ab1f0-4e75-4165-85bc-6f838c221d6a.jpg?1562920093"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Keep Safe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/febfa682-76ae-4979-a40c-c1eae1121f3c.jpg?1591226372", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/febfa682-76ae-4979-a40c-c1eae1121f3c.jpg?1591226372"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kindred Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4fbdeac6-f61b-4669-934c-9216d669500f.jpg?1645417342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4fbdeac6-f61b-4669-934c-9216d669500f.jpg?1645417342"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Lapse of Certainty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec609036-dfbf-47de-9a3a-762aea4196d4.jpg?1562804498", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec609036-dfbf-47de-9a3a-762aea4196d4.jpg?1562804498"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laquatus's Disdain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ea5448-2d72-42eb-814c-197153d8e06a.jpg?1562632366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ea5448-2d72-42eb-814c-197153d8e06a.jpg?1562632366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Last Word", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/139d2ece-f656-4cac-8d77-b0f083f76c70.jpg?1562635496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/139d2ece-f656-4cac-8d77-b0f083f76c70.jpg?1562635496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lay Bare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/0454c2a8-b17d-4cdf-8562-9a28bc6cf0be.jpg?1562700738", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/0454c2a8-b17d-4cdf-8562-9a28bc6cf0be.jpg?1562700738"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Liquify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12fadf25-0995-440d-a3e6-7964ed86cff6.jpg?1562628664", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12fadf25-0995-440d-a3e6-7964ed86cff6.jpg?1562628664"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lofty Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64832674-beb1-446e-b2f7-8a5e271139a5.jpg?1616182218", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64832674-beb1-446e-b2f7-8a5e271139a5.jpg?1616182218"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Logic Knot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/624feb0e-f683-4eb6-a63b-7872d0e28f1f.jpg?1619394325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/624feb0e-f683-4eb6-a63b-7872d0e28f1f.jpg?1619394325"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Logic Knot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e946be1-4ed6-4e2c-9782-3f630f8a8e1f.jpg?1562910897", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e946be1-4ed6-4e2c-9782-3f630f8a8e1f.jpg?1562910897"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lookout's Dispersal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5751a3c-7695-4c47-9cbd-92fd5b1b7ec9.jpg?1562566719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5751a3c-7695-4c47-9cbd-92fd5b1b7ec9.jpg?1562566719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lose Focus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/985bdb0c-ce6c-4506-8163-76f3b2fdf5fb.jpg?1626094565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/985bdb0c-ce6c-4506-8163-76f3b2fdf5fb.jpg?1626094565"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lost in the Mist", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e5fc39d-590a-436b-ab90-a1741d2ae3da.jpg?1562827161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e5fc39d-590a-436b-ab90-a1741d2ae3da.jpg?1562827161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mages' Contest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c516861c-68d9-4d02-a343-689dba0526c6.jpg?1562934507", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c516861c-68d9-4d02-a343-689dba0526c6.jpg?1562934507"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Make Disappear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f2d6a21-ea77-484b-9e3a-1bd49806f907.jpg?1649471769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f2d6a21-ea77-484b-9e3a-1bd49806f907.jpg?1649471769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mana Drain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/416d2d51-8f29-4e95-b037-e8c32b081e6c.jpg?1562848002", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/416d2d51-8f29-4e95-b037-e8c32b081e6c.jpg?1562848002"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Mana Drain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc9a04dc-afee-4194-80f5-fb1d9c906de7.jpg?1562936126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc9a04dc-afee-4194-80f5-fb1d9c906de7.jpg?1562936126"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Mana Drain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e691adef-3027-4e6a-889f-9f4e2df36a7c.jpg?1562861377", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e691adef-3027-4e6a-889f-9f4e2df36a7c.jpg?1562861377"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mana Drain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/456a2f03-8304-4512-804c-76653e30f436.jpg?1655827521", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/456a2f03-8304-4512-804c-76653e30f436.jpg?1655827521"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Mana Leak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7c7757d-8036-4b33-a1cb-07795d392588.jpg?1562470857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7c7757d-8036-4b33-a1cb-07795d392588.jpg?1562470857"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mana Leak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abcaf16d-aa02-43e2-aa38-bb1835d47a05.jpg?1562597349", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abcaf16d-aa02-43e2-aa38-bb1835d47a05.jpg?1562597349"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mana Leak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dea41eb7-5828-4735-bca1-0dbb0fda04e3.jpg?1561758236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dea41eb7-5828-4735-bca1-0dbb0fda04e3.jpg?1561758236"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Mana Tithe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d48d622-f397-4f31-b1a5-0c23f60aa71c.jpg?1562575298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d48d622-f397-4f31-b1a5-0c23f60aa71c.jpg?1562575298"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Mana Tithe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7f32354-893d-4f0b-b555-e0757fb5443b.jpg?1623592291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7f32354-893d-4f0b-b555-e0757fb5443b.jpg?1623592291"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Mana Tithe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/652b0ce3-293d-4599-8a04-9df01b9bc678.jpg?1561757305", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/652b0ce3-293d-4599-8a04-9df01b9bc678.jpg?1561757305"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Memory Drain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aadc1809-d6bb-455c-b6ce-dd11521808b6.jpg?1581479403", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aadc1809-d6bb-455c-b6ce-dd11521808b6.jpg?1581479403"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/30202613-d05f-4f47-af97-d0b75ccac293.jpg?1634131658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/30202613-d05f-4f47-af97-d0b75ccac293.jpg?1634131658"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d85cc30-ccae-4af8-834a-f7870dace679.jpg?1562235009", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d85cc30-ccae-4af8-834a-f7870dace679.jpg?1562235009"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63453ed9-5cf1-4cad-b173-a067f22a4405.jpg?1562719747", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63453ed9-5cf1-4cad-b173-a067f22a4405.jpg?1562719747"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d2cc591-3a81-468a-91a4-3c3aac83a21a.jpg?1562587259", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d2cc591-3a81-468a-91a4-3c3aac83a21a.jpg?1562587259"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c8b5df3-6153-470e-be9c-f38d3cf66081.jpg?1562587296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c8b5df3-6153-470e-be9c-f38d3cf66081.jpg?1562587296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Memory Lapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98c1b465-b6d9-491b-bfc2-c034cc825d27.jpg?1623592117", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98c1b465-b6d9-491b-bfc2-c034cc825d27.jpg?1623592117"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Mental Misstep", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61e9c6df-1c84-4eab-9076-a4feb6347c10.jpg?1566819829", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61e9c6df-1c84-4eab-9076-a4feb6347c10.jpg?1566819829"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Metallic Rebuke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f712ac26-dca4-459b-84c1-010597007f60.jpg?1576381519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f712ac26-dca4-459b-84c1-010597007f60.jpg?1576381519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Minamo's Meddling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/502c4aca-98f8-4c7d-89fd-ee42c938fac7.jpg?1562876978", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/502c4aca-98f8-4c7d-89fd-ee42c938fac7.jpg?1562876978"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mindbreak Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f51140b-6254-431a-8810-94307bfdfbbe.jpg?1562612097", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f51140b-6254-431a-8810-94307bfdfbbe.jpg?1562612097"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mindstatic", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55d3fad5-a12a-4b41-9c7b-c1af5e0b5ca8.jpg?1562910742", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55d3fad5-a12a-4b41-9c7b-c1af5e0b5ca8.jpg?1562910742"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mindswipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/557e8303-a021-4257-b41a-7d25f04618c8.jpg?1562786781", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/557e8303-a021-4257-b41a-7d25f04618c8.jpg?1562786781"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Miscalculation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b4956a2-9a39-4152-9c98-70e4b2acfa26.jpg?1562862809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b4956a2-9a39-4152-9c98-70e4b2acfa26.jpg?1562862809"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Miscast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/033afbd5-9937-4957-98ba-48e469a490bb.jpg?1594735579", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/033afbd5-9937-4957-98ba-48e469a490bb.jpg?1594735579"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Molten Influence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c2b326b-d177-4a03-a0a3-fe2c2d4af272.jpg?1562908953", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c2b326b-d177-4a03-a0a3-fe2c2d4af272.jpg?1562908953"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Muddle the Mixture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cc785b0-0a77-4b02-b0b4-2bda2fc621cc.jpg?1598914378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cc785b0-0a77-4b02-b0b4-2bda2fc621cc.jpg?1598914378"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mystical Dispute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fbe04cb8-a8b9-4241-baae-b398a2509a3a.jpg?1572489956", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fbe04cb8-a8b9-4241-baae-b398a2509a3a.jpg?1572489956"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mystic Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81bbffc2-6f58-4baa-8f95-168eab106b15.jpg?1562706477", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81bbffc2-6f58-4baa-8f95-168eab106b15.jpg?1562706477"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Mystic Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1296ddc4-300d-44f6-95d8-1b392613d379.jpg?1562255840", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1296ddc4-300d-44f6-95d8-1b392613d379.jpg?1562255840"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Mystic Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/30bb424f-f3d6-4616-a368-df12af3ad024.jpg?1562906405", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/30bb424f-f3d6-4616-a368-df12af3ad024.jpg?1562906405"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Mystic Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52d60f29-6da0-4ce6-9c92-96f313007271.jpg?1562446637", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52d60f29-6da0-4ce6-9c92-96f313007271.jpg?1562446637"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Mystic Genesis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae1dd1ac-1a1e-485d-a11f-d1323a69f95e.jpg?1561841867", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae1dd1ac-1a1e-485d-a11f-d1323a69f95e.jpg?1561841867"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Narset's Reversal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63754036-d51e-47bb-925b-564d9dc922ff.jpg?1557576279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63754036-d51e-47bb-925b-564d9dc922ff.jpg?1557576279"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e92c7477-d453-4fa4-acf4-3835ab9eb55a.jpg?1604194548", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e92c7477-d453-4fa4-acf4-3835ab9eb55a.jpg?1604194548"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31534f45-43e6-4103-bf58-ad8fa688e4b0.jpg?1555039942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31534f45-43e6-4103-bf58-ad8fa688e4b0.jpg?1555039942"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb142515-0856-441d-84d4-9c9d450a86e9.jpg?1576381530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb142515-0856-441d-84d4-9c9d450a86e9.jpg?1576381530"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/026c499d-3d5b-4f65-a824-f78f146b82ef.jpg?1562895467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/026c499d-3d5b-4f65-a824-f78f146b82ef.jpg?1562895467"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/60380ed0-fed1-4d68-9763-56a9ff8ac5e6.jpg?1562787156", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60380ed0-fed1-4d68-9763-56a9ff8ac5e6.jpg?1562787156"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a501252-e722-4ebf-bcf7-f53a42745fa7.jpg?1562878670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a501252-e722-4ebf-bcf7-f53a42745fa7.jpg?1562878670"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5bfe3a17-3349-4fcc-a9b5-418faa55cc43.jpg?1623592516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5bfe3a17-3349-4fcc-a9b5-418faa55cc43.jpg?1623592516"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/9850fbe9-68d2-4952-b48d-4737cef34f4a.jpg?1561757632", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/9850fbe9-68d2-4952-b48d-4737cef34f4a.jpg?1561757632"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Negate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/226e5187-d285-4547-869d-761fdbee6f1b.jpg?1561756781", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/226e5187-d285-4547-869d-761fdbee6f1b.jpg?1561756781"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Neutralize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/0430da3c-9460-4b62-ae28-2e7e6f4d06a4.jpg?1591226400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/0430da3c-9460-4b62-ae28-2e7e6f4d06a4.jpg?1591226400"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Neutralizing Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e549a8fc-6001-43db-88b1-ce8ed42a3443.jpg?1562830917", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e549a8fc-6001-43db-88b1-ce8ed42a3443.jpg?1562830917"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3dab4f64-2a91-409a-b83b-45b22afd22ff.jpg?1562907421", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3dab4f64-2a91-409a-b83b-45b22afd22ff.jpg?1562907421"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "No Escape", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc9888a1-6f35-4802-b8fb-902017736d4a.jpg?1557576285", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc9888a1-6f35-4802-b8fb-902017736d4a.jpg?1557576285"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Not of This World", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/569e2c39-7a49-4a3b-afe5-1862a7da8026.jpg?1562704022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/569e2c39-7a49-4a3b-afe5-1862a7da8026.jpg?1562704022"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nullify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a940d859-3fb1-4946-8277-b7c503605b1e.jpg?1593091715", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a940d859-3fb1-4946-8277-b7c503605b1e.jpg?1593091715"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Offering to Asha", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/260fe443-ca03-42b1-bcee-86e5173c1aaf.jpg?1562640177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/260fe443-ca03-42b1-bcee-86e5173c1aaf.jpg?1562640177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ojutai's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c7a7f500-594d-4c7b-80e8-54ae1ada2444.jpg?1562792959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c7a7f500-594d-4c7b-80e8-54ae1ada2444.jpg?1562792959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ojutai's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/939778a2-a10d-4dd4-8f78-0c366b76bf81.jpg?1562876267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/939778a2-a10d-4dd4-8f78-0c366b76bf81.jpg?1562876267"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Oppressive Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abcb5e75-c7a1-41de-a952-05aefb115270.jpg?1562495576", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abcb5e75-c7a1-41de-a952-05aefb115270.jpg?1562495576"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Out of Bounds", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b457672-902b-42c0-9d53-a3c21be2f500.jpg?1562923137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b457672-902b-42c0-9d53-a3c21be2f500.jpg?1562923137"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Outwit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/429f7cf0-579a-4003-b5cf-4baf5d420796.jpg?1592708662", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/429f7cf0-579a-4003-b5cf-4baf5d420796.jpg?1592708662"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Override", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35964fa6-800d-41d6-9f82-fb9c87deee56.jpg?1562140248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35964fa6-800d-41d6-9f82-fb9c87deee56.jpg?1562140248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Overrule", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/22b83a31-f974-4a49-b9ee-92f7767f11e0.jpg?1593273676", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/22b83a31-f974-4a49-b9ee-92f7767f11e0.jpg?1593273676"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Overwhelming Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33ff1000-1a4e-43f6-aa02-1dbe9fac6901.jpg?1562905471", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33ff1000-1a4e-43f6-aa02-1dbe9fac6901.jpg?1562905471"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Overwhelming Intellect", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cbeea686-7efc-48f5-b90b-bf1befc76a30.jpg?1562496066", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cbeea686-7efc-48f5-b90b-bf1befc76a30.jpg?1562496066"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pact of Negation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cca467a2-a2b3-4bdf-9d60-62979f675347.jpg?1562936138", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cca467a2-a2b3-4bdf-9d60-62979f675347.jpg?1562936138"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pact of Negation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3ab90299-547a-4538-a31c-f55afab10c50.jpg?1562906886", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3ab90299-547a-4538-a31c-f55afab10c50.jpg?1562906886"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Perplex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0db57459-29f0-4ef6-b256-56955036c0ef.jpg?1598917204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0db57459-29f0-4ef6-b256-56955036c0ef.jpg?1598917204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plasm Capture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0ffe8485-d5fb-47cc-af53-6e0fd062b7a2.jpg?1562898119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0ffe8485-d5fb-47cc-af53-6e0fd062b7a2.jpg?1562898119"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Power Sink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/662cf693-18c4-4169-bcce-09862778f60c.jpg?1562916378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/662cf693-18c4-4169-bcce-09862778f60c.jpg?1562916378"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Power Sink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abc58c34-c3de-47f8-a42f-3a974dcb9c47.jpg?1562055922", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abc58c34-c3de-47f8-a42f-3a974dcb9c47.jpg?1562055922"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Power Sink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49717583-e0bb-47d6-92d0-8959af13391f.jpg?1562718814", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49717583-e0bb-47d6-92d0-8959af13391f.jpg?1562718814"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Power Sink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85cbec45-81b4-40cc-b356-d6713a6a9b2b.jpg?1562919825", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85cbec45-81b4-40cc-b356-d6713a6a9b2b.jpg?1562919825"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Power Sink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b342dd3-09b9-4108-bf12-a65d4cef4eb9.jpg?1559591331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b342dd3-09b9-4108-bf12-a65d4cef4eb9.jpg?1559591331"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Preemptive Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2314bf1-b22d-48c2-860f-e1081f56296b.jpg?1562257530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2314bf1-b22d-48c2-860f-e1081f56296b.jpg?1562257530"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Prohibit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0daa5458-2a97-40d0-b18d-2381a7a68ee1.jpg?1562897807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0daa5458-2a97-40d0-b18d-2381a7a68ee1.jpg?1562897807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Psychic Barrier", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1cba7d67-5c6c-4738-8907-7cce503e3180.jpg?1562875859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1cba7d67-5c6c-4738-8907-7cce503e3180.jpg?1562875859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Psychic Rebuttal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67a105f8-0c01-4c09-a3bf-8c912b6dc741.jpg?1562023585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67a105f8-0c01-4c09-a3bf-8c912b6dc741.jpg?1562023585"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Psychic Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d87927c-80a6-4146-92a5-58c510ce7958.jpg?1561815780", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d87927c-80a6-4146-92a5-58c510ce7958.jpg?1561815780"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Psychic Trance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5e55695-16cc-4373-8078-959f1ded4c6d.jpg?1562945989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5e55695-16cc-4373-8078-959f1ded4c6d.jpg?1562945989"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Punish Ignorance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9bc37d01-ffe5-4dfe-b59e-204df82d1d36.jpg?1562707043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9bc37d01-ffe5-4dfe-b59e-204df82d1d36.jpg?1562707043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Put Away", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c17dff9e-23f7-4b12-95e7-aa1c00ab3d18.jpg?1562835533", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c17dff9e-23f7-4b12-95e7-aa1c00ab3d18.jpg?1562835533"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b029eb9a-dd7a-40c2-96c4-0063d9cc002c.jpg?1580014621", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b029eb9a-dd7a-40c2-96c4-0063d9cc002c.jpg?1580014621"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Pyroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c342cac5-08ae-4428-9c2c-f6c5904e54d2.jpg?1562931528", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c342cac5-08ae-4428-9c2c-f6c5904e54d2.jpg?1562931528"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/93c460dc-cef2-4345-b9b8-a774307ba2d6.jpg?1593559584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/93c460dc-cef2-4345-b9b8-a774307ba2d6.jpg?1593559584"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Pyroblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33afbf78-7a50-48e0-bec8-656f571759e2.jpg?1562543945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33afbf78-7a50-48e0-bec8-656f571759e2.jpg?1562543945"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Quandrix Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/021b62d8-d160-47f5-bc51-0474f160d13f.jpg?1624739521", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/021b62d8-d160-47f5-bc51-0474f160d13f.jpg?1624739521"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48ca8c31-a9ea-4388-b257-951c1c68b86d.jpg?1562876834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48ca8c31-a9ea-4388-b257-951c1c68b86d.jpg?1562876834"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Quash", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62019ac4-a5a1-4a8c-bfb4-96e818949bbe.jpg?1562444219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62019ac4-a5a1-4a8c-bfb4-96e818949bbe.jpg?1562444219"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quench", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee0ba01b-de96-4f8f-9405-ff3ad288afac.jpg?1589832153", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee0ba01b-de96-4f8f-9405-ff3ad288afac.jpg?1589832153"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakshasa's Disdain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd9e8a25-2e71-431b-897f-8b62520a3ce9.jpg?1562829343", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd9e8a25-2e71-431b-897f-8b62520a3ce9.jpg?1562829343"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rebuff the Wicked", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fa47fcce-d4c4-40a2-8853-6d7569d50926.jpg?1562586538", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fa47fcce-d4c4-40a2-8853-6d7569d50926.jpg?1562586538"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Red Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70a45e9b-699e-425a-9f3d-267274830d3e.jpg?1562436618", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70a45e9b-699e-425a-9f3d-267274830d3e.jpg?1562436618"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Red Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/776ad9be-3309-4f1d-9f27-6219d9477662.jpg?1559591383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/776ad9be-3309-4f1d-9f27-6219d9477662.jpg?1559591383"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Red Elemental Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cdd2a7c-001d-4891-8513-4b6d96968b35.jpg?1562545467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cdd2a7c-001d-4891-8513-4b6d96968b35.jpg?1562545467"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Reinterpret", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/765e64ae-699c-46bd-a8cc-c8c1075d644f.jpg?1625192562", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/765e64ae-699c-46bd-a8cc-c8c1075d644f.jpg?1625192562"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Reject", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d77f0731-fb40-4dc2-8530-afcb5ce1f27f.jpg?1624661968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d77f0731-fb40-4dc2-8530-afcb5ce1f27f.jpg?1624661968"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Remand", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0027e5ca-8046-40a0-bd73-79be55f28bff.jpg?1592754515", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0027e5ca-8046-40a0-bd73-79be55f28bff.jpg?1592754515"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Remand", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/581f3780-c480-48c6-b15c-1618f2feccb9.jpg?1598914434", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/581f3780-c480-48c6-b15c-1618f2feccb9.jpg?1598914434"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Remand", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d41e8cc0-4e05-412b-8ea3-d5b5c45da601.jpg?1562164467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d41e8cc0-4e05-412b-8ea3-d5b5c45da601.jpg?1562164467"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Remove Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f25f4f0e-bbf4-46b1-97fd-e796ff9e138f.jpg?1562251278", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f25f4f0e-bbf4-46b1-97fd-e796ff9e138f.jpg?1562251278"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Remove Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/d/fd6bbb81-b830-4b22-be9a-852d9edbda21.jpg?1562595434", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/d/fd6bbb81-b830-4b22-be9a-852d9edbda21.jpg?1562595434"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Remove Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63de147c-2e62-41b9-8ada-93406387f08b.jpg?1562859196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63de147c-2e62-41b9-8ada-93406387f08b.jpg?1562859196"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Remove Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/675440ff-9701-4310-a4ad-8502b9cb73ae.jpg?1561757323", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/675440ff-9701-4310-a4ad-8502b9cb73ae.jpg?1561757323"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Render Silent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3f3d6e4-0abe-4042-a7f6-0395683e8582.jpg?1562937631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3f3d6e4-0abe-4042-a7f6-0395683e8582.jpg?1562937631"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Render Silent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/4514a13f-5eee-49a8-876c-6b4befff4592.jpg?1561757030", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/4514a13f-5eee-49a8-876c-6b4befff4592.jpg?1561757030"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Repel Intruders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/38e64b09-1a58-4669-b7f2-baa3ccc85f2d.jpg?1568911006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/38e64b09-1a58-4669-b7f2-baa3ccc85f2d.jpg?1568911006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rethink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/915ae03f-22f3-4ecc-a875-5226d8dec384.jpg?1562921984", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/915ae03f-22f3-4ecc-a875-5226d8dec384.jpg?1562921984"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Revolutionary Rebuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6ea63dad-6afe-464e-ab19-fabd9709c6f9.jpg?1576381387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6ea63dad-6afe-464e-ab19-fabd9709c6f9.jpg?1576381387"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rewind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e51c4fb-fb29-4b1c-b78e-1fadf94fc9a5.jpg?1562928379", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e51c4fb-fb29-4b1c-b78e-1fadf94fc9a5.jpg?1562928379"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rites of Refusal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fa88f595-1b6f-4af0-bc50-bd07c8be431f.jpg?1562942139", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fa88f595-1b6f-4af0-bc50-bd07c8be431f.jpg?1562942139"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Runeboggle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37b2fb23-f8b5-4f83-9b29-b18507acaa1a.jpg?1593272065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37b2fb23-f8b5-4f83-9b29-b18507acaa1a.jpg?1593272065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rune Snag", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45b6cadf-1974-47c8-98d8-ba413486c3b5.jpg?1593275010", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45b6cadf-1974-47c8-98d8-ba413486c3b5.jpg?1593275010"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/counterspell3.json b/web/public/mtg/jsons/counterspell3.json new file mode 100644 index 00000000..97aeadc5 --- /dev/null +++ b/web/public/mtg/jsons/counterspell3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Rust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sage's Dousing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saw It Coming", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter Arc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scattering Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter to the Winds", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scent of Brine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Second Guess", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sinister Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Manipulation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Contortion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Counter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Crumple", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spelljack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Spell Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellshift", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Shrivel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Spell Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snip", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Spell Suck", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Swindle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Syphon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Split Decision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Squelch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Statute of Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Steel Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Stoic Rebuttal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stubborn Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stymied Hopes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sublime Epiphany", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Suffocating Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summary Dismissal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summoner's Bane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Swift Silence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syphon Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tale's End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Teferi's Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Temur Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Test of Talents", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Rebuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Collapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thwart", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tibalt's Trickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Time Stop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trap Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Traumatic Visions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trickbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undermine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undersimplify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Unified Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Unwind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Very Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Vex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Void Shatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Warping Wail", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wash Away", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Wild Ricochet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Withering Boon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wizard's Retort", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "You Find the Villains' Lair", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror1.json b/web/public/mtg/jsons/terror1.json new file mode 100644 index 00000000..4bbb9a03 --- /dev/null +++ b/web/public/mtg/jsons/terror1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Abrupt Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b1e92b4-6e53-4dba-a572-c67e01965ac5.jpg?1562785076", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b1e92b4-6e53-4dba-a572-c67e01965ac5.jpg?1562785076"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Abrupt Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b10ef54-368c-4841-ab5d-f2e8e1265c83.jpg?1561756631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b10ef54-368c-4841-ab5d-f2e8e1265c83.jpg?1561756631"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Active Volcano", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad402e65-6fac-4005-a2d4-592983df0c30.jpg?1584237356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad402e65-6fac-4005-a2d4-592983df0c30.jpg?1584237356"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aerial Assault", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64d9c182-cbb3-4791-90dd-0e533ddeebda.jpg?1592515927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64d9c182-cbb3-4791-90dd-0e533ddeebda.jpg?1592515927"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Aerial Predation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec3c023c-037e-495a-b7df-32be42a75f36.jpg?1562795050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec3c023c-037e-495a-b7df-32be42a75f36.jpg?1562795050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Afterlife", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fa2ecf9-b53c-4f1d-9028-ca3820d043cb.jpg?1562381856", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fa2ecf9-b53c-4f1d-9028-ca3820d043cb.jpg?1562381856"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Afterlife", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/4644694d-52e6-4d00-8cad-748899eeea84.jpg?1562718804", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/4644694d-52e6-4d00-8cad-748899eeea84.jpg?1562718804"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aftershock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91a26b2-03f8-43f0-a3a4-ff6c5a3690c4.jpg?1587857346", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91a26b2-03f8-43f0-a3a4-ff6c5a3690c4.jpg?1587857346"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Agonizing Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/539ac5e1-4bad-4f70-abac-e70c406bebec.jpg?1562912008", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/539ac5e1-4bad-4f70-abac-e70c406bebec.jpg?1562912008"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Angrath's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Annihilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a3bf039-ecf6-477e-997c-e32c55323c01.jpg?1562909994", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a3bf039-ecf6-477e-997c-e32c55323c01.jpg?1562909994"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Asphyxiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/894f3f5f-586d-45e4-9af7-4de80e44dfae.jpg?1593091866", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/894f3f5f-586d-45e4-9af7-4de80e44dfae.jpg?1593091866"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassinate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40b67839-622d-41c1-b9c7-1a26b021ec78.jpg?1562908402", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40b67839-622d-41c1-b9c7-1a26b021ec78.jpg?1562908402"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassin's Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b80e8fe0-eccb-4268-a6ce-1365c68e6b13.jpg?1562447376", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b80e8fe0-eccb-4268-a6ce-1365c68e6b13.jpg?1562447376"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Assassin's Ink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a926c10-029d-4e24-8c3f-1808025e30aa.jpg?1654567050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a926c10-029d-4e24-8c3f-1808025e30aa.jpg?1654567050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassin's Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f796e320-9898-45d4-9d7a-6d35de53c9ab.jpg?1562795619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f796e320-9898-45d4-9d7a-6d35de53c9ab.jpg?1562795619"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Avenging Arrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/696678ff-44dc-4fe4-bf17-024e86cd0220.jpg?1562787572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/696678ff-44dc-4fe4-bf17-024e86cd0220.jpg?1562787572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bake into a Pie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42a4d090-1bb7-4334-ab22-e2527391e79b.jpg?1572490064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42a4d090-1bb7-4334-ab22-e2527391e79b.jpg?1572490064"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast Within", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/76f8a300-44a8-4a70-93d1-64333c13f6f2.jpg?1592752271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/76f8a300-44a8-4a70-93d1-64333c13f6f2.jpg?1592752271"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Beast Within", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce5b6d19-22e3-4f57-8f4d-a17e982286c7.jpg?1562881648", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce5b6d19-22e3-4f57-8f4d-a17e982286c7.jpg?1562881648"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bedevil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81e2b96b-ecf2-4dd9-bc9d-3c46ee8c59e6.jpg?1584831400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81e2b96b-ecf2-4dd9-bc9d-3c46ee8c59e6.jpg?1584831400"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dfff5d3-1433-4a24-83e6-6361a446b974.jpg?1562758881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dfff5d3-1433-4a24-83e6-6361a446b974.jpg?1562758881"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c5db137-33b9-4cea-9193-4e637d2966f1.jpg?1562241441", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c5db137-33b9-4cea-9193-4e637d2966f1.jpg?1562241441"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f92cb48d-315b-4877-b615-ffdf275c4d61.jpg?1562947702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f92cb48d-315b-4877-b615-ffdf275c4d61.jpg?1562947702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Betrayal of Flesh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9e2e107-0277-4e5c-81a7-258bb2998f3e.jpg?1562153677", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9e2e107-0277-4e5c-81a7-258bb2998f3e.jpg?1562153677"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodchief's Thirst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/059e8447-6b1c-4651-a734-a8fea2cbf7b2.jpg?1604195360", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/059e8447-6b1c-4651-a734-a8fea2cbf7b2.jpg?1604195360"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blood Curdle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/4184c851-1419-476c-ba9c-9f0cb1137114.jpg?1591226609", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/4184c851-1419-476c-ba9c-9f0cb1137114.jpg?1591226609"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bone Shards", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1ee98955-4c47-4d45-9377-608dfa755337.jpg?1626095299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1ee98955-4c47-4d45-9377-608dfa755337.jpg?1626095299"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74780faa-1c64-4d73-8d09-53b47ba02d7a.jpg?1562922512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74780faa-1c64-4d73-8d09-53b47ba02d7a.jpg?1562922512"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/387eda28-f35b-48b0-ba59-773d82902327.jpg?1592708776", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/387eda28-f35b-48b0-ba59-773d82902327.jpg?1592708776"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4a4b3a3-b7ae-4210-8037-098fdf5808d0.jpg?1562709424", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4a4b3a3-b7ae-4210-8037-098fdf5808d0.jpg?1562709424"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brainspoil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c34fa44f-274e-4914-bbd5-71193f8d2f96.jpg?1598914670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c34fa44f-274e-4914-bbd5-71193f8d2f96.jpg?1598914670"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bright Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3340ffb9-9513-4551-ad64-821600596b2e.jpg?1562553092", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3340ffb9-9513-4551-ad64-821600596b2e.jpg?1562553092"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bring Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e18146f9-369c-41c8-8a1d-7737edd2c18e.jpg?1562940282", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e18146f9-369c-41c8-8a1d-7737edd2c18e.jpg?1562940282"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Broken Visage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/824823fb-5ae1-48b1-bc46-e452afa73cd8.jpg?1562592294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/824823fb-5ae1-48b1-bc46-e452afa73cd8.jpg?1562592294"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Broken Visage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9be199e7-feaa-4f23-b93c-3eab54a02e74.jpg?1562587775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9be199e7-feaa-4f23-b93c-3eab54a02e74.jpg?1562587775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9eb94908-4f4a-487e-87ac-8d5bdefe9983.jpg?1650029788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9eb94908-4f4a-487e-87ac-8d5bdefe9983.jpg?1650029788"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/6201f78e-ff45-4c59-ac85-c8447c14a496.jpg?1631050058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/6201f78e-ff45-4c59-ac85-c8447c14a496.jpg?1631050058"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0fc2dfd-85b0-4add-be18-b39549235921.jpg?1604198611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0fc2dfd-85b0-4add-be18-b39549235921.jpg?1604198611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cast Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/aba79021-39af-4e74-beb5-f2f508c865b2.jpg?1653520579", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/aba79021-39af-4e74-beb5-f2f508c865b2.jpg?1653520579"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Cast Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a41150b2-44a6-4e80-8b32-afc6ea744fb3.jpg?1591104816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a41150b2-44a6-4e80-8b32-afc6ea744fb3.jpg?1591104816"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Casualties of War", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08fc5e50-c6f7-41ec-815a-5667eefded78.jpg?1557577078", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08fc5e50-c6f7-41ec-815a-5667eefded78.jpg?1557577078"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Certain Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c67784b3-eb55-452e-b965-f63220b88896.jpg?1576384279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c67784b3-eb55-452e-b965-f63220b88896.jpg?1576384279"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chastise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/1169dab7-8f4c-474d-9289-42765a275376.jpg?1562628717", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/1169dab7-8f4c-474d-9289-42765a275376.jpg?1562628717"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chill to the Bone", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/312505d7-362e-43cf-bd23-28c248a8b7e1.jpg?1593275049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/312505d7-362e-43cf-bd23-28c248a8b7e1.jpg?1593275049"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cinder Cloud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clear a Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a8f904b-a9a3-4bae-9284-4e9cbe7592ee.jpg?1562920680", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a8f904b-a9a3-4bae-9284-4e9cbe7592ee.jpg?1562920680"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Closing Statement", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/785e6d07-fe40-4723-b963-02da0a0987c7.jpg?1627428302", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/785e6d07-fe40-4723-b963-02da0a0987c7.jpg?1627428302"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Collar the Culprit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdf305b7-d1f7-4770-9201-8f3fb6735cd9.jpg?1572892497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdf305b7-d1f7-4770-9201-8f3fb6735cd9.jpg?1572892497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Collective Effort", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d85a6369-c07f-47d5-8448-72d8ec7e7898.jpg?1576383801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d85a6369-c07f-47d5-8448-72d8ec7e7898.jpg?1576383801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Consign to the Pit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/09991fad-4282-4a17-bfb1-03eaa13502df.jpg?1584830536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/09991fad-4282-4a17-bfb1-03eaa13502df.jpg?1584830536"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Contract Killing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1f20feb-b1ed-4d80-bef9-f3cc44ffb7b0.jpg?1562564388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1f20feb-b1ed-4d80-bef9-f3cc44ffb7b0.jpg?1562564388"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corpsehatch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91c152d-1829-438c-b571-74361e09df62.jpg?1562708566", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91c152d-1829-438c-b571-74361e09df62.jpg?1562708566"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cradle to Grave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3ec275cf-bb4e-4de0-9184-4d53dd87dad3.jpg?1562569856", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3ec275cf-bb4e-4de0-9184-4d53dd87dad3.jpg?1562569856"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crosis's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a586e329-b1e2-4b60-a914-7b9aa2c645c2.jpg?1562929889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a586e329-b1e2-4b60-a914-7b9aa2c645c2.jpg?1562929889"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Crosis's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b59a9e75-9988-4040-a718-b1655fc20d11.jpg?1562933342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b59a9e75-9988-4040-a718-b1655fc20d11.jpg?1562933342"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cruel Cut", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f99ffe22-4dd8-4787-b6e0-e03dea8ab42a.jpg?1590010389", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f99ffe22-4dd8-4787-b6e0-e03dea8ab42a.jpg?1590010389"}, "reprint": true, "digital": true, "set_type": "memorabilia"}, {"name": "Cruel Revival", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a863ae27-a99a-4a60-ab07-25c1bacec64d.jpg?1562035297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a863ae27-a99a-4a60-ab07-25c1bacec64d.jpg?1562035297"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Cruel Revival", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/245aba23-2abb-4084-b4cb-d06e46de2108.jpg?1562903595", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/245aba23-2abb-4084-b4cb-d06e46de2108.jpg?1562903595"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eae67d98-5167-442b-8586-0b2bcb0c56eb.jpg?1643592488", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eae67d98-5167-442b-8586-0b2bcb0c56eb.jpg?1643592488"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0c4f213-0ea4-44c0-8429-172a317b77f5.jpg?1572893325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0c4f213-0ea4-44c0-8429-172a317b77f5.jpg?1572893325"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a66b0e45-e585-44f3-8d2b-e887330ba138.jpg?1562561563", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a66b0e45-e585-44f3-8d2b-e887330ba138.jpg?1562561563"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crushing Vines", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c59b3653-5a50-48f2-bcf1-ab305ef30902.jpg?1562941671", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c59b3653-5a50-48f2-bcf1-ab305ef30902.jpg?1562941671"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Damn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Daring Demolition", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6378898-50b7-47c9-8c25-dc660606be9f.jpg?1576381626", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6378898-50b7-47c9-8c25-dc660606be9f.jpg?1576381626"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d03720d-b0ca-4892-9ad1-52189f4a30a1.jpg?1562244108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d03720d-b0ca-4892-9ad1-52189f4a30a1.jpg?1562244108"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/922d6c8b-70ae-4db4-bf26-1904e4906211.jpg?1562055426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/922d6c8b-70ae-4db4-bf26-1904e4906211.jpg?1562055426"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f983dcb-b077-465f-a70b-6bd0e425556c.jpg?1562719738", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f983dcb-b077-465f-a70b-6bd0e425556c.jpg?1562719738"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7dc2716-ed62-4797-ad2b-227eca5408d0.jpg?1562941556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7dc2716-ed62-4797-ad2b-227eca5408d0.jpg?1562941556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Betrayal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56adf4ea-1b1c-4737-8574-1848ca47d4f3.jpg?1562818301", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56adf4ea-1b1c-4737-8574-1848ca47d4f3.jpg?1562818301"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Offering", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3ce0cef9-6de4-4a71-b76a-eb0198387294.jpg?1562909319", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3ce0cef9-6de4-4a71-b76a-eb0198387294.jpg?1562909319"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Dark Withering", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3da58e0d-5877-43c4-b129-993e154b6087.jpg?1562907804", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3da58e0d-5877-43c4-b129-993e154b6087.jpg?1562907804"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deadly Alliance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/007a5c8c-ed0b-4844-9393-a3d25d4ffa1d.jpg?1604195436", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/007a5c8c-ed0b-4844-9393-a3d25d4ffa1d.jpg?1604195436"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deadly Visit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/462fe190-5264-42d8-bd27-23c5aa0c641f.jpg?1572892937", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/462fe190-5264-42d8-bd27-23c5aa0c641f.jpg?1572892937"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Bomb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8a84715-c5dc-4a19-af6a-796c6ee912c2.jpg?1562947604", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8a84715-c5dc-4a19-af6a-796c6ee912c2.jpg?1562947604"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathmark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61268362-f2ba-469d-8e5a-0b8da96e54a5.jpg?1561982272", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61268362-f2ba-469d-8e5a-0b8da96e54a5.jpg?1561982272"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Deathmark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e72e8728-d0a0-4ee5-87c3-092ca94225e0.jpg?1593275062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e72e8728-d0a0-4ee5-87c3-092ca94225e0.jpg?1593275062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Mutation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c643d87-50bc-4380-b1d6-0a465eef5dbf.jpg?1562912876", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c643d87-50bc-4380-b1d6-0a465eef5dbf.jpg?1562912876"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Rattle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8cddafc8-57d6-456e-af58-4b7f45e195d5.jpg?1562923481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8cddafc8-57d6-456e-af58-4b7f45e195d5.jpg?1562923481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death's Caress", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/0643fb9a-8284-4dfc-836a-c2c69ef09f32.jpg?1562896472", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0643fb9a-8284-4dfc-836a-c2c69ef09f32.jpg?1562896472"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathsprout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d615557-aea8-4057-9fbd-d62dd98edc13.jpg?1557577090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d615557-aea8-4057-9fbd-d62dd98edc13.jpg?1557577090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/7478a471-3bd2-4038-a4eb-70c38a43afa9.jpg?1562596864", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/7478a471-3bd2-4038-a4eb-70c38a43afa9.jpg?1562596864"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decimate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/358bbaf9-8d48-448b-b87f-211344e36e29.jpg?1562864952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/358bbaf9-8d48-448b-b87f-211344e36e29.jpg?1562864952"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Decimate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/912c398a-e49a-4399-ac41-7b1d4328a59d.jpg?1562921956", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/912c398a-e49a-4399-ac41-7b1d4328a59d.jpg?1562921956"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deface", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43df9f41-944e-4cf3-ac80-524eadac221d.jpg?1584830848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43df9f41-944e-4cf3-ac80-524eadac221d.jpg?1584830848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/60473300-0bdc-4e89-87d9-28c8d7b4d83d.jpg?1562787158", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60473300-0bdc-4e89-87d9-28c8d7b4d83d.jpg?1562787158"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defend the Campus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85e4e1b5-77d6-4af4-b22e-6f6b4d129f5d.jpg?1624589309", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85e4e1b5-77d6-4af4-b22e-6f6b4d129f5d.jpg?1624589309"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defenestrate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df3abdcc-83a8-45c3-9bfd-23f929705018.jpg?1634349688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df3abdcc-83a8-45c3-9bfd-23f929705018.jpg?1634349688"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devour in Shadow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98c80584-b7b5-4dcd-8a00-812b9dd9b1b9.jpg?1562878693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98c80584-b7b5-4dcd-8a00-812b9dd9b1b9.jpg?1562878693"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Disembowel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1edb79d-0031-4dc6-8881-f6d1fe4acba2.jpg?1619741469", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1edb79d-0031-4dc6-8881-f6d1fe4acba2.jpg?1619741469"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed07b708-7232-4b87-b5d9-edaa20a69293.jpg?1555039673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed07b708-7232-4b87-b5d9-edaa20a69293.jpg?1555039673"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79f46ac0-9e2f-4f9f-beee-0a7914475ac1.jpg?1562820257", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79f46ac0-9e2f-4f9f-beee-0a7914475ac1.jpg?1562820257"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48444e14-c73b-47d1-9c55-0ff4dc3c6034.jpg?1561978713", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48444e14-c73b-47d1-9c55-0ff4dc3c6034.jpg?1561978713"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/176cdb4b-6ad4-4991-8456-28579640063d.jpg?1562229273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/176cdb4b-6ad4-4991-8456-28579640063d.jpg?1562229273"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e19acff-f3dd-417a-a9ab-ea3e36c1ba61.jpg?1561983934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e19acff-f3dd-417a-a9ab-ea3e36c1ba61.jpg?1561983934"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7e6c0fe2-a82b-42cb-8629-b9f00b7f08e9.jpg?1623780045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7e6c0fe2-a82b-42cb-8629-b9f00b7f08e9.jpg?1623780045"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37468ade-27b1-4128-9a62-1293ec2aab41.jpg?1561756922", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37468ade-27b1-4128-9a62-1293ec2aab41.jpg?1561756922"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Drag to the Underworld", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91852444-9361-4588-a44f-fb90ba1b30e5.jpg?1581479732", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91852444-9361-4588-a44f-fb90ba1b30e5.jpg?1581479732"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dreadbore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a83945c6-4dc6-4d9a-9bc2-2d4a264e5422.jpg?1562791208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a83945c6-4dc6-4d9a-9bc2-2d4a264e5422.jpg?1562791208"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Duh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fa5b9b30-4950-4c9c-9ce8-6d271bb7aa01.jpg?1562489857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fa5b9b30-4950-4c9c-9ce8-6d271bb7aa01.jpg?1562489857"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Easy Prey", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/312fb6e4-1eb1-4fbb-b7a4-125829a6e96a.jpg?1591226769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/312fb6e4-1eb1-4fbb-b7a4-125829a6e96a.jpg?1591226769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eaten by Spiders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0efea1b1-f212-4b97-98dd-922f85ab191f.jpg?1592709344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0efea1b1-f212-4b97-98dd-922f85ab191f.jpg?1592709344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eightfold Maze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc8c377a-82c4-46ee-94c2-b970160a3205.jpg?1562257975", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc8c377a-82c4-46ee-94c2-b970160a3205.jpg?1562257975"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Eliminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8eb4087-3a4c-4de8-8e29-f4cd71acb180.jpg?1594736106", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8eb4087-3a4c-4de8-8e29-f4cd71acb180.jpg?1594736106"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Eliminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c55b2b50-ac83-4a78-8f84-580193d1ca0f.jpg?1623780234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c55b2b50-ac83-4a78-8f84-580193d1ca0f.jpg?1623780234"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Enduring Victory", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54fef763-7ee2-4341-9c67-546e4b6710b7.jpg?1562786446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54fef763-7ee2-4341-9c67-546e4b6710b7.jpg?1562786446"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Vortex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe07e496-5070-4116-a91a-a3bbe19c12af.jpg?1562942896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe07e496-5070-4116-a91a-a3bbe19c12af.jpg?1562942896"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eviscerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62ba90b8-3a30-4058-b8d3-72900b1f4fe0.jpg?1562736723", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62ba90b8-3a30-4058-b8d3-72900b1f4fe0.jpg?1562736723"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Execute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/333123bc-fb66-4b5a-bf55-045d2906c8c3.jpg?1562904481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/333123bc-fb66-4b5a-bf55-045d2906c8c3.jpg?1562904481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Expunge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/0576ffe8-a7b9-479b-8ea0-418b430b1aa1.jpg?1562896134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/0576ffe8-a7b9-479b-8ea0-418b430b1aa1.jpg?1562896134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eyeblight's Ending", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0c08701-7038-4d6b-bbf8-056fd8ffb226.jpg?1562371343", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0c08701-7038-4d6b-bbf8-056fd8ffb226.jpg?1562371343"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/044dc7c2-6198-4526-b79a-f3d8ee7a157a.jpg?1562799109", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/044dc7c2-6198-4526-b79a-f3d8ee7a157a.jpg?1562799109"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5e81649-9954-424c-89d1-f87d73b66047.jpg?1595869185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5e81649-9954-424c-89d1-f87d73b66047.jpg?1595869185"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/5427d8a6-ac9e-4e50-bd39-81713b2ade25.jpg?1607041515", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/5427d8a6-ac9e-4e50-bd39-81713b2ade25.jpg?1607041515"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9a50516-a20f-4e6e-b4f2-0049b673f942.jpg?1599711004", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9a50516-a20f-4e6e-b4f2-0049b673f942.jpg?1599711004"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86d1119d-7585-4699-8649-e3743c02d7a9.jpg?1562636837", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86d1119d-7585-4699-8649-e3743c02d7a9.jpg?1562636837"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Fateful Absence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/eca8d6f8-c6f1-437c-99e2-4281eae14a6f.jpg?1634346819", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/eca8d6f8-c6f1-437c-99e2-4281eae14a6f.jpg?1634346819"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1a7dd5e2-b2a5-46ab-a67c-499451706505.jpg?1562610240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1a7dd5e2-b2a5-46ab-a67c-499451706505.jpg?1562610240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/658bf8b7-fbc4-4046-9300-249cdeb87924.jpg?1561757312", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/658bf8b7-fbc4-4046-9300-249cdeb87924.jpg?1561757312"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Feast of Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de07e21e-c12a-47a6-ad2c-ef6fed343407.jpg?1593095705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de07e21e-c12a-47a6-ad2c-ef6fed343407.jpg?1593095705"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92105bc6-b64a-4bdc-99fe-7a2ccdbd4486.jpg?1592713797", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92105bc6-b64a-4bdc-99fe-7a2ccdbd4486.jpg?1592713797"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/302ec21d-bb10-4651-80da-11852768165d.jpg?1559592569", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/302ec21d-bb10-4651-80da-11852768165d.jpg?1559592569"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c185b4d-8da5-4b8a-85f0-5f0622c7bade.jpg?1562769209", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c185b4d-8da5-4b8a-85f0-5f0622c7bade.jpg?1562769209"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feed the Swarm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6b2eba7-862a-4efd-9f65-065fb2070855.jpg?1604195649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6b2eba7-862a-4efd-9f65-065fb2070855.jpg?1604195649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fierce Retribution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/9597b163-5c6b-4f64-b1f1-5f1fa2e23e5d.jpg?1643586258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/9597b163-5c6b-4f64-b1f1-5f1fa2e23e5d.jpg?1643586258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Payment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49a21a8f-9c7b-4ae8-8635-f2ee2151c8de.jpg?1584831505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49a21a8f-9c7b-4ae8-8635-f2ee2151c8de.jpg?1584831505"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Finders, Keepers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f4b7148-e98f-40a4-95e3-ffdd2daa324b.jpg?1562914921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f4b7148-e98f-40a4-95e3-ffdd2daa324b.jpg?1562914921"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Finishing Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b85a552-2119-4d9c-b7c1-c09c2d9f2f38.jpg?1594736130", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b85a552-2119-4d9c-b7c1-c09c2d9f2f38.jpg?1594736130"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Fissure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa2d778d-d74b-45ec-a86b-5d52ffad6ba5.jpg?1562935207", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa2d778d-d74b-45ec-a86b-5d52ffad6ba5.jpg?1562935207"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flash Flood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5ae88c06-f28c-4fbc-a28c-5eb203a04722.jpg?1562859177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5ae88c06-f28c-4fbc-a28c-5eb203a04722.jpg?1562859177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flesh Allergy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c729525-b954-42dd-9877-f4360d99b961.jpg?1562820900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c729525-b954-42dd-9877-f4360d99b961.jpg?1562820900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flesh to Dust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16b2e842-6c92-47b0-bed4-e0e64485f168.jpg?1562783120", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16b2e842-6c92-47b0-bed4-e0e64485f168.jpg?1562783120"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Foul Play", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e4b75c-e993-4983-8933-977be314bba6.jpg?1634349812", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e4b75c-e993-4983-8933-977be314bba6.jpg?1634349812"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fumarole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa53e9a-0d7c-4d17-b2be-56930edfa2c2.jpg?1562940031", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa53e9a-0d7c-4d17-b2be-56930edfa2c2.jpg?1562940031"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gang Up", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10d01449-3e4e-44ef-90aa-9489c86c57df.jpg?1595438095", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10d01449-3e4e-44ef-90aa-9489c86c57df.jpg?1595438095"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Get the Point", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/821c4ab5-eb75-445a-bbec-e50af54dba7a.jpg?1584831541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/821c4ab5-eb75-445a-bbec-e50af54dba7a.jpg?1584831541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghastly Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9d2bfa3-0499-43ea-a76d-b12fddbc104e.jpg?1562935702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9d2bfa3-0499-43ea-a76d-b12fddbc104e.jpg?1562935702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghostly Visit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/06f6938a-229a-4521-b5d5-7999ce5fb372.jpg?1562255824", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/06f6938a-229a-4521-b5d5-7999ce5fb372.jpg?1562255824"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Gloomlance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b45bfb2-7c48-4da5-a0fd-29d353221814.jpg?1562832072", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b45bfb2-7c48-4da5-a0fd-29d353221814.jpg?1562832072"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gloomwidow's Feast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59b989b4-692c-4ccb-a290-0ff00abacba9.jpg?1562830513", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59b989b4-692c-4ccb-a290-0ff00abacba9.jpg?1562830513"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Go for the Throat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c6cb231-41df-409c-923e-100319f27ee3.jpg?1562605365", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c6cb231-41df-409c-923e-100319f27ee3.jpg?1562605365"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Go for the Throat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3109aaa-b1e9-4c68-85f0-7515c8eeadc3.jpg?1562636862", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3109aaa-b1e9-4c68-85f0-7515c8eeadc3.jpg?1562636862"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Grim Bounty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b98e0ab1-dea8-492b-a712-2057f2b1d020.jpg?1627704924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b98e0ab1-dea8-492b-a712-2057f2b1d020.jpg?1627704924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grisly Ritual", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/53cdf2ab-3acd-49bd-8273-84c1cfc92883.jpg?1643589817", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/53cdf2ab-3acd-49bd-8273-84c1cfc92883.jpg?1643589817"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grisly Spectacle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c26d0f6e-e7bd-4206-a0da-1c9c203a73f2.jpg?1561844583", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c26d0f6e-e7bd-4206-a0da-1c9c203a73f2.jpg?1561844583"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guiding Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd552f81-1947-47e0-beee-f04e73551055.jpg?1653690524", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd552f81-1947-47e0-beee-f04e73551055.jpg?1653690524"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Hand of Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9761136-9e1c-4d86-98ce-7abe1d8e6a8d.jpg?1562935064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9761136-9e1c-4d86-98ce-7abe1d8e6a8d.jpg?1562935064"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hand of Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27f136b8-52be-49b9-919b-2b9785254350.jpg?1546740328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27f136b8-52be-49b9-919b-2b9785254350.jpg?1546740328"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Hearth Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/caa9ac66-51b7-4aec-92dc-0f0656b0f7fe.jpg?1562278639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/caa9ac66-51b7-4aec-92dc-0f0656b0f7fe.jpg?1562278639"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Heartless Act", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4e6794a-feeb-4fc8-a2ee-38c75c18aaae.jpg?1591226819", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4e6794a-feeb-4fc8-a2ee-38c75c18aaae.jpg?1591226819"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d22dd514-814f-4a62-926d-fef311896c02.jpg?1562879959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d22dd514-814f-4a62-926d-fef311896c02.jpg?1562879959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1b0751e-3a7e-4568-8c64-7429d6829687.jpg?1643589948", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1b0751e-3a7e-4568-8c64-7429d6829687.jpg?1643589948"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/596822f6-dbd4-4cc8-aa50-9331ff42544e.jpg?1562818494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/596822f6-dbd4-4cc8-aa50-9331ff42544e.jpg?1562818494"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed96b05d-b2ca-4c8f-969b-cac9b4562fab.jpg?1636900809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed96b05d-b2ca-4c8f-969b-cac9b4562fab.jpg?1636900809"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64aa5cbd-98e9-46fc-8de4-64eab7afc90f.jpg?1561757293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64aa5cbd-98e9-46fc-8de4-64eab7afc90f.jpg?1561757293"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hideous End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b33e6056-00c9-4731-b364-b0214398848d.jpg?1562842860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b33e6056-00c9-4731-b364-b0214398848d.jpg?1562842860"}, "reprint": false, "digital": false, "set_type": "planechase"}, {"name": "Horobi's Whisper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1aad5179-4b73-498e-85c5-1fc363d26223.jpg?1562875751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1aad5179-4b73-498e-85c5-1fc363d26223.jpg?1562875751"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Human Frailty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d1de712-86ac-4c03-be86-2403cd121f66.jpg?1592708908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d1de712-86ac-4c03-be86-2403cd121f66.jpg?1592708908"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Humble the Brute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c105686-8b45-494a-b9ef-8aa267bb1b5a.jpg?1656286373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c105686-8b45-494a-b9ef-8aa267bb1b5a.jpg?1656286373"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f468338-bb66-4db0-a883-69095566092b.jpg?1562904646", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f468338-bb66-4db0-a883-69095566092b.jpg?1562904646"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0ddbcd23-e206-4a12-968a-3854693d1e60.jpg?1562870987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0ddbcd23-e206-4a12-968a-3854693d1e60.jpg?1562870987"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Impale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfa0c4f7-3497-467d-9453-104fb4b5a0f3.jpg?1555040252", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfa0c4f7-3497-467d-9453-104fb4b5a0f3.jpg?1555040252"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infernal Grasp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/17824929-f131-4b8d-addb-66c25323155e.jpg?1634349911", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/17824929-f131-4b8d-addb-66c25323155e.jpg?1634349911"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inscription of Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/93612079-0b8d-489d-9ae1-3593414a8cee.jpg?1604195857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/93612079-0b8d-489d-9ae1-3593414a8cee.jpg?1604195857"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Into the Maw of Hell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d188d9b-7a12-4eaf-855b-af4f0204dc5a.jpg?1562830878", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d188d9b-7a12-4eaf-855b-af4f0204dc5a.jpg?1562830878"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Just Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6e5e572-030d-4a41-89e6-e720b49bc131.jpg?1562934537", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6e5e572-030d-4a41-89e6-e720b49bc131.jpg?1562934537"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Kaervek's Purge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kill! Destroy!", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49dd5a66-101d-4f88-b1ba-e2368203d408.jpg?1605097368", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49dd5a66-101d-4f88-b1ba-e2368203d408.jpg?1605097368"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Killing Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7a4d87d-b844-4f20-8b14-4fd32c53dea5.jpg?1561852883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7a4d87d-b844-4f20-8b14-4fd32c53dea5.jpg?1561852883"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kill Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b0b9a3-8f50-4fba-9978-409f3369afa6.jpg?1650026094", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b0b9a3-8f50-4fba-9978-409f3369afa6.jpg?1650026094"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Kill Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f30d4136-78a3-4760-83af-d365cc97d118.jpg?1562795914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f30d4136-78a3-4760-83af-d365cc97d118.jpg?1562795914"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror2.json b/web/public/mtg/jsons/terror2.json new file mode 100644 index 00000000..162e6ad0 --- /dev/null +++ b/web/public/mtg/jsons/terror2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Krovikan Rot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/17597c66-0d9f-41af-9160-0d92be88f450.jpg?1593275116", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/17597c66-0d9f-41af-9160-0d92be88f450.jpg?1593275116"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Launch Party", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/53f29821-902e-41bc-97a2-6fc7a710cbdb.jpg?1562786438", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/53f29821-902e-41bc-97a2-6fc7a710cbdb.jpg?1562786438"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Flow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89e825e4-98be-49f0-bc5e-c8988118dcef.jpg?1562446890", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89e825e4-98be-49f0-bc5e-c8988118dcef.jpg?1562446890"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Legion's Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/385bea20-c196-4da8-bc3e-36f8d50dcc17.jpg?1562553483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/385bea20-c196-4da8-bc3e-36f8d50dcc17.jpg?1562553483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lethal Scheme", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65864680-9520-4eb3-9774-fa478e54a290.jpg?1650411151", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65864680-9520-4eb3-9774-fa478e54a290.jpg?1650411151"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Lethal Sting", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eaded6bf-2db7-4b1d-93cc-4b7b571cd2de.jpg?1562819094", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eaded6bf-2db7-4b1d-93cc-4b7b571cd2de.jpg?1562819094"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lich's Caress", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32bd3acd-aa62-4708-9336-e3430fd0e541.jpg?1562301277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32bd3acd-aa62-4708-9336-e3430fd0e541.jpg?1562301277"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Liliana's Defeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f72b028-b9df-40c7-822f-4acc6bdcc719.jpg?1562789479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f72b028-b9df-40c7-822f-4acc6bdcc719.jpg?1562789479"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Liliana's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b231f941-4acb-46f2-81ae-16e5a28e65af.jpg?1596250190", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b231f941-4acb-46f2-81ae-16e5a28e65af.jpg?1596250190"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Liturgy of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/3532105d-c550-4c20-8465-a6a19169efbd.jpg?1562827834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/3532105d-c550-4c20-8465-a6a19169efbd.jpg?1562827834"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb651c3a-cb27-4b73-8eb6-b87d65211097.jpg?1562644898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb651c3a-cb27-4b73-8eb6-b87d65211097.jpg?1562644898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2d85423-ebd8-4a6e-aedf-90e52f918764.jpg?1562940541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2d85423-ebd8-4a6e-aedf-90e52f918764.jpg?1562940541"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d351c901-103b-460f-9d01-6e4d4b25cac8.jpg?1561929932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d351c901-103b-460f-9d01-6e4d4b25cac8.jpg?1561929932"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Mage Hunters' Onslaught", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed85140f-f0e0-4ac1-a67f-26d17ff95e31.jpg?1624591129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed85140f-f0e0-4ac1-a67f-26d17ff95e31.jpg?1624591129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Malicious Affliction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6ea704f-a06c-4d3b-80a3-d23f739c74aa.jpg?1561960653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6ea704f-a06c-4d3b-80a3-d23f739c74aa.jpg?1561960653"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Misfortune's Gain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/80abd7c1-8f7a-4279-b76f-251a02624345.jpg?1562257029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/80abd7c1-8f7a-4279-b76f-251a02624345.jpg?1562257029"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Mob", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c216e13-3779-4734-b481-9aad7aba9925.jpg?1562201673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c216e13-3779-4734-b481-9aad7aba9925.jpg?1562201673"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Molten Frame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58356504-e28e-456c-b1d3-e6232f4d78a6.jpg?1562801105", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58356504-e28e-456c-b1d3-e6232f4d78a6.jpg?1562801105"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/38c5e2e8-b781-4265-bce1-98fa25ddd8c3.jpg?1592714339", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/38c5e2e8-b781-4265-bce1-98fa25ddd8c3.jpg?1592714339"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b2c5187-71c7-4801-8a76-339c67322d35.jpg?1593272729", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b2c5187-71c7-4801-8a76-339c67322d35.jpg?1593272729"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/645f4d28-68cb-4386-91b9-c748930d69fa.jpg?1570573674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/645f4d28-68cb-4386-91b9-c748930d69fa.jpg?1570573674"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a36a42b0-8216-4c99-a85f-22a520f31fd4.jpg?1561757738", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a36a42b0-8216-4c99-a85f-22a520f31fd4.jpg?1561757738"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdef7fea-2bd0-42a2-96f6-6def18bd7f0c.jpg?1653725816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdef7fea-2bd0-42a2-96f6-6def18bd7f0c.jpg?1653725816"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c13ac76-7cd9-456f-9b89-92bfa07c64c5.jpg?1649362504", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c13ac76-7cd9-456f-9b89-92bfa07c64c5.jpg?1649362504"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f2eb849-b3ab-4d26-86c5-235c8161cf2a.jpg?1576384369", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f2eb849-b3ab-4d26-86c5-235c8161cf2a.jpg?1576384369"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8676f02-cf1e-4d40-a0c5-6e5a97417898.jpg?1562559978", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8676f02-cf1e-4d40-a0c5-6e5a97417898.jpg?1562559978"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Murderous Compulsion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33b94db1-ac8c-4667-81d5-408df0f30879.jpg?1576384534", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33b94db1-ac8c-4667-81d5-408df0f30879.jpg?1576384534"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murderous Cut", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b2dadff2-883f-4134-a881-be145cdcbd84.jpg?1562792142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b2dadff2-883f-4134-a881-be145cdcbd84.jpg?1562792142"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murderous Spoils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91ece344-c516-449e-ab7c-2e78d4778f02.jpg?1562638187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91ece344-c516-449e-ab7c-2e78d4778f02.jpg?1562638187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mutual Destruction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85ac0b25-80bf-4871-a6f6-5cf4d5b9496e.jpg?1591226898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85ac0b25-80bf-4871-a6f6-5cf4d5b9496e.jpg?1591226898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mythos of Nethroi", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6abc24e1-e721-471a-9efd-547f320675b0.jpg?1591226925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6abc24e1-e721-471a-9efd-547f320675b0.jpg?1591226925"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Neck Snap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc326b79-363e-4c14-86e4-23041f2d6b4f.jpg?1562375861", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc326b79-363e-4c14-86e4-23041f2d6b4f.jpg?1562375861"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Noxious Grasp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e5758cc-1f84-455d-a983-8ec471727eaf.jpg?1592516744", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e5758cc-1f84-455d-a983-8ec471727eaf.jpg?1592516744"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Orim's Thunder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d00bf192-4baf-46ba-947b-a22d07635b04.jpg?1562944526", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d00bf192-4baf-46ba-947b-a22d07635b04.jpg?1562944526"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Orzhov Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8ca44265-5e1b-4fbf-9002-52b2ce9b7448.jpg?1561835927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8ca44265-5e1b-4fbf-9002-52b2ce9b7448.jpg?1561835927"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parting Thoughts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e60b5a1-923c-4c67-ae06-2a498dc46506.jpg?1562393855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e60b5a1-923c-4c67-ae06-2a498dc46506.jpg?1562393855"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41369848-ba9a-40ef-931e-1a65bc979209.jpg?1562434966", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41369848-ba9a-40ef-931e-1a65bc979209.jpg?1562434966"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af7a2719-7910-4601-be88-7b3c249199d3.jpg?1562932043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af7a2719-7910-4601-be88-7b3c249199d3.jpg?1562932043"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb14d3f4-09f3-4113-bdc3-0fd753137f7c.jpg?1562942983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb14d3f4-09f3-4113-bdc3-0fd753137f7c.jpg?1562942983"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1f3e1c9-bfad-49a1-b171-6fa344ef2eef.jpg?1562447361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1f3e1c9-bfad-49a1-b171-6fa344ef2eef.jpg?1562447361"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Phthisis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9ba55f16-a37c-4caa-9417-227a06cf4061.jpg?1562927843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9ba55f16-a37c-4caa-9417-227a06cf4061.jpg?1562927843"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pinion Feast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45d6df03-c3c3-42c3-85a4-6fccb0741592.jpg?1562785514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45d6df03-c3c3-42c3-85a4-6fccb0741592.jpg?1562785514"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pistus Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1a2918d6-50f7-4bc1-aef2-930a5c84be8d.jpg?1562609919", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1a2918d6-50f7-4bc1-aef2-930a5c84be8d.jpg?1562609919"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pitfall Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/2823d9a5-dd2f-4e6a-8e3d-554c4204aa32.jpg?1562610754", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/2823d9a5-dd2f-4e6a-8e3d-554c4204aa32.jpg?1562610754"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Spores", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d106d56-a688-49cc-8d5d-0279a5a7c0a7.jpg?1562897663", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d106d56-a688-49cc-8d5d-0279a5a7c0a7.jpg?1562897663"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/5469e696-bbf1-43e3-9c25-fe089b36caed.jpg?1636224615", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/5469e696-bbf1-43e3-9c25-fe089b36caed.jpg?1636224615"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4be85ceb-be98-43ce-9565-a72990797437.jpg?1627708161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4be85ceb-be98-43ce-9565-a72990797437.jpg?1627708161"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d884b2f2-946e-4d5d-b8cf-ef035726a188.jpg?1591227840", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d884b2f2-946e-4d5d-b8cf-ef035726a188.jpg?1591227840"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8b2f186-4e04-49cb-a206-257cfb7e9361.jpg?1581480847", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8b2f186-4e04-49cb-a206-257cfb7e9361.jpg?1581480847"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54a0afaa-f99f-4c7a-9fa1-c6a46dfb2a29.jpg?1561480279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54a0afaa-f99f-4c7a-9fa1-c6a46dfb2a29.jpg?1561480279"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f6acb5b-b087-4cad-b40f-2de37029847c.jpg?1562917482", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f6acb5b-b087-4cad-b40f-2de37029847c.jpg?1562917482"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a67bb585-cc4f-4cbc-9a5a-d31df98c07ae.jpg?1562930081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a67bb585-cc4f-4cbc-9a5a-d31df98c07ae.jpg?1562930081"}, "reprint": false, "digital": false, "set_type": "archenemy"}, {"name": "Poison Arrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b7b5f34-c250-484e-9bae-94789b2a87fb.jpg?1562256571", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b7b5f34-c250-484e-9bae-94789b2a87fb.jpg?1562256571"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Poison the Cup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fb94456-5266-47db-b514-a0e17e34b771.jpg?1631048334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fb94456-5266-47db-b514-a0e17e34b771.jpg?1631048334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Polymorph", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fbae8702-a152-4c53-8a76-691a221f2475.jpg?1562722872", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fbae8702-a152-4c53-8a76-691a221f2475.jpg?1562722872"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pongify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cce74a84-4441-4f2e-89d8-df0b096790ed.jpg?1562582099", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cce74a84-4441-4f2e-89d8-df0b096790ed.jpg?1562582099"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Power Word Kill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/395b6ce4-143f-4eed-b565-98aa3d6208ef.jpg?1627705234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/395b6ce4-143f-4eed-b565-98aa3d6208ef.jpg?1627705234"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Power Word Kill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36c71043-1c11-4377-ab33-41d19927143a.jpg?1654010561", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36c71043-1c11-4377-ab33-41d19927143a.jpg?1654010561"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Premature Burial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e96cea6a-fea6-4a6b-84b2-7b57237be96a.jpg?1562944222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e96cea6a-fea6-4a6b-84b2-7b57237be96a.jpg?1562944222"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Price of Fame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b52152-0f7c-4466-9e49-033477028f67.jpg?1572893038", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b52152-0f7c-4466-9e49-033477028f67.jpg?1572893038"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prismatic Wardrobe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79624ebe-7110-486d-82ff-b64c662dc6de.jpg?1593865843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79624ebe-7110-486d-82ff-b64c662dc6de.jpg?1593865843"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Public Execution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48188942-d0ba-4503-bd75-c7a5329bb7c8.jpg?1562553248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48188942-d0ba-4503-bd75-c7a5329bb7c8.jpg?1562553248"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Puncturing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5b101264-4994-43b7-9156-228f7d10d2bd.jpg?1576383877", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5b101264-4994-43b7-9156-228f7d10d2bd.jpg?1576383877"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Puncturing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e52d260a-e1ca-4228-855e-2e104b86fd6c.jpg?1562709696", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e52d260a-e1ca-4228-855e-2e104b86fd6c.jpg?1562709696"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Purge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdcbe727-81f0-469e-92f1-0dd9acdb54ea.jpg?1562639281", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdcbe727-81f0-469e-92f1-0dd9acdb54ea.jpg?1562639281"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d43a0b6-2a5c-4959-96ee-6e570949dfed.jpg?1562897570", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d43a0b6-2a5c-4959-96ee-6e570949dfed.jpg?1562897570"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c0aca3e-d91d-4bb7-ba4a-500d93f71718.jpg?1592713790", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c0aca3e-d91d-4bb7-ba4a-500d93f71718.jpg?1592713790"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0a16086c-5a74-45d0-8b38-e832cfbc80f7.jpg?1598917276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0a16086c-5a74-45d0-8b38-e832cfbc80f7.jpg?1598917276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/3882ebea-2864-40ef-a21d-6ba80a0bd417.jpg?1624065750", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/3882ebea-2864-40ef-a21d-6ba80a0bd417.jpg?1624065750"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/236f46d9-276b-4418-a959-39b0963fc525.jpg?1561756786", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/236f46d9-276b-4418-a959-39b0963fc525.jpg?1561756786"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiant's Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28d2718e-c6fc-4961-b094-11f25f1177ff.jpg?1562862779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28d2718e-c6fc-4961-b094-11f25f1177ff.jpg?1562862779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rapid Hybridization", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83557f55-f1ab-4995-9cc1-37be895a59db.jpg?1561834181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83557f55-f1ab-4995-9cc1-37be895a59db.jpg?1561834181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reach of Shadows", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbf9a803-473a-4c38-b352-d47c4fd93d5e.jpg?1562829283", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbf9a803-473a-4c38-b352-d47c4fd93d5e.jpg?1562829283"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reave Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce7ff657-aa44-4336-895a-87518159cef6.jpg?1572490229", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce7ff657-aa44-4336-895a-87518159cef6.jpg?1572490229"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Reave Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db3d5e9d-07e8-43e1-aaf0-1f9e4ed2834a.jpg?1562045144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db3d5e9d-07e8-43e1-aaf0-1f9e4ed2834a.jpg?1562045144"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rebuke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/267185ac-a176-423e-a7f8-ee966d1d9a1e.jpg?1562827636", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/267185ac-a176-423e-a7f8-ee966d1d9a1e.jpg?1562827636"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Regicide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07f56287-91e0-418f-8b57-35c6c30cee33.jpg?1576381853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07f56287-91e0-418f-8b57-35c6c30cee33.jpg?1576381853"}, "reprint": false, "frame_effects": ["draft"], "digital": false, "set_type": "draft_innovation"}, {"name": "Reign of Chaos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/9285b14a-fc8e-457a-b803-202e05be41e5.jpg?1562720487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/9285b14a-fc8e-457a-b803-202e05be41e5.jpg?1562720487"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rend Flesh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92b300a3-e6a8-4ca9-bb26-03f57b5ff6ec.jpg?1562762516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92b300a3-e6a8-4ca9-bb26-03f57b5ff6ec.jpg?1562762516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/343baad1-dd58-4d64-9b0a-258618094ceb.jpg?1593095328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/343baad1-dd58-4d64-9b0a-258618094ceb.jpg?1593095328"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/3868f7ff-8a84-4153-bf5a-ff001d34e0f0.jpg?1562235914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/3868f7ff-8a84-4153-bf5a-ff001d34e0f0.jpg?1562235914"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/179f50be-6658-42f4-b9b9-c97c7d3f239a.jpg?1562768219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/179f50be-6658-42f4-b9b9-c97c7d3f239a.jpg?1562768219"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/839df85a-1aca-4d4b-b327-2778caa6d289.jpg?1562769214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/839df85a-1aca-4d4b-b327-2778caa6d289.jpg?1562769214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Return to the Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95a53144-2ef3-47d9-a176-73d620202df6.jpg?1562827827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95a53144-2ef3-47d9-a176-73d620202df6.jpg?1562827827"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ride Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c021868f-9ab8-4a52-b12e-3cc35c9d67f0.jpg?1576385014", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c021868f-9ab8-4a52-b12e-3cc35c9d67f0.jpg?1576385014"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ride Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bc9a434-9617-4a20-88f0-355b20f2c538.jpg?1562785134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bc9a434-9617-4a20-88f0-355b20f2c538.jpg?1562785134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rite of the Serpent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/005b9fec-66de-4079-88e0-c7de7e22d18e.jpg?1562781741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/005b9fec-66de-4079-88e0-c7de7e22d18e.jpg?1562781741"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ruinous Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/709ab9cf-eed8-4d73-b10d-c7f6d8750328.jpg?1562921535", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/709ab9cf-eed8-4d73-b10d-c7f6d8750328.jpg?1562921535"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ruinous Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a41a241-ee56-486a-9b4d-fb355b5f65b2.jpg?1562133050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a41a241-ee56-486a-9b4d-fb355b5f65b2.jpg?1562133050"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sagittars' Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saltblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/edd1833d-64b0-4c9b-8f6b-1cf15c29d473.jpg?1562585578", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/edd1833d-64b0-4c9b-8f6b-1cf15c29d473.jpg?1562585578"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saw in Half", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05e6a7bc-a35a-4e68-99a0-be264553b5de.jpg?1638258467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05e6a7bc-a35a-4e68-99a0-be264553b5de.jpg?1638258467"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Scorch the Fields", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05c4338d-e5c0-46b4-ab16-1f9aa97b4026.jpg?1562896337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05c4338d-e5c0-46b4-ab16-1f9aa97b4026.jpg?1562896337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/76dadfd8-8492-4c55-827c-cd4e6a40ae97.jpg?1562918808", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/76dadfd8-8492-4c55-827c-cd4e6a40ae97.jpg?1562918808"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seize the Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29bf245f-e8e0-4d32-8cd7-06d832609910.jpg?1593272276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29bf245f-e8e0-4d32-8cd7-06d832609910.jpg?1593272276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Severed Strands", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bce654d6-fcf1-40a8-8bdb-5c37e561f7dc.jpg?1572893052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bce654d6-fcf1-40a8-8bdb-5c37e561f7dc.jpg?1572893052"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sever Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df1cb775-3a45-4f2c-9c45-febda6434c59.jpg?1562939859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df1cb775-3a45-4f2c-9c45-febda6434c59.jpg?1562939859"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Sever Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2d84fec-18f1-4231-a293-0dc1ff868a40.jpg?1562383023", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2d84fec-18f1-4231-a293-0dc1ff868a40.jpg?1562383023"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sheer Drop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca6e9658-684e-44fd-9c72-c5c3faa9fb1f.jpg?1593095413", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca6e9658-684e-44fd-9c72-c5c3faa9fb1f.jpg?1593095413"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Silverstrike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f27b92a-cde9-41bc-9b23-d83b74b167d4.jpg?1576383889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f27b92a-cde9-41bc-9b23-d83b74b167d4.jpg?1576383889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sip of Hemlock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/22051427-9b2a-4571-8c9f-ee84d8d0e4d1.jpg?1562815635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/22051427-9b2a-4571-8c9f-ee84d8d0e4d1.jpg?1562815635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skywhaler's Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54dd4948-dc79-4fe5-b4a0-fb257058f9dd.jpg?1576381006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54dd4948-dc79-4fe5-b4a0-fb257058f9dd.jpg?1576381006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8ff06c7d-5e78-4bcf-864b-34487f6555b2.jpg?1562088317", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8ff06c7d-5e78-4bcf-864b-34487f6555b2.jpg?1562088317"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter Pact", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42696fdb-de1f-44ae-bef3-b6af068958d0.jpg?1562908356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42696fdb-de1f-44ae-bef3-b6af068958d0.jpg?1562908356"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter Pact", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc8475bd-bdd4-421c-ace7-c6262f7405ce.jpg?1562932879", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc8475bd-bdd4-421c-ace7-c6262f7405ce.jpg?1562932879"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Slay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/eccda747-2680-4793-8a13-35e49b4de12f.jpg?1562944937", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/eccda747-2680-4793-8a13-35e49b4de12f.jpg?1562944937"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slingbow Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/def592b9-9d8b-4e2d-9b52-e1bc9f4bd019.jpg?1562297661", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/def592b9-9d8b-4e2d-9b52-e1bc9f4bd019.jpg?1562297661"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ff799e40-fd40-4f6a-8fa8-c22d77476168.jpg?1561854361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ff799e40-fd40-4f6a-8fa8-c22d77476168.jpg?1561854361"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/2698f01a-8574-4ae8-9441-a4361b1c29c6.jpg?1562702095", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/2698f01a-8574-4ae8-9441-a4361b1c29c6.jpg?1562702095"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14f165ad-cfe6-4a5d-8073-a70969494855.jpg?1562595916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14f165ad-cfe6-4a5d-8073-a70969494855.jpg?1562595916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c103163-31b7-4d25-aa2c-02ca082ee1bf.jpg?1604193448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c103163-31b7-4d25-aa2c-02ca082ee1bf.jpg?1604193448"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/766aad27-e987-45ab-82aa-e5f44fcc34ef.jpg?1562922992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/766aad27-e987-45ab-82aa-e5f44fcc34ef.jpg?1562922992"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1405bb2e-2204-43ab-82a3-5d0c8537325a.jpg?1562782881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1405bb2e-2204-43ab-82a3-5d0c8537325a.jpg?1562782881"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0103f3b1-88c2-4cbf-a67c-49420f92970f.jpg?1562825351", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0103f3b1-88c2-4cbf-a67c-49420f92970f.jpg?1562825351"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/2969e9b5-64d3-401f-9878-32ec283680ab.jpg?1562633742", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/2969e9b5-64d3-401f-9878-32ec283680ab.jpg?1562633742"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Smother", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/09b4deea-c077-46ab-898f-41b3907ecf33.jpg?1562281733", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/09b4deea-c077-46ab-898f-41b3907ecf33.jpg?1562281733"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smother", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/a/9a8321af-d667-44e7-8c03-3957286604b9.jpg?1562931422", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/a/9a8321af-d667-44e7-8c03-3957286604b9.jpg?1562931422"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snuff Out", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db8b3560-4940-40cc-9797-f909dcb1519b.jpg?1562090223", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db8b3560-4940-40cc-9797-f909dcb1519b.jpg?1562090223"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Snuff Out", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/8/18a3cca1-e50e-49b6-9e1a-f86640e3b177.jpg?1562379436", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/8/18a3cca1-e50e-49b6-9e1a-f86640e3b177.jpg?1562379436"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Reap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2a129e2-bed5-4ee7-b223-851452f72682.jpg?1562942827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2a129e2-bed5-4ee7-b223-851452f72682.jpg?1562942827"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Rend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fa084e1-05c2-4691-b9fe-3e3c717e5c9d.jpg?1562720249", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fa084e1-05c2-4691-b9fe-3e3c717e5c9d.jpg?1562720249"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Harvest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2013a138-f8e2-4a67-91e8-759288d985a7.jpg?1557576556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2013a138-f8e2-4a67-91e8-759288d985a7.jpg?1557576556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiteful Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fafaa798-e534-4cd0-b369-9e767a02fe3d.jpg?1593095848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fafaa798-e534-4cd0-b369-9e767a02fe3d.jpg?1593095848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spread the Sickness", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de42a771-4f5c-4295-b070-8cb857a0279e.jpg?1562615413", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de42a771-4f5c-4295-b070-8cb857a0279e.jpg?1562615413"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Strangling Soot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/6723e552-baf5-4b6a-8af6-843fd8597f6c.jpg?1562916570", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/6723e552-baf5-4b6a-8af6-843fd8597f6c.jpg?1562916570"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stream of Acid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbbf00b3-2a1b-4ad3-8a5b-deec9e08a231.jpg?1562875294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbbf00b3-2a1b-4ad3-8a5b-deec9e08a231.jpg?1562875294"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Sultai Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/993c9028-9b1b-4903-81b2-3cf4f37b7229.jpg?1562790829", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/993c9028-9b1b-4903-81b2-3cf4f37b7229.jpg?1562790829"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sultai Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c72495e-3c03-4dff-b671-47764af5058d.jpg?1562701596", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c72495e-3c03-4dff-b671-47764af5058d.jpg?1562701596"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sungold Barrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee507688-9890-47c4-bb04-43c51eb48e22.jpg?1634348527", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee507688-9890-47c4-bb04-43c51eb48e22.jpg?1634348527"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Surge of Righteousness", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/114366f3-237f-4f96-b644-5bd82d97b18b.jpg?1562782657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/114366f3-237f-4f96-b644-5bd82d97b18b.jpg?1562782657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cec3a260-6c50-401d-a0ff-bf49a973e1a1.jpg?1562943805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cec3a260-6c50-401d-a0ff-bf49a973e1a1.jpg?1562943805"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Swat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/947b8923-d9d6-4dd8-928b-91be9105ffb4.jpg?1562863743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/947b8923-d9d6-4dd8-928b-91be9105ffb4.jpg?1562863743"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swift Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/904cb2f5-eb62-4416-8236-d2fbeadf1dc4.jpg?1562031231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/904cb2f5-eb62-4416-8236-d2fbeadf1dc4.jpg?1562031231"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Swift Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a90c1ad0-83bd-471c-8d4c-e65bc2abaa18.jpg?1594735305", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a90c1ad0-83bd-471c-8d4c-e65bc2abaa18.jpg?1594735305"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Take Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66fbde22-d98d-4f12-b4d8-1bad2a9878b2.jpg?1562302645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66fbde22-d98d-4f12-b4d8-1bad2a9878b2.jpg?1562302645"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Terashi's Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc5fa34b-95c6-4e02-9e15-3f595f744741.jpg?1562879427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc5fa34b-95c6-4e02-9e15-3f595f744741.jpg?1562879427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminal Agony", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3ddb6d98-3a3a-4332-a64e-97aec71777a4.jpg?1626103523", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3ddb6d98-3a3a-4332-a64e-97aec71777a4.jpg?1626103523"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8af2d815-d8b2-42ff-9889-acbe77a42583.jpg?1593814672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8af2d815-d8b2-42ff-9889-acbe77a42583.jpg?1593814672"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc8acab8-4469-4baa-af2f-a3f49b841a55.jpg?1562644597", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc8acab8-4469-4baa-af2f-a3f49b841a55.jpg?1562644597"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/190ca502-672d-4cc0-b6e0-b9de517058d0.jpg?1562900286", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/190ca502-672d-4cc0-b6e0-b9de517058d0.jpg?1562900286"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54f3c523-09dc-4f2a-9bd9-7614e061de28.jpg?1655823700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54f3c523-09dc-4f2a-9bd9-7614e061de28.jpg?1655823700"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfd77920-7dbb-4673-9317-095ce9483878.jpg?1575602242", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfd77920-7dbb-4673-9317-095ce9483878.jpg?1575602242"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d1ccc3b-a6bd-4dc8-b7ba-99172d612106.jpg?1562546519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d1ccc3b-a6bd-4dc8-b7ba-99172d612106.jpg?1562546519"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f41651db-619a-4ab4-86cf-a0d32297dbdf.jpg?1562163040", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f41651db-619a-4ab4-86cf-a0d32297dbdf.jpg?1562163040"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21004958-2c7e-4a55-bc80-411c4d780106.jpg?1559591536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21004958-2c7e-4a55-bc80-411c4d780106.jpg?1559591536"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba9d4863-75f2-4894-8033-e4ffebe0547a.jpg?1561757930", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba9d4863-75f2-4894-8033-e4ffebe0547a.jpg?1561757930"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Tezzeret's Betrayal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d71efa6-5de8-476f-86ce-0790956e574f.jpg?1562932177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d71efa6-5de8-476f-86ce-0790956e574f.jpg?1562932177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thornado", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eadffd6b-d707-4fc5-a600-44eb9124b195.jpg?1615475425", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eadffd6b-d707-4fc5-a600-44eb9124b195.jpg?1615475425"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Tidy Conclusion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfcf6849-4fac-41b9-8e70-dc77c4562a42.jpg?1576381900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfcf6849-4fac-41b9-8e70-dc77c4562a42.jpg?1576381900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trip Wire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4eb1e16f-002e-4a81-ba41-cfe41f3a9071.jpg?1634292196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4eb1e16f-002e-4a81-ba41-cfe41f3a9071.jpg?1634292196"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Triumphant Surge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75d6eb18-a49d-4fa5-a333-78aafbc4abcb.jpg?1581479273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75d6eb18-a49d-4fa5-a333-78aafbc4abcb.jpg?1581479273"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tyrant's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7e2708c-2824-4925-b529-d625deb77924.jpg?1557577324", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7e2708c-2824-4925-b529-d625deb77924.jpg?1557577324"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b41f7cf3-bd76-4184-b694-f565aa5cf3a4.jpg?1562791851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b41f7cf3-bd76-4184-b694-f565aa5cf3a4.jpg?1562791851"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2b4912a-83a2-4870-8fac-81fa79da2830.jpg?1562793639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2b4912a-83a2-4870-8fac-81fa79da2830.jpg?1562793639"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/334e3ffc-a4dc-405c-b6e4-7182f28241fe.jpg?1562639743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/334e3ffc-a4dc-405c-b6e4-7182f28241fe.jpg?1562639743"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Unforge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d369a3da-3424-4984-a50a-59fd9c3d689e.jpg?1562639761", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d369a3da-3424-4984-a50a-59fd9c3d689e.jpg?1562639761"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unholy Hunger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/5994b7b0-3bca-480b-b265-ed269f15c17e.jpg?1562021369", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/5994b7b0-3bca-480b-b265-ed269f15c17e.jpg?1562021369"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0e6b9a3b-8a19-4094-8dbb-08a0a9ca04a0.jpg?1643587276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0e6b9a3b-8a19-4094-8dbb-08a0a9ca04a0.jpg?1643587276"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65998e94-15a0-41f1-8288-730b957f81df.jpg?1562825972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65998e94-15a0-41f1-8288-730b957f81df.jpg?1562825972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f482213a-4e3e-4e13-82a1-88e7d6c4ba2c.jpg?1561758433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f482213a-4e3e-4e13-82a1-88e7d6c4ba2c.jpg?1561758433"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vanquish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27bae717-56c0-4028-b1e7-a445d6a57176.jpg?1562875950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27bae717-56c0-4028-b1e7-a445d6a57176.jpg?1562875950"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Foul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fdcec06-e33c-4737-b81e-b156d6e3fd77.jpg?1562821391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fdcec06-e33c-4737-b81e-b156d6e3fd77.jpg?1562821391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c15852d4-2c79-4841-bb65-6661d88fdfab.jpg?1604196688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c15852d4-2c79-4841-bb65-6661d88fdfab.jpg?1604196688"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e599ed0b-4b3b-4341-b6ac-7fdfdc6799a3.jpg?1562565757", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e599ed0b-4b3b-4341-b6ac-7fdfdc6799a3.jpg?1562565757"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vendetta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/039fc76d-3b7e-4329-a997-07c25509e421.jpg?1562700700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/039fc76d-3b7e-4329-a997-07c25509e421.jpg?1562700700"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Vendetta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67ced38e-0f33-4bda-8e18-09f6ac03a3d7.jpg?1562381344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67ced38e-0f33-4bda-8e18-09f6ac03a3d7.jpg?1562381344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/011b9836-fee4-4e83-add7-5e13cb1275d6.jpg?1562231350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/011b9836-fee4-4e83-add7-5e13cb1275d6.jpg?1562231350"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a882fbcc-b2b9-44f3-b5cc-56759879f473.jpg?1562257514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a882fbcc-b2b9-44f3-b5cc-56759879f473.jpg?1562257514"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/3209ee48-4485-44fc-b71d-cd6241674e64.jpg?1562906693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/3209ee48-4485-44fc-b71d-cd6241674e64.jpg?1562906693"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91c249b-157c-4f1d-8171-29d1e75b1c9f.jpg?1562447828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91c249b-157c-4f1d-8171-29d1e75b1c9f.jpg?1562447828"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Venomous Vines", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db10359c-1ea8-4453-bc01-f638ad20a5ec.jpg?1562632255", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db10359c-1ea8-4453-bc01-f638ad20a5ec.jpg?1562632255"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror3.json b/web/public/mtg/jsons/terror3.json new file mode 100644 index 00000000..463fb4a7 --- /dev/null +++ b/web/public/mtg/jsons/terror3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Victim of Night", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee4c6135-eee9-43ec-bbe8-76912352dcac.jpg?1562839346", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee4c6135-eee9-43ec-bbe8-76912352dcac.jpg?1562839346"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a1bfefd-dae8-49e9-9d56-cc852e3dc93b.jpg?1562904968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a1bfefd-dae8-49e9-9d56-cc852e3dc93b.jpg?1562904968"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4978ecd-3c2e-49e2-98e0-0172887e4319.jpg?1628337210", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4978ecd-3c2e-49e2-98e0-0172887e4319.jpg?1628337210"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "draft_innovation"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97aeb745-5b98-4240-a1a8-861c06d616cc.jpg?1562925629", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97aeb745-5b98-4240-a1a8-861c06d616cc.jpg?1562925629"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6fef34ec-f728-4919-9254-576ed889a654.jpg?1561757378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6fef34ec-f728-4919-9254-576ed889a654.jpg?1561757378"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c2d88dd-813a-4cd5-9a6a-ca6f80564078.jpg?1561756842", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c2d88dd-813a-4cd5-9a6a-ca6f80564078.jpg?1561756842"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Violet Pall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdfd0fa3-37d2-403e-99fe-8c9e57515e9d.jpg?1562881062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdfd0fa3-37d2-403e-99fe-8c9e57515e9d.jpg?1562881062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vraska's Stoneglare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27fc4db6-a5f5-4254-ae64-c8eaf2c98030.jpg?1572894308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27fc4db6-a5f5-4254-ae64-c8eaf2c98030.jpg?1572894308"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Walk the Plank", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0038ac6a-318f-44fb-bb64-7ae172c4aca3.jpg?1562549640", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0038ac6a-318f-44fb-bb64-7ae172c4aca3.jpg?1562549640"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Walk the Plank", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d7f3b24f-e2ec-4405-b6f5-147292063b0a.jpg?1562935396", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d7f3b24f-e2ec-4405-b6f5-147292063b0a.jpg?1562935396"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Wallop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45ce5126-e7b1-41ab-9e56-1e12927c4d27.jpg?1562909144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45ce5126-e7b1-41ab-9e56-1e12927c4d27.jpg?1562909144"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Weed Strangle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1f7fb79-19a8-483a-bf91-e687f7da4e9c.jpg?1562366513", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1f7fb79-19a8-483a-bf91-e687f7da4e9c.jpg?1562366513"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wing Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d37ba325-5a14-473b-9def-6a4660a50d7a.jpg?1562248658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d37ba325-5a14-473b-9def-6a4660a50d7a.jpg?1562248658"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wing Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19116d5d-8f2d-4e85-849d-1fbaa67e8cfd.jpg?1562862328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19116d5d-8f2d-4e85-849d-1fbaa67e8cfd.jpg?1562862328"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Winnow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d61748dd-4010-47da-8717-ca0147877057.jpg?1562937982", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d61748dd-4010-47da-8717-ca0147877057.jpg?1562937982"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Witherbloom Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87d5e94b-0b35-4efd-9158-1767dcaea38c.jpg?1624740473", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87d5e94b-0b35-4efd-9158-1767dcaea38c.jpg?1624740473"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrecking Ball", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/1182e0cf-475e-4cb9-a00a-c9a4032f51e4.jpg?1593273836", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/1182e0cf-475e-4cb9-a00a-c9a4032f51e4.jpg?1593273836"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wretched Banquet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bdaf55b-2de3-4c8a-90ae-9c88c9d00fd7.jpg?1562800483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bdaf55b-2de3-4c8a-90ae-9c88c9d00fd7.jpg?1562800483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "You Are Already Dead", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/768727ce-4f84-4527-8d69-3c9b7877b748.jpg?1654567474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/768727ce-4f84-4527-8d69-3c9b7877b748.jpg?1654567474"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath1.json b/web/public/mtg/jsons/wrath1.json new file mode 100644 index 00000000..267e34b0 --- /dev/null +++ b/web/public/mtg/jsons/wrath1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Aetherize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33303859-c6e0-4ebd-bb5f-44be7f5d7459.jpg?1561821990", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33303859-c6e0-4ebd-bb5f-44be7f5d7459.jpg?1561821990"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aetherspouts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46f1b48f-6528-46bd-a384-2358af25e500.jpg?1562786278", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46f1b48f-6528-46bd-a384-2358af25e500.jpg?1562786278"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Aggravate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/999f40a7-b723-42e1-83c1-f45a72a26dd4.jpg?1592709004", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/999f40a7-b723-42e1-83c1-f45a72a26dd4.jpg?1592709004"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Akroma's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e33aaf7-7490-4b64-a966-82fbf7ca8686.jpg?1562917166", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e33aaf7-7490-4b64-a966-82fbf7ca8686.jpg?1562917166"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Akroma's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f112edd-1d2f-45ad-aaeb-6c0934d24c1f.jpg?1570203942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f112edd-1d2f-45ad-aaeb-6c0934d24c1f.jpg?1570203942"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Alpha Brawl", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ec168a-3e4f-4527-901a-bc28cc28d125.jpg?1562949045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ec168a-3e4f-4527-901a-bc28cc28d125.jpg?1562949045"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90795891-5e67-47c0-8d52-a5e5c5a9ef81.jpg?1562821425", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90795891-5e67-47c0-8d52-a5e5c5a9ef81.jpg?1562821425"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dedcbd3b-7e30-44cf-b9b7-1bb32c11ef67.jpg?1655825935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dedcbd3b-7e30-44cf-b9b7-1bb32c11ef67.jpg?1655825935"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec898bc9-9ab8-4394-8c4c-8d652f313919.jpg?1607042506", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec898bc9-9ab8-4394-8c4c-8d652f313919.jpg?1607042506"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88f2ca85-de02-4471-b90f-d13ccb93c8bb.jpg?1597250046", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88f2ca85-de02-4471-b90f-d13ccb93c8bb.jpg?1597250046"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Arcbond", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9bc397d1-50a8-46cd-98b2-7104f2241420.jpg?1562828028", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9bc397d1-50a8-46cd-98b2-7104f2241420.jpg?1562828028"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arms of Hadar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db1fd431-8f6d-4ca5-bc0c-53881c500da1.jpg?1653767219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db1fd431-8f6d-4ca5-bc0c-53881c500da1.jpg?1653767219"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bef16a71-5ed2-4f30-a844-c02a0754f679.jpg?1562853529", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bef16a71-5ed2-4f30-a844-c02a0754f679.jpg?1562853529"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8ee73fe8-d52b-43bb-ab91-5545192be676.jpg?1562357897", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8ee73fe8-d52b-43bb-ab91-5545192be676.jpg?1562357897"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbbf55bc-6bb3-458a-8cf0-1f603bb2acb3.jpg?1562939169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbbf55bc-6bb3-458a-8cf0-1f603bb2acb3.jpg?1562939169"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Baki's Curse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3261b4c-7963-4ca0-875d-77b7c8571b3f.jpg?1562588703", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3261b4c-7963-4ca0-875d-77b7c8571b3f.jpg?1562588703"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barrage of Boulders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2eb1a9f7-32ba-48fd-a7f7-788b0ec052c6.jpg?1562784418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2eb1a9f7-32ba-48fd-a7f7-788b0ec052c6.jpg?1562784418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Begin Anew", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d885aedb-2c65-4099-af2e-0a540caf8d33.jpg?1645417110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d885aedb-2c65-4099-af2e-0a540caf8d33.jpg?1645417110"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Bite of the Black Rose", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/254d1363-1204-41d2-9799-34484a3eb211.jpg?1562864493", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/254d1363-1204-41d2-9799-34484a3eb211.jpg?1562864493"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Biting Rain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5ac62d2f-6834-4d98-b69d-bd7b5831d981.jpg?1576384359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5ac62d2f-6834-4d98-b69d-bd7b5831d981.jpg?1576384359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Black Sun's Zenith", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/03bdcf52-50b8-42c0-9665-931d83f5f314.jpg?1562609329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03bdcf52-50b8-42c0-9665-931d83f5f314.jpg?1562609329"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Black Sun's Zenith", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd88131a-2811-4a1f-bb9a-c82e12c1493b.jpg?1561758222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd88131a-2811-4a1f-bb9a-c82e12c1493b.jpg?1561758222"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Blasphemous Act", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/509ce648-fb76-486d-8b39-183e368b7cb7.jpg?1562830111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/509ce648-fb76-486d-8b39-183e368b7cb7.jpg?1562830111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blazing Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3adc0288-acdf-4a99-9bfb-919cae1aeb69.jpg?1591227065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3adc0288-acdf-4a99-9bfb-919cae1aeb69.jpg?1591227065"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Blazing Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba450179-4591-4e8a-b6ca-66cbef1817f2.jpg?1543675486", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba450179-4591-4e8a-b6ca-66cbef1817f2.jpg?1543675486"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodline Culling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fac827f7-a587-4adf-8408-2d9ccd9c1343.jpg?1634349575", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fac827f7-a587-4adf-8408-2d9ccd9c1343.jpg?1634349575"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blood Money", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d45c18c-b8eb-465c-8dfc-fd6da73e25b5.jpg?1653442044", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d45c18c-b8eb-465c-8dfc-fd6da73e25b5.jpg?1653442044"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Blood on the Snow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8606f40-0af4-443b-a413-a88dc3e8f32e.jpg?1631047655", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8606f40-0af4-443b-a413-a88dc3e8f32e.jpg?1631047655"}, "reprint": false, "frame_effects": ["snow"], "digital": false, "set_type": "expansion"}, {"name": "Boiling Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdaab44c-4ce1-43fb-915c-c687fe8559ce.jpg?1562943558", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdaab44c-4ce1-43fb-915c-c687fe8559ce.jpg?1562943558"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bonfire of the Damned", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Bontu's Last Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b4d0102-c0d6-4d50-941a-dd1c3575a3a8.jpg?1562791273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b4d0102-c0d6-4d50-941a-dd1c3575a3a8.jpg?1562791273"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Borrowing the East Wind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/6/96ba9014-d750-4924-aa6f-8b9f421807f9.jpg?1562257056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/6/96ba9014-d750-4924-aa6f-8b9f421807f9.jpg?1562257056"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Breaking Point", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath of Darigaaz", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath Weapon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0174e40a-0ef5-4439-91e6-3fc39f482520.jpg?1653596065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0174e40a-0ef5-4439-91e6-3fc39f482520.jpg?1653596065"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Burn Down the House", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20ded7af-8086-465e-a980-3099217d324c.jpg?1634350460", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20ded7af-8086-465e-a980-3099217d324c.jpg?1634350460"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burning of Xinye", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33a1fe45-52d2-4c50-bedc-eee156ab69c8.jpg?1562256064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33a1fe45-52d2-4c50-bedc-eee156ab69c8.jpg?1562256064"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "By Invitation Only", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46764e49-64da-4a94-b61c-75e006b2c5a9.jpg?1643585907", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46764e49-64da-4a94-b61c-75e006b2c5a9.jpg?1643585907"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Canopy Surge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e19d68e-7554-4627-a316-beb1f75fa494.jpg?1562904391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e19d68e-7554-4627-a316-beb1f75fa494.jpg?1562904391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cataclysm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/024ae668-a1ae-4020-89c8-acbd8bd0a691.jpg?1593863070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/024ae668-a1ae-4020-89c8-acbd8bd0a691.jpg?1593863070"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cataclysm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3ed0d87b-1ce8-452b-9558-fa1923407f16.jpg?1559618030", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3ed0d87b-1ce8-452b-9558-fa1923407f16.jpg?1559618030"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Catastrophe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/294d21dc-5c76-4449-936f-9b7541d37c86.jpg?1562903769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/294d21dc-5c76-4449-936f-9b7541d37c86.jpg?1562903769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cave-In", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Celestial Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5fd29cd7-9950-49c0-9e71-d6b0f944292c.jpg?1637627823", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5fd29cd7-9950-49c0-9e71-d6b0f944292c.jpg?1637627823"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Cerebral Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77161159-ee2c-485d-8674-d8590ccc62e1.jpg?1562819165", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77161159-ee2c-485d-8674-d8590ccc62e1.jpg?1562819165"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chain Reaction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/614b9df9-c959-4bdb-91c0-75ae60b724e4.jpg?1567754665", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/614b9df9-c959-4bdb-91c0-75ae60b724e4.jpg?1567754665"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chandra's Flame Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f13b6a7-fa62-4d94-a56c-f2e64c8c1666.jpg?1592518162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f13b6a7-fa62-4d94-a56c-f2e64c8c1666.jpg?1592518162"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Chandra's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Chandra's Ignition", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d4c90de-49aa-43ed-a18a-f7f96268e5eb.jpg?1562027623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d4c90de-49aa-43ed-a18a-f7f96268e5eb.jpg?1562027623"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cinderclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/5516cf97-805f-4a21-a4c6-2d6e55865336.jpg?1604196918", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/5516cf97-805f-4a21-a4c6-2d6e55865336.jpg?1604196918"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Citywide Bust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a995200f-1e9d-4ff3-9e04-4a4309e0e09c.jpg?1572892490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a995200f-1e9d-4ff3-9e04-4a4309e0e09c.jpg?1572892490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Claws of Wirewood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b94cd33f-40b6-4b11-97a4-8676ef27631e.jpg?1562533774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b94cd33f-40b6-4b11-97a4-8676ef27631e.jpg?1562533774"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cleanse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fbd611b-ac97-4516-bad7-cc9ee4ef74f7.jpg?1591836785", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fbd611b-ac97-4516-bad7-cc9ee4ef74f7.jpg?1591836785"}, "content_warning": true, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cleansing Nova", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/11f1b6cd-d89a-4468-a097-7a54efe22f2c.jpg?1625192921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/11f1b6cd-d89a-4468-a097-7a54efe22f2c.jpg?1625192921"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Cleansing Nova", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5be8eed7-c033-42cc-bd21-4512db7af66c.jpg?1562302239", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5be8eed7-c033-42cc-bd21-4512db7af66c.jpg?1562302239"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cloudkill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c71b2b8-f5ef-4885-9f8d-284fe335d184.jpg?1654365309", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c71b2b8-f5ef-4885-9f8d-284fe335d184.jpg?1654365309"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Collision of Realms", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49618217-1bbb-498a-a6f0-f269ce7166a6.jpg?1651655330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49618217-1bbb-498a-a6f0-f269ce7166a6.jpg?1651655330"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Conductive Current", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43adef3c-87f0-4db1-9fbb-017c96c815ff.jpg?1645416694", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43adef3c-87f0-4db1-9fbb-017c96c815ff.jpg?1645416694"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Consume the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c94dcaed-55da-41f4-a61f-2a79ef6c1459.jpg?1593095734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c94dcaed-55da-41f4-a61f-2a79ef6c1459.jpg?1593095734"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Consume the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/921ebea0-48bf-4338-9e84-2cd06ffe6f4b.jpg?1562706383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/921ebea0-48bf-4338-9e84-2cd06ffe6f4b.jpg?1562706383"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corpse Explosion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c700eff3-138b-4d4c-ba36-58b98986168c.jpg?1650029916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c700eff3-138b-4d4c-ba36-58b98986168c.jpg?1650029916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corrosive Gale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/04a13825-ab9b-4ffd-9b59-6198181891b9.jpg?1562875245", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/04a13825-ab9b-4ffd-9b59-6198181891b9.jpg?1562875245"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cosmotronic Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/69c5bafa-8cd8-4158-98e0-46dc74c027c0.jpg?1572893121", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/69c5bafa-8cd8-4158-98e0-46dc74c027c0.jpg?1572893121"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cower in Fear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf2d53b8-7847-4b94-9711-eca29facccba.jpg?1562559508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf2d53b8-7847-4b94-9711-eca29facccba.jpg?1562559508"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Crippling Fear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d9bd181-b99f-477e-bcfb-9b78cbf51224.jpg?1631047737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d9bd181-b99f-477e-bcfb-9b78cbf51224.jpg?1631047737"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crush the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/875a20c2-1d17-46ea-b4d2-3e70bc05aae3.jpg?1631049096", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/875a20c2-1d17-46ea-b4d2-3e70bc05aae3.jpg?1631049096"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crux of Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1d45374-a41b-4b3f-a7c8-3eb5ca767cf6.jpg?1648060698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1d45374-a41b-4b3f-a7c8-3eb5ca767cf6.jpg?1648060698"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crux of Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3ccea48-ee90-4da8-832d-8c30c98bf1dd.jpg?1623779891", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3ccea48-ee90-4da8-832d-8c30c98bf1dd.jpg?1623779891"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Cry of the Carnarium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/715a14a3-046e-45ca-b943-dd630e5202b7.jpg?1584830546", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/715a14a3-046e-45ca-b943-dd630e5202b7.jpg?1584830546"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Culling Sun", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ec5a956-c846-46b6-91bd-37e4db542280.jpg?1593272635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ec5a956-c846-46b6-91bd-37e4db542280.jpg?1593272635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dakmor Plague", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58b38ef1-5839-4292-91d6-e45698c69a75.jpg?1562915882", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58b38ef1-5839-4292-91d6-e45698c69a75.jpg?1562915882"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Damn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/26c68473-70ca-40ba-b5c6-71ec30f88a2c.jpg?1562568132", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/26c68473-70ca-40ba-b5c6-71ec30f88a2c.jpg?1562568132"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dca972d7-fcf8-4ac4-a98b-fffb2fbb4dbc.jpg?1656326586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dca972d7-fcf8-4ac4-a98b-fffb2fbb4dbc.jpg?1656326586"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fc1d7db-11a3-4ff9-8d27-1fe401053080.jpg?1615223046", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fc1d7db-11a3-4ff9-8d27-1fe401053080.jpg?1615223046"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c916a119-9eee-440d-90ef-05ab35bf3fbe.jpg?1562935376", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c916a119-9eee-440d-90ef-05ab35bf3fbe.jpg?1562935376"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c5823bb-d56d-4bed-ba3f-09bdd93c52dc.jpg?1561757368", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c5823bb-d56d-4bed-ba3f-09bdd93c52dc.jpg?1561757368"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Damning Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5be40c34-6df0-4471-b99b-850ae2be9923.jpg?1650406359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5be40c34-6df0-4471-b99b-850ae2be9923.jpg?1650406359"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2aa98fca-972b-46c2-bdec-6ace35c988d5.jpg?1562610835", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2aa98fca-972b-46c2-bdec-6ace35c988d5.jpg?1562610835"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5bf85d00-52cc-4594-b4fd-5ec424210524.jpg?1623592427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5bf85d00-52cc-4594-b4fd-5ec424210524.jpg?1623592427"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/aea87800-6725-4399-b489-651637e1804a.jpg?1561757821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/aea87800-6725-4399-b489-651637e1804a.jpg?1561757821"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6ba873f7-a7a4-44aa-84a6-44501424dc7a.jpg?1561757360", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6ba873f7-a7a4-44aa-84a6-44501424dc7a.jpg?1561757360"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Deadly Tempest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9ca2810-3c1b-43cf-af1e-078015bf3492.jpg?1562708889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9ca2810-3c1b-43cf-af1e-078015bf3492.jpg?1562708889"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Dead of Winter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f480df6d-e227-4ccb-ad6d-a4ad48a360ad.jpg?1562201599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f480df6d-e227-4ccb-ad6d-a4ad48a360ad.jpg?1562201599"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Deafening Clarion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e115a81-001d-4e17-98af-6a63f2b0967f.jpg?1572893584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e115a81-001d-4e17-98af-6a63f2b0967f.jpg?1572893584"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Cloud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97a0bfb9-859b-4fed-a1c4-1f0924715801.jpg?1562638297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97a0bfb9-859b-4fed-a1c4-1f0924715801.jpg?1562638297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Frenzy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92096311-a3fa-41fc-b7a9-71ac2310f7fe.jpg?1562790443", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92096311-a3fa-41fc-b7a9-71ac2310f7fe.jpg?1562790443"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Annihilation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73744717-518c-478e-9da9-201c49124f37.jpg?1562530626", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73744717-518c-478e-9da9-201c49124f37.jpg?1562530626"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Pain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1958a07-fc75-41cd-ac45-d92d49587754.jpg?1562536145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1958a07-fc75-41cd-ac45-d92d49587754.jpg?1562536145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Pain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/03c37c68-cccf-4309-80c5-828108b942a4.jpg?1569957295", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03c37c68-cccf-4309-80c5-828108b942a4.jpg?1569957295"}, "reprint": true, "digital": false, "set_type": "arsenal"}, {"name": "Delayed Blast Fireball", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e59903e3-a344-4218-9d41-8b19a9bc8311.jpg?1654082475", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e59903e3-a344-4218-9d41-8b19a9bc8311.jpg?1654082475"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Depopulate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c53c1898-9107-4bf8-b249-d0502fb9596d.jpg?1649698259", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c53c1898-9107-4bf8-b249-d0502fb9596d.jpg?1649698259"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Descend upon the Sinful", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c9ff2cbf-a1dc-4cc5-9a5d-8439899d4e87.jpg?1576383726", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c9ff2cbf-a1dc-4cc5-9a5d-8439899d4e87.jpg?1576383726"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desert Sandstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/588ad2bf-405d-4c36-b485-e415c22f2703.jpg?1562256542", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/588ad2bf-405d-4c36-b485-e415c22f2703.jpg?1562256542"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Destructive Force", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1abde258-08e0-4762-8142-38e08a960f9d.jpg?1562452402", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1abde258-08e0-4762-8142-38e08a960f9d.jpg?1562452402"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Devastate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devastating Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9fffeed0-a5ea-47ac-a7a4-0cc3bb1d408a.jpg?1562631212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9fffeed0-a5ea-47ac-a7a4-0cc3bb1d408a.jpg?1562631212"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devastation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/71cce019-162c-4969-89ac-1cf94148a032.jpg?1562446865", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cce019-162c-4969-89ac-1cf94148a032.jpg?1562446865"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Disaster Radius", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9318ae4a-1084-49d9-b5de-dbe4d80836cb.jpg?1562706406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9318ae4a-1084-49d9-b5de-dbe4d80836cb.jpg?1562706406"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disorder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6d11422-60a9-4386-8e7f-dd7dcdac58d8.jpg?1562246308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6d11422-60a9-4386-8e7f-dd7dcdac58d8.jpg?1562246308"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Disorder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3fa5ec10-dfea-4e6d-8996-553a4a0eb8a4.jpg?1562908220", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3fa5ec10-dfea-4e6d-8996-553a4a0eb8a4.jpg?1562908220"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Divine Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/446ea3a4-206a-4097-87c1-c04bb7812972.jpg?1562829296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/446ea3a4-206a-4097-87c1-c04bb7812972.jpg?1562829296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Doomskar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/130ee895-1e5e-4f82-bb66-e1275bac75dd.jpg?1631045641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/130ee895-1e5e-4f82-bb66-e1275bac75dd.jpg?1631045641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Do or Die", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05f63cd9-e82b-4cf8-b8ce-f0aa0157692b.jpg?1562896148", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05f63cd9-e82b-4cf8-b8ce-f0aa0157692b.jpg?1562896148"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Draconic Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/657de246-b9fc-47b1-b932-091e9500bb82.jpg?1624591671", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/657de246-b9fc-47b1-b932-091e9500bb82.jpg?1624591671"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in Sorrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/287c7570-8080-43dc-a586-963e15566446.jpg?1593091908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/287c7570-8080-43dc-a586-963e15566446.jpg?1593091908"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in Sorrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/107cdfa4-da15-4610-9b72-e6e6c59deec4.jpg?1630641355", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/107cdfa4-da15-4610-9b72-e6e6c59deec4.jpg?1630641355"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a142f369-8fdd-4dc8-b5d9-3493455cc588.jpg?1562447357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a142f369-8fdd-4dc8-b5d9-3493455cc588.jpg?1562447357"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/547c10ea-8ace-4496-8b99-61863c0cec1b.jpg?1562587287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/547c10ea-8ace-4496-8b99-61863c0cec1b.jpg?1562587287"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/997ea663-40a1-49b7-80f1-2e1febc1b6fa.jpg?1562587769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/997ea663-40a1-49b7-80f1-2e1febc1b6fa.jpg?1562587769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Duneblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e3fba5b-b4cd-4050-b9f0-d8eabe82e7d6.jpg?1562701635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e3fba5b-b4cd-4050-b9f0-d8eabe82e7d6.jpg?1562701635"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Dwarven Catapult", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8c1c6932-638a-4df7-bf9b-8d921f7484d9.jpg?1562921034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8c1c6932-638a-4df7-bf9b-8d921f7484d9.jpg?1562921034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electrickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ed81ee8-d5e4-4127-876e-9bff81f9c726.jpg?1562787062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ed81ee8-d5e4-4127-876e-9bff81f9c726.jpg?1562787062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Endemic Plague", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/15326971-a53b-45f2-8f1d-1b82935286e1.jpg?1562900082", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/15326971-a53b-45f2-8f1d-1b82935286e1.jpg?1562900082"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "End Hostilities", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/80a53ed7-a7b7-40d8-9239-cf6f205dbc59.jpg?1562789330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/80a53ed7-a7b7-40d8-9239-cf6f205dbc59.jpg?1562789330"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "End the Festivities", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bec748e6-7245-4a71-aeee-cefed8346948.jpg?1643591154", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bec748e6-7245-4a71-aeee-cefed8346948.jpg?1643591154"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Engulf the Shore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/22909767-a088-49ff-83be-37f967d1da3d.jpg?1576384043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/22909767-a088-49ff-83be-37f967d1da3d.jpg?1576384043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3e32d1b-e580-4d09-b285-c8d6c5297896.jpg?1625191655", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3e32d1b-e580-4d09-b285-c8d6c5297896.jpg?1625191655"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a978fa0a-a52b-4464-afe3-d9f7bc202e63.jpg?1562553159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a978fa0a-a52b-4464-afe3-d9f7bc202e63.jpg?1562553159"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e1144eb-701d-4716-9051-e8b77480e72d.jpg?1595438077", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e1144eb-701d-4716-9051-e8b77480e72d.jpg?1595438077"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1cb8ae53-a53f-4a0f-94f7-559aca041797.jpg?1562595927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1cb8ae53-a53f-4a0f-94f7-559aca041797.jpg?1562595927"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Evaporate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3c99939-4854-4e28-a142-4cb7f89fe898.jpg?1562587778", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3c99939-4854-4e28-a142-4cb7f89fe898.jpg?1562587778"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Evincar's Justice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d53f46f-b069-4b34-af4b-98143328c078.jpg?1562054236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d53f46f-b069-4b34-af4b-98143328c078.jpg?1562054236"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a233a244-7f84-4525-b0ce-e10db0a95385.jpg?1562055894", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a233a244-7f84-4525-b0ce-e10db0a95385.jpg?1562055894"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinction Event", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/8725a869-462b-4381-880a-b4bcc63a655b.jpg?1591226783", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/8725a869-462b-4381-880a-b4bcc63a655b.jpg?1591226783"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinguish All Hope", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/6895024f-a04b-46cf-b020-df4487d0c758.jpg?1593095692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/6895024f-a04b-46cf-b020-df4487d0c758.jpg?1593095692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eyeblight Massacre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d73484db-5fd0-4a01-83fd-54748cc21a0f.jpg?1562044208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d73484db-5fd0-4a01-83fd-54748cc21a0f.jpg?1562044208"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ezuri's Predation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d9b4ad1-3d5c-43b6-9284-9ec427936dd2.jpg?1562704058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d9b4ad1-3d5c-43b6-9284-9ec427936dd2.jpg?1562704058"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Falling Star", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2b9983e-20d4-4d12-9e2c-ec6d9a345787.jpg?1562861838", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2b9983e-20d4-4d12-9e2c-ec6d9a345787.jpg?1562861838"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a56410a7-6f99-4bdf-9385-f23571c263c3.jpg?1562929852", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a56410a7-6f99-4bdf-9385-f23571c263c3.jpg?1562929852"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d6c10ca-f6d6-4322-aa17-7e874cb10bb1.jpg?1562257044", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d6c10ca-f6d6-4322-aa17-7e874cb10bb1.jpg?1562257044"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Farewell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1068723-d1ef-4007-97d9-b10dccdbade4.jpg?1654566260", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1068723-d1ef-4007-97d9-b10dccdbade4.jpg?1654566260"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Farewell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98c664bc-9585-47a7-9514-b3e30a4e1b59.jpg?1654569820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98c664bc-9585-47a7-9514-b3e30a4e1b59.jpg?1654569820"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Fated Retribution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8158b330-2868-4147-907e-4d86e44cfaad.jpg?1593091437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8158b330-2868-4147-907e-4d86e44cfaad.jpg?1593091437"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fault Line", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Succession", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac83f97d-c8c9-480c-a32c-918035673ab4.jpg?1608909745", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac83f97d-c8c9-480c-a32c-918035673ab4.jpg?1608909745"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Fell the Mighty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4e999d3-c2d7-47dc-81ad-a2baf6cc4757.jpg?1561960243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4e999d3-c2d7-47dc-81ad-a2baf6cc4757.jpg?1561960243"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Festergloom", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3125137-bd18-488e-b45e-6fc23828c5bd.jpg?1562796922", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3125137-bd18-488e-b45e-6fc23828c5bd.jpg?1562796922"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Festering March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c34e6aa-0414-45ba-b6eb-1ac4255d7de8.jpg?1562903995", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c34e6aa-0414-45ba-b6eb-1ac4255d7de8.jpg?1562903995"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Cannonade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/664d21c9-4b6c-4797-845f-7bca79c2b76b.jpg?1562556766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/664d21c9-4b6c-4797-845f-7bca79c2b76b.jpg?1562556766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Fight to the Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/5552ca9b-0245-4f91-9646-a5b5443863a2.jpg?1562641354", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/5552ca9b-0245-4f91-9646-a5b5443863a2.jpg?1562641354"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2503e136-031f-498a-b042-4077baebe8f8.jpg?1562876056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2503e136-031f-498a-b042-4077baebe8f8.jpg?1562876056"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Revels", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99f3744a-71c4-4a54-9e1c-92420526b792.jpg?1562359766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99f3744a-71c4-4a54-9e1c-92420526b792.jpg?1562359766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Firespout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13454f69-1458-4c03-ab02-bd697a32eb17.jpg?1562826991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13454f69-1458-4c03-ab02-bd697a32eb17.jpg?1562826991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Firespout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fecd098-bbf2-44f1-b9f1-7b93ea660880.jpg?1559966438", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fecd098-bbf2-44f1-b9f1-7b93ea660880.jpg?1559966438"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Fire Tempest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Flamebreak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flames of the Raze-Boar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16957271-12bb-4031-b476-f7678b753ae3.jpg?1584830878", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16957271-12bb-4031-b476-f7678b753ae3.jpg?1584830878"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Sweep", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e489d6c-2eb2-4914-ae71-c9da55b51d0b.jpg?1586187261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e489d6c-2eb2-4914-ae71-c9da55b51d0b.jpg?1586187261"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Flame Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flaying Tendrils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77899cb2-4d87-4c2d-99ae-1ae75bc5dc86.jpg?1562918962", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77899cb2-4d87-4c2d-99ae-1ae75bc5dc86.jpg?1562918962"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Flaying Tendrils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10bf8dbf-ae2e-41cd-904c-84b9cca14c27.jpg?1575936034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10bf8dbf-ae2e-41cd-904c-84b9cca14c27.jpg?1575936034"}, "reprint": true, "frame_effects": ["devoid"], "digital": false, "set_type": "promo"}, {"name": "Flowstone Slide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec7b02e1-0a20-4247-ae2a-056c5356f168.jpg?1562632691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec7b02e1-0a20-4247-ae2a-056c5356f168.jpg?1562632691"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Forced March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36eae0e1-7100-449d-a259-7abfcd429117.jpg?1562379925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36eae0e1-7100-449d-a259-7abfcd429117.jpg?1562379925"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fumigate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f00f27a7-9e92-4fbf-baa8-f47a5eee48a6.jpg?1576380863", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f00f27a7-9e92-4fbf-baa8-f47a5eee48a6.jpg?1576380863"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gale Force", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/26c5c233-a373-4ac4-9b99-81ed97df1f9b.jpg?1562758454", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/26c5c233-a373-4ac4-9b99-81ed97df1f9b.jpg?1562758454"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gates Ablaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b574b44-01e1-4197-99bd-57e54aebc5ff.jpg?1584830891", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b574b44-01e1-4197-99bd-57e54aebc5ff.jpg?1584830891"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Golden Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88bb420a-8bf1-4504-b1b5-2d929be978be.jpg?1555040232", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88bb420a-8bf1-4504-b1b5-2d929be978be.jpg?1555040232"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Golgari Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48fce388-eefc-4234-8dd9-1260c1ba97eb.jpg?1562785737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48fce388-eefc-4234-8dd9-1260c1ba97eb.jpg?1562785737"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/9235afe5-0a6b-43c2-921c-18524cf032f1.jpg?1561836885", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/9235afe5-0a6b-43c2-921c-18524cf032f1.jpg?1561836885"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guan Yu's 1,000-Li March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fa7526a-7a4e-4b3d-b96e-91f2bbf1c7bd.jpg?1562257048", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fa7526a-7a4e-4b3d-b96e-91f2bbf1c7bd.jpg?1562257048"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Hail Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7e9d786-4e9b-447b-a5dc-ca117c4961c5.jpg?1562769694", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7e9d786-4e9b-447b-a5dc-ca117c4961c5.jpg?1562769694"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hallowed Burial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c42fad4b-caeb-4aa2-9586-cb26bdec56cd.jpg?1562936481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c42fad4b-caeb-4aa2-9586-cb26bdec56cd.jpg?1562936481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Harsh Mercy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6473b4d-1f59-4216-ace9-f3e5306266fb.jpg?1562937932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6473b4d-1f59-4216-ace9-f3e5306266fb.jpg?1562937932"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hazardous Conditions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daa9b08b-c56f-480e-874e-069e72d979c8.jpg?1576382835", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daa9b08b-c56f-480e-874e-069e72d979c8.jpg?1576382835"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hellfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/362f1fe9-20af-434c-9957-7a1a564d89e6.jpg?1592364391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/362f1fe9-20af-434c-9957-7a1a564d89e6.jpg?1592364391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hellion Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/6529c92e-c79b-4953-8bd0-50ceae2ce261.jpg?1562704497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/6529c92e-c79b-4953-8bd0-50ceae2ce261.jpg?1562704497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hideous Laughter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/941fd135-1c5a-4650-8faf-dfa2c93ec8c9.jpg?1562762525", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/941fd135-1c5a-4650-8faf-dfa2c93ec8c9.jpg?1562762525"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath2.json b/web/public/mtg/jsons/wrath2.json new file mode 100644 index 00000000..ea785118 --- /dev/null +++ b/web/public/mtg/jsons/wrath2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Holy Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3c8a850-bc99-4679-a316-45ecdea696b2.jpg?1592364686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3c8a850-bc99-4679-a316-45ecdea696b2.jpg?1592364686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hostile Takeover", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd7df727-50ea-4ea8-bdb9-d7ef16199d8a.jpg?1649697248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd7df727-50ea-4ea8-bdb9-d7ef16199d8a.jpg?1649697248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hostile Takeover", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8137f134-0148-4df1-b575-ec861192c65c.jpg?1649695787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8137f134-0148-4df1-b575-ec861192c65c.jpg?1649695787"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Hour of Devastation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d420cc12-cfd7-4007-a0c2-b16c8f63a754.jpg?1562816057", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d420cc12-cfd7-4007-a0c2-b16c8f63a754.jpg?1562816057"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hour of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d12768a5-8ee6-407b-87cf-703e69a0c32a.jpg?1568003844", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d12768a5-8ee6-407b-87cf-703e69a0c32a.jpg?1568003844"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Hour of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bec7a987-1ef2-40aa-a744-92d90b246df4.jpg?1598913735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bec7a987-1ef2-40aa-a744-92d90b246df4.jpg?1598913735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Howling Gale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9917cf32-0236-4463-9b1d-e8193754ff97.jpg?1562923428", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9917cf32-0236-4463-9b1d-e8193754ff97.jpg?1562923428"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Hurly-Burly", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a6e0b97-c2a9-4cd6-957e-87e9b22f7b48.jpg?1562354283", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a6e0b97-c2a9-4cd6-957e-87e9b22f7b48.jpg?1562354283"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0526077-79b6-40ae-8178-8b97c33a53fb.jpg?1562250875", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0526077-79b6-40ae-8178-8b97c33a53fb.jpg?1562250875"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b4dd722-4729-444a-9d81-e2e93317fbd5.jpg?1562920277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b4dd722-4729-444a-9d81-e2e93317fbd5.jpg?1562920277"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b97904e-80ba-4d65-808a-a528200430f8.jpg?1562446872", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b97904e-80ba-4d65-808a-a528200430f8.jpg?1562446872"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8cc6db7-1f40-40e3-a7ea-92f1d05e2e3d.jpg?1562926538", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8cc6db7-1f40-40e3-a7ea-92f1d05e2e3d.jpg?1562926538"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52f5a19f-16e4-4d35-89e1-969ac8202f88.jpg?1559591426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52f5a19f-16e4-4d35-89e1-969ac8202f88.jpg?1559591426"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ichor Explosion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b207e2f-4604-43c5-bb35-a877e35ddd81.jpg?1562875473", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b207e2f-4604-43c5-bb35-a877e35ddd81.jpg?1562875473"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Gyre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd0b8aee-fbfb-470f-9ac2-64fce0b4b2fb.jpg?1632261825", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd0b8aee-fbfb-470f-9ac2-64fce0b4b2fb.jpg?1632261825"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Incandescent Aria", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77e2ed9e-ee1d-440a-94b4-d4b17d30b800.jpg?1649801687", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77e2ed9e-ee1d-440a-94b4-d4b17d30b800.jpg?1649801687"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incandescent Aria", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63167d77-a8d5-468f-9132-a5000c57901a.jpg?1649801714", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63167d77-a8d5-468f-9132-a5000c57901a.jpg?1649801714"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Incendiary Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incendiary Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0ee44ca0-1989-42fa-8024-b6b3e5c3883c.jpg?1576382098", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0ee44ca0-1989-42fa-8024-b6b3e5c3883c.jpg?1576382098"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incite Rebellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/628c4a6f-6970-407d-a774-e67bfcdf7ee2.jpg?1561944371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/628c4a6f-6970-407d-a774-e67bfcdf7ee2.jpg?1561944371"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb9dd080-5e13-4334-8614-8eec41ae89c2.jpg?1562711058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb9dd080-5e13-4334-8614-8eec41ae89c2.jpg?1562711058"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7890ba2-aa42-4c8d-bbc1-94fb1d4150fc.jpg?1562938305", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7890ba2-aa42-4c8d-bbc1-94fb1d4150fc.jpg?1562938305"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9350a640-3f22-478f-b463-6b50cfe766e1.jpg?1561757603", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9350a640-3f22-478f-b463-6b50cfe766e1.jpg?1561757603"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Inflame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1efad9a-2fcf-4045-8105-bf9f5e79d12c.jpg?1562640129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1efad9a-2fcf-4045-8105-bf9f5e79d12c.jpg?1562640129"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Inflame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd7bc4c0-9bfd-444b-b22c-f1b7e1426807.jpg?1562933469", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd7bc4c0-9bfd-444b-b22c-f1b7e1426807.jpg?1562933469"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "In Garruk's Wake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6f2c2f6-d07f-42af-9944-70d3dac8348c.jpg?1562797158", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6f2c2f6-d07f-42af-9944-70d3dac8348c.jpg?1562797158"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "In Garruk's Wake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/987ace55-8f39-4d5e-8604-9e99d065b4d5.jpg?1561757636", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/987ace55-8f39-4d5e-8604-9e99d065b4d5.jpg?1561757636"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Inundate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5047c92-2885-4a7b-b51f-f3e093dca5ad.jpg?1562940048", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5047c92-2885-4a7b-b51f-f3e093dca5ad.jpg?1562940048"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jokulhaups", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d81e479-45b7-4237-a0eb-95245582e87d.jpg?1562591373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d81e479-45b7-4237-a0eb-95245582e87d.jpg?1562591373"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Jokulhaups", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bf0d325-5928-4593-8faa-64ffa414cb48.jpg?1562906050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bf0d325-5928-4593-8faa-64ffa414cb48.jpg?1562906050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jund Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a0ddf00-926c-4283-a8b2-daa02fa99b8b.jpg?1562705657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a0ddf00-926c-4283-a8b2-daa02fa99b8b.jpg?1562705657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaervek's Hex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/097910fb-7c48-4535-8ffc-b521d08294b0.jpg?1562717830", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/097910fb-7c48-4535-8ffc-b521d08294b0.jpg?1562717830"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaya's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ed140c1-752b-4539-88f2-1fa354049b17.jpg?1584831638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ed140c1-752b-4539-88f2-1fa354049b17.jpg?1584831638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Killing Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33de2371-175e-4f8a-9636-35f996e3cf24.jpg?1592708920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33de2371-175e-4f8a-9636-35f996e3cf24.jpg?1592708920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Killing Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e40ed6b1-7b92-4ba4-b197-07c3f171a935.jpg?1561758322", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e40ed6b1-7b92-4ba4-b197-07c3f171a935.jpg?1561758322"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Kindle the Carnage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b5dfa91-8f93-41b7-95e9-3374550f1617.jpg?1593273180", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b5dfa91-8f93-41b7-95e9-3374550f1617.jpg?1593273180"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kindred Dominance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9794115a-5509-4d9a-b119-d2b61942e87b.jpg?1562617149", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9794115a-5509-4d9a-b119-d2b61942e87b.jpg?1562617149"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Kirtar's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5a0c4e6-d50e-42e8-b062-8f6ef5950ab7.jpg?1562928851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5a0c4e6-d50e-42e8-b062-8f6ef5950ab7.jpg?1562928851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Klauth's Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/761e1f77-5231-4008-829f-99650b429fb3.jpg?1631585593", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/761e1f77-5231-4008-829f-99650b429fb3.jpg?1631585593"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Kozilek's Return", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72765559-0a78-4aa3-827e-cb4612720991.jpg?1618608556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72765559-0a78-4aa3-827e-cb4612720991.jpg?1618608556"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Languish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3593efa-0a05-4061-9f6e-edd0a5ca9a1f.jpg?1562043520", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3593efa-0a05-4061-9f6e-edd0a5ca9a1f.jpg?1562043520"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Last One Standing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e2ee71-293d-452b-89a5-b15990186f5b.jpg?1562922467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e2ee71-293d-452b-89a5-b15990186f5b.jpg?1562922467"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lavaball Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0d411e1-5488-4818-95a4-9f637efb9be6.jpg?1562616217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0d411e1-5488-4818-95a4-9f637efb9be6.jpg?1562616217"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lavalanche", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Life's Finale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffd3fbd2-87c7-4f08-baaa-91d61c1114da.jpg?1562883140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffd3fbd2-87c7-4f08-baaa-91d61c1114da.jpg?1562883140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Living Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0e73682a-56a2-4796-9902-a03aaa3815e8.jpg?1562897968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0e73682a-56a2-4796-9902-a03aaa3815e8.jpg?1562897968"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Living Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c820476-fbda-4073-baf6-51e71f45ed58.jpg?1562054465", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c820476-fbda-4073-baf6-51e71f45ed58.jpg?1562054465"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Living End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3be0ff69-d9f3-4b81-b02f-1360e4064aff.jpg?1562907448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3be0ff69-d9f3-4b81-b02f-1360e4064aff.jpg?1562907448"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Magmaquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac85679e-17c7-4525-8eed-979d04feb8f1.jpg?1562558550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac85679e-17c7-4525-8eed-979d04feb8f1.jpg?1562558550"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Magmaquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1476d42e-6cf8-4612-ae75-b3044d1eebbe.jpg?1605361705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1476d42e-6cf8-4612-ae75-b3044d1eebbe.jpg?1605361705"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Make Obsolete", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0a96feb-accc-4c30-8ecd-7d9272ebd45b.jpg?1576381736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0a96feb-accc-4c30-8ecd-7d9272ebd45b.jpg?1576381736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Malicious Malfunction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56e7415f-f014-4ece-81db-d8271444d9e9.jpg?1654567289", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56e7415f-f014-4ece-81db-d8271444d9e9.jpg?1654567289"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "March of Souls", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f07dd0f1-b80b-4af0-ae76-907ec55ec7d5.jpg?1562945732", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f07dd0f1-b80b-4af0-ae76-907ec55ec7d5.jpg?1562945732"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Marsh Casualties", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28476d0d-60ea-4d08-890c-0e6502ee3d2a.jpg?1562610765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28476d0d-60ea-4d08-890c-0e6502ee3d2a.jpg?1562610765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Martial Coup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4201385f-6f74-4e3d-aafb-0eff82cb24c1.jpg?1562800634", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4201385f-6f74-4e3d-aafb-0eff82cb24c1.jpg?1562800634"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Martyr's Cry", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2c9f463-d1cc-4f11-aad2-d4a4520aa978.jpg?1562949002", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2c9f463-d1cc-4f11-aad2-d4a4520aa978.jpg?1562949002"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Massacre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f05f5d93-50d1-4aa6-af05-383a6808345b.jpg?1562632742", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f05f5d93-50d1-4aa6-af05-383a6808345b.jpg?1562632742"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mass Calcify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d24be94-9922-43bb-83c8-98090adc3f32.jpg?1562829041", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d24be94-9922-43bb-83c8-98090adc3f32.jpg?1562829041"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mephitic Vapors", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/675640ba-37e7-4231-8524-87e8b87ea46f.jpg?1572892991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/675640ba-37e7-4231-8524-87e8b87ea46f.jpg?1572892991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Merciless Eviction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9876a4c-714b-47e5-9589-148a623af96a.jpg?1561848654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9876a4c-714b-47e5-9589-148a623af96a.jpg?1561848654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mizzium Mortars", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/544b2931-0af1-4743-b7c1-91e1dc9294d5.jpg?1654120304", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/544b2931-0af1-4743-b7c1-91e1dc9294d5.jpg?1654120304"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Mizzium Mortars", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4ded88d-2688-4f5e-a8b2-16216cf9c792.jpg?1562793745", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4ded88d-2688-4f5e-a8b2-16216cf9c792.jpg?1562793745"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mogg Infestation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a91aa6f-cb2f-4aad-9415-bba4eb9b76ca.jpg?1562596412", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a91aa6f-cb2f-4aad-9415-bba4eb9b76ca.jpg?1562596412"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molten Disaster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c48bc86b-df0a-4a9c-8aad-c3ffb742a5ff.jpg?1588005547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c48bc86b-df0a-4a9c-8aad-c3ffb742a5ff.jpg?1588005547"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9cbdabf-18e3-4c0c-b37b-097aaa650066.jpg?1562090221", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9cbdabf-18e3-4c0c-b37b-097aaa650066.jpg?1562090221"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/6189cab3-1963-4590-9cbc-7ab4a693d7c6.jpg?1562629994", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/6189cab3-1963-4590-9cbc-7ab4a693d7c6.jpg?1562629994"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nature's Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/5950f52a-493e-432e-9175-0272c0edb232.jpg?1562446647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/5950f52a-493e-432e-9175-0272c0edb232.jpg?1562446647"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2569173f-df5e-4518-9fb3-f972210595df.jpg?1580014299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2569173f-df5e-4518-9fb3-f972210595df.jpg?1580014299"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b71315e3-14c1-433b-97be-2cdf99213bba.jpg?1562246310", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b71315e3-14c1-433b-97be-2cdf99213bba.jpg?1562246310"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a10531d8-fc99-4a2b-94b0-97a25521d725.jpg?1562088332", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a10531d8-fc99-4a2b-94b0-97a25521d725.jpg?1562088332"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Necromantic Selection", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/9462680e-b83d-44cc-a7a6-505fbc69ab41.jpg?1561950631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/9462680e-b83d-44cc-a7a6-505fbc69ab41.jpg?1561950631"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Needle Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/be80dd2d-f595-4d80-84ae-66d3d18e7399.jpg?1562056388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/be80dd2d-f595-4d80-84ae-66d3d18e7399.jpg?1562056388"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Needle Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29a44e44-94b1-4bd2-8e00-6bd2ec07ee4c.jpg?1562446151", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29a44e44-94b1-4bd2-8e00-6bd2ec07ee4c.jpg?1562446151"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Nightmare Unmaking", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95c0ff1b-bd97-4115-8486-62a18bab2610.jpg?1568003505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95c0ff1b-bd97-4115-8486-62a18bab2610.jpg?1568003505"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Nylea's Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daa2f963-9d16-4224-b24e-b6a79f2b9d75.jpg?1581480794", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daa2f963-9d16-4224-b24e-b6a79f2b9d75.jpg?1581480794"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obliterate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdabde40-2143-4677-b7b4-ea8fbf9b1f25.jpg?1562936357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdabde40-2143-4677-b7b4-ea8fbf9b1f25.jpg?1562936357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oddly Uneven", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/830d5f87-1c8b-414a-a91e-4805f5bdca54.jpg?1562922623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/830d5f87-1c8b-414a-a91e-4805f5bdca54.jpg?1562922623"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Olivia's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98893cc1-f502-4ca6-b6c1-e09fa1f4ef7a.jpg?1641600703", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98893cc1-f502-4ca6-b6c1-e09fa1f4ef7a.jpg?1641600703"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Organic Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fea0f8be-c242-49dd-bae3-0b306107ac0b.jpg?1651655197", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fea0f8be-c242-49dd-bae3-0b306107ac0b.jpg?1651655197"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Outbreak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f43c30d9-23a5-4872-925d-3427f5f57995.jpg?1562940897", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f43c30d9-23a5-4872-925d-3427f5f57995.jpg?1562940897"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oversimplify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56eae179-f850-4661-b3f0-4d10be77ed8a.jpg?1629806259", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56eae179-f850-4661-b3f0-4d10be77ed8a.jpg?1629806259"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Overwhelming Forces", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c56c7fb4-8b7b-40fc-879c-76cfb5d417b8.jpg?1562257531", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c56c7fb4-8b7b-40fc-879c-76cfb5d417b8.jpg?1562257531"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Part the Veil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d870e607-1607-46f3-bc9f-925d0164bcf9.jpg?1562764693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d870e607-1607-46f3-bc9f-925d0164bcf9.jpg?1562764693"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Path of Peril", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0c5449a-d63b-4b22-9432-8f0365c3c4d9.jpg?1643590080", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0c5449a-d63b-4b22-9432-8f0365c3c4d9.jpg?1643590080"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Perish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e47ace1d-73de-44aa-a3fe-2e2a21ebec79.jpg?1562057337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e47ace1d-73de-44aa-a3fe-2e2a21ebec79.jpg?1562057337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Perplexing Test", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31f2cbcc-d5b8-4659-ae51-e567c555a743.jpg?1625191389", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31f2cbcc-d5b8-4659-ae51-e567c555a743.jpg?1625191389"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Pestilent Haze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08b78aa8-a63a-4aa2-bb82-3fbf2595ed7c.jpg?1594736339", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08b78aa8-a63a-4aa2-bb82-3fbf2595ed7c.jpg?1594736339"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Phyrexian Rebirth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36b7536d-6b0b-4906-ba88-7fcfe9b854ee.jpg?1562610586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36b7536d-6b0b-4906-ba88-7fcfe9b854ee.jpg?1562610586"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Wind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0d4bd20-7422-45ed-aa76-3ef055c556e7.jpg?1562927896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0d4bd20-7422-45ed-aa76-3ef055c556e7.jpg?1562927896"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Planar Despair", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3a92d454-3f23-45bf-921f-25b0da4ce138.jpg?1562908776", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3a92d454-3f23-45bf-921f-25b0da4ce138.jpg?1562908776"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Planar Outburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f34f930-a7c6-400d-b6e8-b9908e0f0404.jpg?1562917450", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f34f930-a7c6-400d-b6e8-b9908e0f0404.jpg?1562917450"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Promise of Loyalty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc21e7d5-3641-47fe-add0-8becf5173e28.jpg?1625191123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc21e7d5-3641-47fe-add0-8becf5173e28.jpg?1625191123"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Psychotic Haze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d3f6cd2-0138-40e7-a975-3f7c68db0d93.jpg?1562630817", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d3f6cd2-0138-40e7-a975-3f7c68db0d93.jpg?1562630817"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Puppet's Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/052b743a-456d-49c3-881e-4f30c7645fa5.jpg?1562378946", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/052b743a-456d-49c3-881e-4f30c7645fa5.jpg?1562378946"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34ec6e8f-a8be-4efe-8082-d807378066b1.jpg?1562905712", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34ec6e8f-a8be-4efe-8082-d807378066b1.jpg?1562905712"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7afce33f-2ead-4943-9655-bff6eaa9fe6b.jpg?1562241054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7afce33f-2ead-4943-9655-bff6eaa9fe6b.jpg?1562241054"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de214247-e5e3-4d8f-935a-797218416be1.jpg?1562448294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de214247-e5e3-4d8f-935a-797218416be1.jpg?1562448294"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88040748-ad76-4b9a-bd4e-87e5980e9816.jpg?1562920179", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88040748-ad76-4b9a-bd4e-87e5980e9816.jpg?1562920179"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0581322-d901-465e-b22c-cd99ddbb4839.jpg?1561758268", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0581322-d901-465e-b22c-cd99ddbb4839.jpg?1561758268"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiant Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70f4fe69-c541-4320-9074-9c6a3bc70ea3.jpg?1562921619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70f4fe69-c541-4320-9074-9c6a3bc70ea3.jpg?1562921619"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Radiant Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2548487e-a355-4a05-acbc-3031d98f4289.jpg?1562132516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2548487e-a355-4a05-acbc-3031d98f4289.jpg?1562132516"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiating Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94454128-92f1-475d-abc4-c235f501eeb6.jpg?1562739709", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94454128-92f1-475d-abc4-c235f501eeb6.jpg?1562739709"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rain of Daggers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb09a5bb-9730-43cd-8dea-3842634c9983.jpg?1562939110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb09a5bb-9730-43cd-8dea-3842634c9983.jpg?1562939110"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rain of Embers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rancid Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23d07a96-85ba-4714-94a5-4a8125954f58.jpg?1562628959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23d07a96-85ba-4714-94a5-4a8125954f58.jpg?1562628959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reckless Endeavor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba98a4bd-e217-4dba-aee9-315b4f843cdf.jpg?1631585239", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba98a4bd-e217-4dba-aee9-315b4f843cdf.jpg?1631585239"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Reign of Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bd83049-aec1-4911-bc70-39adba04b174.jpg?1587856923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bd83049-aec1-4911-bc70-39adba04b174.jpg?1587856923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Retaliate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58acdda6-6754-46f2-ad68-f1580b8ab0dd.jpg?1562877159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58acdda6-6754-46f2-ad68-f1580b8ab0dd.jpg?1562877159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Retribution of the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/860b8633-1bfc-426a-8666-5e6a584d4525.jpg?1587857186", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/860b8633-1bfc-426a-8666-5e6a584d4525.jpg?1587857186"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Righteous Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c408f43e-9092-440d-a15f-bef4ad58bcc6.jpg?1562941331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c408f43e-9092-440d-a15f-bef4ad58bcc6.jpg?1562941331"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rising Miasma", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f9a8e87-3b8b-4dbf-9c1e-0a3290a33a0b.jpg?1562913657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f9a8e87-3b8b-4dbf-9c1e-0a3290a33a0b.jpg?1562913657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ritual of Soot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/269af993-4894-4bf1-b55a-af4d736cb3cc.jpg?1572893045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/269af993-4894-4bf1-b55a-af4d736cb3cc.jpg?1572893045"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Riveteers Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb15ba71-c3b3-4a9f-b000-bd788514211c.jpg?1650549265", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb15ba71-c3b3-4a9f-b000-bd788514211c.jpg?1650549265"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Rollick of Abandon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f1a80c4-8119-437d-bf5b-549c5679c90a.jpg?1593096073", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f1a80c4-8119-437d-bf5b-549c5679c90a.jpg?1593096073"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rolling Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rolling Spoil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6c5546f-2429-4099-a9bd-eda3f52779b7.jpg?1598916497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6c5546f-2429-4099-a9bd-eda3f52779b7.jpg?1598916497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rolling Temblor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/060ce982-94dd-4b9e-b240-15da297e29f9.jpg?1562825667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/060ce982-94dd-4b9e-b240-15da297e29f9.jpg?1562825667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c3da3f0-bf90-461a-b62d-5c00d5c9aebd.jpg?1562865454", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c3da3f0-bf90-461a-b62d-5c00d5c9aebd.jpg?1562865454"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Rout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94bc55ed-b89b-4e22-b3f1-4ce0f8d180d7.jpg?1562924999", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94bc55ed-b89b-4e22-b3f1-4ce0f8d180d7.jpg?1562924999"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db53c1fb-3641-44a3-b0b4-b7b2ba993646.jpg?1562632349", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db53c1fb-3641-44a3-b0b4-b7b2ba993646.jpg?1562632349"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2a2a4e7-3173-4b73-8898-2c668f9eebf9.jpg?1562548310", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2a2a4e7-3173-4b73-8898-2c668f9eebf9.jpg?1562548310"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Sagittars' Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Savage Alliance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5255da8-8511-48a7-98e5-ba43ca6e8681.jpg?1576384658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5255da8-8511-48a7-98e5-ba43ca6e8681.jpg?1576384658"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99d22b83-381d-47da-b983-8f77d19b0c01.jpg?1562927484", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99d22b83-381d-47da-b983-8f77d19b0c01.jpg?1562927484"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/682ee5a9-2995-4868-b7ea-8735b2aee77e.jpg?1593272763", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/682ee5a9-2995-4868-b7ea-8735b2aee77e.jpg?1593272763"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb73313b-d39a-46ab-abfc-76f94a75dfca.jpg?1593014734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb73313b-d39a-46ab-abfc-76f94a75dfca.jpg?1593014734"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scouring Sands", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/273f25fc-9c9f-4b73-a28b-1461d8fcd443.jpg?1593092358", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/273f25fc-9c9f-4b73-a28b-1461d8fcd443.jpg?1593092358"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sculpted Sunburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d16d8fe-a770-4bbd-bf20-447c0165de5a.jpg?1654291825", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d16d8fe-a770-4bbd-bf20-447c0165de5a.jpg?1654291825"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Seismic Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9b952e4e-c1ed-4455-90d5-46b56478e6b0.jpg?1562790481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9b952e4e-c1ed-4455-90d5-46b56478e6b0.jpg?1562790481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seismic Shudder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20365082-6102-4e3b-8791-c9b66846270d.jpg?1562610483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20365082-6102-4e3b-8791-c9b66846270d.jpg?1562610483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seismic Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Settle the Wreckage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9cbd346e-098a-4cf6-a72f-468376fd2e8f.jpg?1562560853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9cbd346e-098a-4cf6-a72f-468376fd2e8f.jpg?1562560853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shadowstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/367c4ad6-973d-47ba-9431-312f9f2996f6.jpg?1562053739", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/367c4ad6-973d-47ba-9431-312f9f2996f6.jpg?1562053739"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shadows' Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52470883-b44d-415b-9324-8074e66f79ae.jpg?1604196514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52470883-b44d-415b-9324-8074e66f79ae.jpg?1604196514"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shake the Foundations", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5bd4bdd-3a2a-40d9-9f86-fefe0a462cd2.jpg?1555040519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5bd4bdd-3a2a-40d9-9f86-fefe0a462cd2.jpg?1555040519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shatter the Sky", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b706977b-db8e-4810-882d-ed3745404489.jpg?1581479244", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b706977b-db8e-4810-882d-ed3745404489.jpg?1581479244"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shrivel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a87c80a1-5818-45fd-9a37-a2ee3396626e.jpg?1562707116", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a87c80a1-5818-45fd-9a37-a2ee3396626e.jpg?1562707116"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sickening Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9396ac77-9f53-46bd-b126-02441a0f5594.jpg?1562630974", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9396ac77-9f53-46bd-b126-02441a0f5594.jpg?1562630974"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Simoon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/84b1930d-2e4b-472f-98a9-008fd632f3be.jpg?1562921826", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/84b1930d-2e4b-472f-98a9-008fd632f3be.jpg?1562921826"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Simoon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/642d9239-82e0-4696-ad99-10796042d1f8.jpg?1587913163", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/642d9239-82e0-4696-ad99-10796042d1f8.jpg?1587913163"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Single Combat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce0e7c6a-e628-4327-a16f-2062c5a662df.jpg?1557576066", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce0e7c6a-e628-4327-a16f-2062c5a662df.jpg?1557576066"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyreaping", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40eb76b3-b527-4ed8-8ce3-d3de48562b6e.jpg?1593092666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40eb76b3-b527-4ed8-8ce3-d3de48562b6e.jpg?1593092666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slagstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slash the Ranks", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/0913a5e8-7f77-44f2-a7cf-c8c0d6270a86.jpg?1608909011", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/0913a5e8-7f77-44f2-a7cf-c8c0d6270a86.jpg?1608909011"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Slaughter the Strong", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c9f8aea-0c9a-4686-b551-35e2a72ef701.jpg?1653521934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c9f8aea-0c9a-4686-b551-35e2a72ef701.jpg?1653521934"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Slaughter the Strong", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4217ab21-181e-4c32-97c3-d8bd441287e0.jpg?1555039791", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4217ab21-181e-4c32-97c3-d8bd441287e0.jpg?1555039791"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slice and Dice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59262684-86e3-4485-9e35-202771c3eaa6.jpg?1562916006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59262684-86e3-4485-9e35-202771c3eaa6.jpg?1562916006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Solar Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb72ba0f-ab3a-41e6-906d-a84039efa0af.jpg?1557577261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb72ba0f-ab3a-41e6-906d-a84039efa0af.jpg?1557577261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Solar Tide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57ce33b6-267f-4ee8-a3f7-f41c619d0cfa.jpg?1562144484", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57ce33b6-267f-4ee8-a3f7-f41c619d0cfa.jpg?1562144484"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soulquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b3a7470-b93e-4c3a-ab1c-0a4dd401e95a.jpg?1562641103", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b3a7470-b93e-4c3a-ab1c-0a4dd401e95a.jpg?1562641103"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spectral Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7238c46e-6338-4aca-96f2-934c44c8cc36.jpg?1631233619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7238c46e-6338-4aca-96f2-934c44c8cc36.jpg?1631233619"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spontaneous Combustion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55d50177-736a-44d6-a2a3-f6892d7037b3.jpg?1562865429", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55d50177-736a-44d6-a2a3-f6892d7037b3.jpg?1562865429"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Spontaneous Combustion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34e6c04f-9d1a-497b-bc96-a0e48a1c1904.jpg?1562053293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34e6c04f-9d1a-497b-bc96-a0e48a1c1904.jpg?1562053293"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46460e5f-2756-486b-99a6-c3a9a209bfaa.jpg?1594065372", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46460e5f-2756-486b-99a6-c3a9a209bfaa.jpg?1594065372"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5409b54-66ed-4add-bf43-cfeb074b1c50.jpg?1562383517", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5409b54-66ed-4add-bf43-cfeb074b1c50.jpg?1562383517"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63c1b2f6-e47f-4f18-a94a-1d08eb009ef3.jpg?1594065383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63c1b2f6-e47f-4f18-a94a-1d08eb009ef3.jpg?1594065383"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Squall Line", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f368729-a6f2-4bf7-8b06-39c551f0b24a.jpg?1562908127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f368729-a6f2-4bf7-8b06-39c551f0b24a.jpg?1562908127"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Star of Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/021f57dc-80f3-4ede-99d5-4a44aade44e2.jpg?1562549822", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/021f57dc-80f3-4ede-99d5-4a44aade44e2.jpg?1562549822"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Starstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/439aa3eb-fa1f-46b2-a13a-369b6a88d97c.jpg?1562908992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/439aa3eb-fa1f-46b2-a13a-369b6a88d97c.jpg?1562908992"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Starstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b54d72ba-05ce-4299-a7c3-a9e9f126fffb.jpg?1562937719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b54d72ba-05ce-4299-a7c3-a9e9f126fffb.jpg?1562937719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Steam Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stench of Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9a45644-549a-4eaa-8367-b170027bd5a2.jpg?1562770859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9a45644-549a-4eaa-8367-b170027bd5a2.jpg?1562770859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stench of Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4b93845-f17a-4892-a1ce-a4630dced218.jpg?1562770150", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4b93845-f17a-4892-a1ce-a4630dced218.jpg?1562770150"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stick Together", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d77a57a-e30b-46d7-acb8-1d164c7dff78.jpg?1654036983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d77a57a-e30b-46d7-acb8-1d164c7dff78.jpg?1654036983"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Storm's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4bc9ecd2-7664-471b-90f2-2d0dd1acec80.jpg?1581480444", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4bc9ecd2-7664-471b-90f2-2d0dd1acec80.jpg?1581480444"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Strategy, Schmategy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Street Spasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f599948f-1561-415f-b415-c9c991896704.jpg?1592713487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f599948f-1561-415f-b415-c9c991896704.jpg?1592713487"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Structural Assault", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cbdb50e3-fe15-4431-b9bd-c4de65820734.jpg?1650025267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cbdb50e3-fe15-4431-b9bd-c4de65820734.jpg?1650025267"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sublime Exhalation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac6d4a9e-a7fd-480e-96cf-5cf6d2390189.jpg?1562414946", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac6d4a9e-a7fd-480e-96cf-5cf6d2390189.jpg?1562414946"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Subterranean Tremors", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b9510b8-6601-4116-8713-ff7649c000eb.jpg?1576381980", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b9510b8-6601-4116-8713-ff7649c000eb.jpg?1576381980"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath3.json b/web/public/mtg/jsons/wrath3.json new file mode 100644 index 00000000..19e27341 --- /dev/null +++ b/web/public/mtg/jsons/wrath3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Sudden Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7217afaa-00e1-45a7-bb7f-66a770487b77.jpg?1562918949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7217afaa-00e1-45a7-bb7f-66a770487b77.jpg?1562918949"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Suffocating Fumes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66b562e4-35df-4aee-848d-ceb4204bbe58.jpg?1591226972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66b562e4-35df-4aee-848d-ceb4204bbe58.jpg?1591226972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sulfurous Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sunscour", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44c726db-a30a-4e76-9fbf-ec6d5cd7a1ba.jpg?1593274832", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44c726db-a30a-4e76-9fbf-ec6d5cd7a1ba.jpg?1593274832"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e9648f9-7a67-4717-bca1-861d1f7fed43.jpg?1562786100", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e9648f9-7a67-4717-bca1-861d1f7fed43.jpg?1562786100"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2b760cc-800a-48a3-97d9-316e1eeafd4c.jpg?1655619437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2b760cc-800a-48a3-97d9-316e1eeafd4c.jpg?1655619437"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35e3b17c-1af9-4a6d-9cbe-e9d23ea52c53.jpg?1562497060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35e3b17c-1af9-4a6d-9cbe-e9d23ea52c53.jpg?1562497060"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sweltering Suns", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f11cd406-c6ae-4018-ae45-4e5577aa82ae.jpg?1543675701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f11cd406-c6ae-4018-ae45-4e5577aa82ae.jpg?1543675701"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swirling Sandstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d757ec3-c15f-4d6e-8e18-36ebae985448.jpg?1562629788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d757ec3-c15f-4d6e-8e18-36ebae985448.jpg?1562629788"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Synthetic Destiny", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6ab025e6-9ee7-45f0-b829-199637eb0038.jpg?1562705395", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6ab025e6-9ee7-45f0-b829-199637eb0038.jpg?1562705395"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Take Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8e702db-8c73-4947-9c13-5dcb50f4efab.jpg?1576382690", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8e702db-8c73-4947-9c13-5dcb50f4efab.jpg?1576382690"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/0982ea7e-05a4-4e40-98ab-ea9aa6c7342e.jpg?1592708421", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/0982ea7e-05a4-4e40-98ab-ea9aa6c7342e.jpg?1592708421"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Thunder of Hooves", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e4f796a-6831-4d83-824d-88fd2148b4c1.jpg?1562932440", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e4f796a-6831-4d83-824d-88fd2148b4c1.jpg?1562932440"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderwave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9531098-63ea-4568-81e9-80e00a5f8995.jpg?1653417329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9531098-63ea-4568-81e9-80e00a5f8995.jpg?1653417329"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Time Wipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62c59475-6f15-48d2-b105-f49901f20d44.jpg?1557577308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62c59475-6f15-48d2-b105-f49901f20d44.jpg?1557577308"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Time Wipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6ab4b490-67d8-4f13-86cb-858a8012a46a.jpg?1558324717", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6ab4b490-67d8-4f13-86cb-858a8012a46a.jpg?1558324717"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Tivadar's Crusade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b6da540-6803-47e5-9af0-7ae8e2f84b6c.jpg?1562927916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b6da540-6803-47e5-9af0-7ae8e2f84b6c.jpg?1562927916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Torrent of Lava", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19528a24-4968-4742-a2d1-06f94e60f290.jpg?1562718298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19528a24-4968-4742-a2d1-06f94e60f290.jpg?1562718298"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db34617f-b04f-4b65-84cf-5c5be1eb7226.jpg?1651951814", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db34617f-b04f-4b65-84cf-5c5be1eb7226.jpg?1651951814"}, "reprint": true, "digital": false, "set_type": "arsenal"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/564caf57-4ba5-4993-a35e-945699c94eb7.jpg?1562913020", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/564caf57-4ba5-4993-a35e-945699c94eb7.jpg?1562913020"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73731e45-51bb-4188-a54d-fdaa4bdfaf1f.jpg?1599711037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73731e45-51bb-4188-a54d-fdaa4bdfaf1f.jpg?1599711037"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b281c013-b35a-4c4a-aaee-b6f93968485c.jpg?1562246219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b281c013-b35a-4c4a-aaee-b6f93968485c.jpg?1562246219"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/8531efb1-d77d-451a-8621-424fc278ccf9.jpg?1562381834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/8531efb1-d77d-451a-8621-424fc278ccf9.jpg?1562381834"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2509285-a88e-4f5c-86c1-c0386da0f0c5.jpg?1562948885", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2509285-a88e-4f5c-86c1-c0386da0f0c5.jpg?1562948885"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9d64665-c1e0-40ab-a358-247f82966379.jpg?1562278171", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9d64665-c1e0-40ab-a358-247f82966379.jpg?1562278171"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tropical Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd5f473c-e11e-4047-91f9-81b80f0a3562.jpg?1587912688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd5f473c-e11e-4047-91f9-81b80f0a3562.jpg?1587912688"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tsabo's Decree", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c1a0ebd-1add-49e6-b5e6-5b26abb1de88.jpg?1562897461", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c1a0ebd-1add-49e6-b5e6-5b26abb1de88.jpg?1562897461"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Underworld Fires", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0fe616c4-dcb0-4284-ba10-6fbf7cecd217.jpg?1581480512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0fe616c4-dcb0-4284-ba10-6fbf7cecd217.jpg?1581480512"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Urborg Justice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/39f322ff-0b04-41ce-90cd-9896f941e703.jpg?1562800263", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/39f322ff-0b04-41ce-90cd-9896f941e703.jpg?1562800263"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valiant Endeavor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/0445def0-8921-4579-912f-035d9fbce3c0.jpg?1631584806", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/0445def0-8921-4579-912f-035d9fbce3c0.jpg?1631584806"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Vampires' Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20d2d886-13a2-44f1-966a-ec674622fd01.jpg?1643592028", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20d2d886-13a2-44f1-966a-ec674622fd01.jpg?1643592028"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vampires' Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f4ba693-0323-415d-ad91-c083fbbab7f7.jpg?1645228860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f4ba693-0323-415d-ad91-c083fbbab7f7.jpg?1645228860"}, "flavor_name": "Mysterious Blood Illness", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e264615c-eb99-4cb3-844a-2b4a94ba5203.jpg?1634348651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e264615c-eb99-4cb3-844a-2b4a94ba5203.jpg?1634348651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Virtue's Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/7854928a-d467-4616-b96b-de7e5fe7303e.jpg?1562446869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/7854928a-d467-4616-b96b-de7e5fe7303e.jpg?1562446869"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0b3e320-a85c-4d92-944e-0a5e78a066a5.jpg?1580015114", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0b3e320-a85c-4d92-944e-0a5e78a066a5.jpg?1580015114"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62dc1df7-b9db-4f5f-a340-08287cd3d9e5.jpg?1562915020", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62dc1df7-b9db-4f5f-a340-08287cd3d9e5.jpg?1562915020"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a80582b1-09db-45f8-b362-0e5207a5a8e6.jpg?1559591541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a80582b1-09db-45f8-b362-0e5207a5a8e6.jpg?1559591541"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Volcanic Spray", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Volcanic Torrent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb586da6-670d-4c50-9d9b-f320f1c288d7.jpg?1608910472", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb586da6-670d-4c50-9d9b-f320f1c288d7.jpg?1608910472"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Volcanic Vision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f973e1a6-c6f9-47f5-9bf0-b7fa06959bd4.jpg?1625194143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f973e1a6-c6f9-47f5-9bf0-b7fa06959bd4.jpg?1625194143"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Volcanic Vision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/979da13b-9be6-49cc-a62c-67eeea289612.jpg?1562790292", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/979da13b-9be6-49cc-a62c-67eeea289612.jpg?1562790292"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wail of the Nim", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8c32faa-c6d1-418a-aed6-ccc5849daa1f.jpg?1562153645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8c32faa-c6d1-418a-aed6-ccc5849daa1f.jpg?1562153645"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wave of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27d6655d-f55c-4bfc-a9c6-10232ebc707b.jpg?1562392689", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27d6655d-f55c-4bfc-a9c6-10232ebc707b.jpg?1562392689"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Wave of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b101b5e-d478-4686-b3cf-bdc545f089e5.jpg?1562378964", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b101b5e-d478-4686-b3cf-bdc545f089e5.jpg?1562378964"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whelming Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fcabd4c7-093f-4ef6-8b89-b08565c48e3c.jpg?1593091836", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fcabd4c7-093f-4ef6-8b89-b08565c48e3c.jpg?1593091836"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whipflare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a7e6c10-d066-4967-932f-5b6c8d74568b.jpg?1562877860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a7e6c10-d066-4967-932f-5b6c8d74568b.jpg?1562877860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8101bab4-ef93-451a-a24f-e1456c82837c.jpg?1562922208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8101bab4-ef93-451a-a24f-e1456c82837c.jpg?1562922208"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Widespread Brutality", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97715d22-f432-4f67-b4ea-47b8fe6edca5.jpg?1557577331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97715d22-f432-4f67-b4ea-47b8fe6edca5.jpg?1557577331"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/826fd527-9356-4eec-8542-781116f23eb7.jpg?1562241689", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/826fd527-9356-4eec-8542-781116f23eb7.jpg?1562241689"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72d50972-4549-40cd-9c33-4b341333803f.jpg?1562919111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72d50972-4549-40cd-9c33-4b341333803f.jpg?1562919111"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b69cfcb0-db68-4494-a3e1-7c2ca279fcf5.jpg?1562938018", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b69cfcb0-db68-4494-a3e1-7c2ca279fcf5.jpg?1562938018"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Winds of Abandon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bb17913-fe4d-4acd-9b75-71f5a90f898b.jpg?1562201278", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bb17913-fe4d-4acd-9b75-71f5a90f898b.jpg?1562201278"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Winds of Rath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6d731b2-0113-4fd5-8b78-1aa1064bb4f5.jpg?1562055907", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6d731b2-0113-4fd5-8b78-1aa1064bb4f5.jpg?1562055907"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Windstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/154dc31c-ac9d-4b78-b92b-e7bacc532915.jpg?1562782948", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/154dc31c-ac9d-4b78-b92b-e7bacc532915.jpg?1562782948"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Windstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee3768ec-bb3b-44dc-9fa3-7cb3d3ee9f8c.jpg?1562000543", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee3768ec-bb3b-44dc-9fa3-7cb3d3ee9f8c.jpg?1562000543"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Winter Sky", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Witch's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbf16457-3444-4130-b220-834b69d9faa3.jpg?1572490276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbf16457-3444-4130-b220-834b69d9faa3.jpg?1572490276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d223e83-0d3c-459e-96f5-ba9227fe49dd.jpg?1562232378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d223e83-0d3c-459e-96f5-ba9227fe49dd.jpg?1562232378"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d75d8204-6f9d-4a7a-bb8b-d51ac65a30fa.jpg?1562447853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d75d8204-6f9d-4a7a-bb8b-d51ac65a30fa.jpg?1562447853"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2788d69-6a3a-42f0-8736-cc6b57755ecd.jpg?1559591620", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2788d69-6a3a-42f0-8736-cc6b57755ecd.jpg?1559591620"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0adf3831-93d9-4995-b8c8-0d8c03fee872.jpg?1657809849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0adf3831-93d9-4995-b8c8-0d8c03fee872.jpg?1657809849"}, "flavor_name": "Shrinking Storm", "reprint": true, "digital": false, "set_type": "box"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a91be77a-bd5b-485f-b5ca-0e6148c236ca.jpg?1619340331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a91be77a-bd5b-485f-b5ca-0e6148c236ca.jpg?1619340331"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/4351bf97-0b9e-44a5-bb7c-1098a683b18d.jpg?1562908574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/4351bf97-0b9e-44a5-bb7c-1098a683b18d.jpg?1562908574"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98890cd7-ebd5-4fea-814e-4f612abfe3a5.jpg?1560576455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98890cd7-ebd5-4fea-814e-4f612abfe3a5.jpg?1560576455"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10c5810f-83f6-43bf-8ece-047be42d7d58.jpg?1561756672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10c5810f-83f6-43bf-8ece-047be42d7d58.jpg?1561756672"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Yahenni's Expertise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2f28735-122c-45ba-bde5-decfd9b11b32.jpg?1576381752", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2f28735-122c-45ba-bde5-decfd9b11b32.jpg?1576381752"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yahenni's Expertise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f4ddbb7-b317-44dc-bb3d-52f52c0a8f96.jpg?1562270938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f4ddbb7-b317-44dc-bb3d-52f52c0a8f96.jpg?1562270938"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Yamabushi's Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0a5a930d-ae59-47e2-9b98-f703e308b5c0.jpg?1562757474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0a5a930d-ae59-47e2-9b98-f703e308b5c0.jpg?1562757474"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zealous Persecution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07d8ae46-14ec-4878-ba8a-a47d4508c6d7.jpg?1562639500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07d8ae46-14ec-4878-ba8a-a47d4508c6d7.jpg?1562639500"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zealous Persecution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/00963993-ff4d-4cc6-a7e0-ed8adac40bfd.jpg?1562895154", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/00963993-ff4d-4cc6-a7e0-ed8adac40bfd.jpg?1562895154"}, "reprint": true, "digital": false, "set_type": "box"}]} \ No newline at end of file From c3b825cc447cae8ed56dddd65dd507d0ed2b2ea9 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 16:59:40 -0700 Subject: [PATCH 286/519] Adjust card positioning --- web/public/mtg/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 8ca9264c..722e4714 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -66,7 +66,7 @@ background-size: 220px; background-repeat: no-repeat; transition: height 1s, background-image 1s, border 0.4s 0.6s; - background-position-y: calc(50% - 20px); + background-position-y: calc(50% - 18px); } .card:not([data-name^='name'])::after { From 6b5b9b42f5a86f642c9966645560f2f8160454ad Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 17:14:49 -0700 Subject: [PATCH 287/519] Make the select screen index.html --- web/public/mtg/choose.html | 225 --------------- web/public/mtg/guess.html | 554 ++++++++++++++++++++++++++++++++++++ web/public/mtg/index.html | 565 ++++++++----------------------------- 3 files changed, 672 insertions(+), 672 deletions(-) delete mode 100644 web/public/mtg/choose.html create mode 100644 web/public/mtg/guess.html diff --git a/web/public/mtg/choose.html b/web/public/mtg/choose.html deleted file mode 100644 index cb84ced5..00000000 --- a/web/public/mtg/choose.html +++ /dev/null @@ -1,225 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <!-- Google Tag Manager --> - <script> - ;(function (w, d, s, l, i) { - w[l] = w[l] || [] - w[l].push({ - 'gtm.start': new Date().getTime(), - event: 'gtm.js', - }) - var f = d.getElementsByTagName(s)[0], - j = d.createElement(s), - dl = l !== 'dataLayer' ? '&l=' + l : '' - j.async = true - j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl - f.parentNode.insertBefore(j, f) - })(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG') - </script> - <!-- End Google Tag Manager --> - <meta charset="UTF-8" /> - <style type="text/css"> - body { - position: relative; - } - - .play-page { - display: flex; - flex-direction: row-reverse; - font-family: Georgia, 'Times New Roman', Times, serif; - min-height: 200px; - } - - h1, - h3 { - font-family: Verdana, Geneva, Tahoma, sans-serif; - text-align: center; - } - - #submit { - margin-top: 10px; - padding: 8px 20px; - background-color: cadetblue; - border: none; - border-radius: 3px; - font-size: 1.1em; - color: white; - cursor: pointer; - } - - #submit:hover { - background-color: rgb(0, 146, 156); - } - - [type='radio'] { - display: none; - } - - [type='radio'] + label.radio-label { - background: lightgrey; - display: block; - padding: 10px; - border-radius: 4px; - cursor: pointer; - } - - label.radio-label:hover { - background: darkgrey; - } - - [type='radio']:checked + label.radio-label { - background: lightcoral; - } - - .radio-label h3 { - margin: 0; - display: inline-block; - vertical-align: middle; - width: 220px; - } - - .thumbnail { - display: inline-block; - vertical-align: middle; - width: 67px; - height: 48px; - margin-right: 4px; - } - - body { - padding: 70px 0 30px; - } - - #addl-options { - position: absolute; - top: 30px; - right: 30px; - background-color: white; - padding: 10px; - cursor: pointer; - width: 200px; - } - - #addl-options > summary { - list-style: none; - text-align: right; - } - </style> - </head> - <body> - <!-- Google Tag Manager (noscript) --> - <noscript> - <iframe - src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG" - height="0" - width="0" - style="display: none; visibility: hidden" - ></iframe> - </noscript> - <!-- End Google Tag Manager (noscript) --> - <h1>Magic the Guessering</h1> - <div class="play-page" style="justify-content: center"> - <form - method="get" - action="index.html" - style="display: flex; flex-direction: column; align-items: center" - > - <!-- <input type="radio" id="wrath" name="whichguesser" value="wrath" /> - <label class="radio-label" for="wrath"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0619d670-7b53-4185-a25d-2fab5db1aab5.jpg?1562896185" - /> - <h3>I'll Clean Sweep</h3></label - ><br /> --> - - <input - type="radio" - id="counterspell" - name="whichguesser" - value="counterspell" - checked - /> - <label class="radio-label" for="counterspell"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855" - /> - <h3>Counterspell Guesser</h3></label - ><br /> - - <!-- <input type="radio" id="terror" name="whichguesser" value="terror" /> - <label class="radio-label" for="terror"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dd5d601-aff7-4b7a-ab6c-b89f403af076.jpg?1562905752" - /> - <h3>I'm a Terror-able Guesser</h3></label - ><br /> --> - - <input type="radio" id="burn" name="whichguesser" value="burn" /> - <label class="radio-label" for="burn"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596" - /> - <h3>Match With Hot Singles</h3></label - ><br /> - - <!-- <input type="radio" id="beast" name="whichguesser" value="beast" /> - <label class="radio-label" for="beast"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469" - /> - <h3>Finding Fantastic Beasts</h3></label - > - <br /> --> - - <details id="addl-options"> - <summary> - <img - src="http://mythicspoiler.com/images/buttons/ustset.png" - style="width: 32px; vertical-align: top" - /> - Options - </summary> - <input type="checkbox" name="digital" id="digital" checked /> - <label for="digital">include digital cards</label> - <br /> - <input type="checkbox" name="un" id="un" checked /> - <label for="un">include un-cards</label> - <br /> - <input type="checkbox" name="original" id="original" /> - <label for="original">restrict to only original printing</label> - </details> - <input type="submit" id="submit" value="Play" /> - </form> - </div> - - <div style="margin: -40px 0 0; height: 60px"> - <a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a> - </div> - - <div - style=" - font-size: 0.9em; - position: absolute; - bottom: 0; - left: 0; - right: 0; - color: grey; - font-style: italic; - " - > - made by - <a - style="color: rgb(0, 146, 156); font-style: italic" - href="https://idamayer.com" - >Ida Mayer</a - > - & Alex Lien 2022 - </div> - </body> -</html> diff --git a/web/public/mtg/guess.html b/web/public/mtg/guess.html new file mode 100644 index 00000000..f0045f08 --- /dev/null +++ b/web/public/mtg/guess.html @@ -0,0 +1,554 @@ +<!DOCTYPE html> +<html> + <head> + <!-- Google Tag Manager --> + <script> + ;(function (w, d, s, l, i) { + w[l] = w[l] || [] + w[l].push({ + 'gtm.start': new Date().getTime(), + event: 'gtm.js', + }) + var f = d.getElementsByTagName(s)[0], + j = d.createElement(s), + dl = l !== 'dataLayer' ? '&l=' + l : '' + j.async = true + j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl + f.parentNode.insertBefore(j, f) + })(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG') + </script> + <!-- End Google Tag Manager --> + <meta charset="UTF-8" /> + <script type="text/javascript" src="app.js"></script> + <style type="text/css"> + body { + position: relative; + } + + .play-page { + display: flex; + flex-direction: row-reverse; + font-family: Georgia, 'Times New Roman', Times, serif; + } + + h1 { + font-family: Verdana, Geneva, Tahoma, sans-serif; + text-align: center; + } + + form { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-right: 240px; + } + + .cards-container { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + } + + .card { + width: 230px; + height: 208px; + border: 5px solid lightgrey; + margin: 5px; + align-items: flex-end; + box-sizing: border-box; + border-radius: 11px; + position: relative; + display: flex; + justify-content: center; + /*background-size: contain;*/ + background-size: 220px; + background-repeat: no-repeat; + transition: height 1s, background-image 1s, border 0.4s 0.6s; + background-position-y: calc(50% - 18px); + } + + .card:not([data-name^='name'])::after { + content: ''; + height: 34px; + background: white; + width: 100%; + } + + .answer-page .card { + height: 350px; + /*padding-top: 310px;*/ + /*background-size: cover;*/ + overflow: hidden; + border-color: rgb(0, 146, 156); + } + + .answer-page .card.incorrect { + border-color: rgb(216, 27, 96); + } + + .names-bank { + position: fixed; + padding: 10px 10px 40px; + } + + .names-bank .name { + margin: 6px 0; + } + + .answer-page .names-bank .name { + display: none; + } + + .answer-page .names-bank .word-count { + display: none; + } + + .word-count { + text-align: center; + font-style: italic; + color: #444; + } + + .score { + width: 100%; + text-align: center; + background-color: rgb(255, 193, 7); + width: 200px; + font-family: Verdana, Geneva, Tahoma, sans-serif; + opacity: 0; + } + + .names-bank .score { + overflow: hidden; + height: 0; + } + + .answer-page .names-bank .score { + height: auto; + display: block; + opacity: 1; + transition: opacity 1.2s 0.2s; + padding: 20px; + } + + .name { + width: 230px; + min-height: 36px; + border-radius: 2px; + background-color: lightgrey; + padding: 8px 12px 2px; + box-sizing: border-box; + } + + .card .name { + border-radius: 0 0 5px 5px; + } + + #submit { + margin-top: 10px; + padding: 8px 20px; + background-color: cadetblue; + border: none; + border-radius: 3px; + font-size: 1.1em; + color: white; + cursor: pointer; + } + + #submit:hover { + background-color: rgb(0, 146, 156); + } + + #newGame { + padding: 8px 20px; + background-color: lightpink; + border: none; + position: absolute; + top: 5px; + left: 20px; + border-radius: 3px; + font-size: 0.7em; + cursor: pointer; + } + + #newGame:hover { + background-color: coral; + } + + .selected { + background-color: orange; + } + + @media screen and (orientation: landscape) and (max-height: 680px) { + /* CSS applied when the device is in landscape mode*/ + .names-bank { + padding: 0; + top: 0; + max-height: 100vh; + overflow: scroll; + } + + body { + font-size: 20px; + } + + .word-count { + font-size: 14px; + } + + h1 { + margin-right: 240px; + } + } + + @media screen and (orientation: portrait) and (max-width: 1100px) { + body { + font-size: 1.8em; + } + + .play-page { + flex-direction: column; + } + + .names-bank { + flex-direction: row; + display: flex; + flex-wrap: wrap; + /* position: fixed; */ + padding: 10px 10px 40px; + position: sticky; + top: 0; + z-index: 100; + background: white; + } + + .answer-page .names-bank { + min-width: 100%; + justify-content: center; + } + + form { + margin: 0; + } + + .names-bank .name { + margin: 6px; + } + + .names-bank .score { + width: 0; + } + + .answer-page .names-bank .score { + width: auto; + } + + .word-count { + position: absolute; + margin-top: -20px; + } + + .name { + width: 300px; + } + + .card { + width: 300px; + background-size: 300px; + height: 266px; + } + + .answer-page .card { + height: 454px; + } + } + </style> + </head> + <body> + <!-- Google Tag Manager (noscript) --> + <noscript> + <iframe + src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG" + height="0" + width="0" + style="display: none; visibility: hidden" + ></iframe> + </noscript> + <!-- End Google Tag Manager (noscript) --> + + <h1><span id="guess-type"></span>: <span id="round-number"></span></h1> + + <div class="play-page"> + <div + class="names-bank" + ondrop="returnDrop(event)" + ondragover="event.preventDefault()" + > + <div class="score"> + YOUR SCORE + <div>Correct Answers This Round: <span id="score-amount"></span></div> + <div> + Correct Answers In Total: <span id="score-amount-total"></span> + </div> + <div>Overall Percent: <span id="score-percent"></span>%</div> + </div> + <div class="word-count"><span id="words-left"></span></div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-1" + > + Name 1 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-2" + > + Name 2 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-3" + > + Name 3 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-4" + > + Name 4 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-5" + > + Name 5 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-6" + > + Name 6 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-7" + > + Name 7 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-8" + > + Name 8 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-9" + > + Name 9 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-10" + > + Name 10 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-11" + > + Name 11 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-12" + > + Name 12 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-13" + > + Name 13 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-14" + > + Name 14 + </div> + <div + class="name" + draggable="true" + ondragstart="drag(event)" + onClick="selectName(event)" + id="name-15" + > + Name 15 + </div> + </div> + <form onsubmit="toggleMode(event)"> + <div class="cards-container"> + <div + class="card" + ondrop="drop(event,1)" + ondragover="allowDrop(event,1)" + onclick="dropSelected(event, 1)" + id="card-1" + ></div> + <div + class="card" + ondrop="drop(event,2)" + ondragover="allowDrop(event,2)" + onclick="dropSelected(event, 2)" + id="card-2" + ></div> + <div + class="card" + ondrop="drop(event,3)" + ondragover="allowDrop(event,3)" + onclick="dropSelected(event, 3)" + id="card-3" + ></div> + <div + class="card" + ondrop="drop(event,4)" + ondragover="allowDrop(event,4)" + onclick="dropSelected(event, 4)" + id="card-4" + ></div> + <div + class="card" + ondrop="drop(event,5)" + ondragover="allowDrop(event,5)" + onclick="dropSelected(event, 5)" + id="card-5" + ></div> + <div + class="card" + ondrop="drop(event, 6)" + ondragover="allowDrop(event,6)" + onclick="dropSelected(event,6)" + id="card-6" + ></div> + <div + class="card" + ondrop="drop(event,7)" + ondragover="allowDrop(event,7)" + onclick="dropSelected(event, 7)" + id="card-7" + ></div> + <div + class="card" + ondrop="drop(event,8)" + ondragover="allowDrop(event,8)" + onclick="dropSelected(event, 8)" + id="card-8" + ></div> + <div + class="card" + ondrop="drop(event,9)" + ondragover="allowDrop(event,9)" + onclick="dropSelected(event, 9)" + id="card-9" + ></div> + <div + class="card" + ondrop="drop(event,10)" + ondragover="allowDrop(event,10)" + onclick="dropSelected(event, 10)" + id="card-10" + ></div> + <div + class="card" + ondrop="drop(event,11)" + ondragover="allowDrop(event,11)" + onclick="dropSelected(event, 11)" + id="card-11" + ></div> + <div + class="card" + ondrop="drop(event,12)" + ondragover="allowDrop(event,12)" + onclick="dropSelected(event, 12)" + id="card-12" + ></div> + </div> + <input type="submit" id="submit" value="Submit" /> + </form> + </div> + + <div style="position: absolute; top: 0; left: 0; right: 0; color: grey"> + <form method="get" action="index.html"> + <input type="submit" id="newGame" value="New Game" /> + </form> + </div> + <div style="margin: -40px 0 0; height: 60px"> + <a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a> + </div> + + <div + style=" + font-size: 0.9em; + position: absolute; + bottom: 0; + left: 0; + right: 0; + color: grey; + font-style: italic; + " + > + made by + <a + style="color: rgb(0, 146, 156); font-style: italic" + href="https://idamayer.com" + >Ida Mayer</a + > + & Alex Lien 2022 + </div> + </body> +</html> diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 722e4714..8fbd9991 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -19,7 +19,6 @@ </script> <!-- End Google Tag Manager --> <meta charset="UTF-8" /> - <script type="text/javascript" src="app.js"></script> <style type="text/css"> body { position: relative; @@ -29,123 +28,15 @@ display: flex; flex-direction: row-reverse; font-family: Georgia, 'Times New Roman', Times, serif; + min-height: 200px; } - h1 { + h1, + h3 { font-family: Verdana, Geneva, Tahoma, sans-serif; text-align: center; } - form { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin-right: 240px; - } - - .cards-container { - display: flex; - flex-wrap: wrap; - flex-direction: row; - justify-content: center; - } - - .card { - width: 230px; - height: 208px; - border: 5px solid lightgrey; - margin: 5px; - align-items: flex-end; - box-sizing: border-box; - border-radius: 11px; - position: relative; - display: flex; - justify-content: center; - /*background-size: contain;*/ - background-size: 220px; - background-repeat: no-repeat; - transition: height 1s, background-image 1s, border 0.4s 0.6s; - background-position-y: calc(50% - 18px); - } - - .card:not([data-name^='name'])::after { - content: ''; - height: 34px; - background: white; - width: 100%; - } - - .answer-page .card { - height: 350px; - /*padding-top: 310px;*/ - /*background-size: cover;*/ - overflow: hidden; - border-color: rgb(0, 146, 156); - } - - .answer-page .card.incorrect { - border-color: rgb(216, 27, 96); - } - - .names-bank { - position: fixed; - padding: 10px 10px 40px; - } - - .names-bank .name { - margin: 6px 0; - } - - .answer-page .names-bank .name { - display: none; - } - - .answer-page .names-bank .word-count { - display: none; - } - - .word-count { - text-align: center; - font-style: italic; - color: #444; - } - - .score { - width: 100%; - text-align: center; - background-color: rgb(255, 193, 7); - width: 200px; - font-family: Verdana, Geneva, Tahoma, sans-serif; - opacity: 0; - } - - .names-bank .score { - overflow: hidden; - height: 0; - } - - .answer-page .names-bank .score { - height: auto; - display: block; - opacity: 1; - transition: opacity 1.2s 0.2s; - padding: 20px; - } - - .name { - width: 230px; - min-height: 36px; - border-radius: 2px; - background-color: lightgrey; - padding: 8px 12px 2px; - box-sizing: border-box; - } - - .card .name { - border-radius: 0 0 5px 5px; - } - #submit { margin-top: 10px; padding: 8px 20px; @@ -161,108 +52,58 @@ background-color: rgb(0, 146, 156); } - #newGame { - padding: 8px 20px; - background-color: lightpink; - border: none; - position: absolute; - top: 5px; - left: 20px; - border-radius: 3px; - font-size: 0.7em; + [type='radio'] { + display: none; + } + + [type='radio'] + label.radio-label { + background: lightgrey; + display: block; + padding: 10px; + border-radius: 4px; cursor: pointer; } - #newGame:hover { - background-color: coral; + label.radio-label:hover { + background: darkgrey; } - .selected { - background-color: orange; + [type='radio']:checked + label.radio-label { + background: lightcoral; } - @media screen and (orientation: landscape) and (max-height: 680px) { - /* CSS applied when the device is in landscape mode*/ - .names-bank { - padding: 0; - top: 0; - max-height: 100vh; - overflow: scroll; - } - - body { - font-size: 20px; - } - - .word-count { - font-size: 14px; - } - - h1 { - margin-right: 240px; - } + .radio-label h3 { + margin: 0; + display: inline-block; + vertical-align: middle; + width: 220px; } - @media screen and (orientation: portrait) and (max-width: 1100px) { - body { - font-size: 1.8em; - } + .thumbnail { + display: inline-block; + vertical-align: middle; + width: 67px; + height: 48px; + margin-right: 4px; + } - .play-page { - flex-direction: column; - } + body { + padding: 70px 0 30px; + } - .names-bank { - flex-direction: row; - display: flex; - flex-wrap: wrap; - /* position: fixed; */ - padding: 10px 10px 40px; - position: sticky; - top: 0; - z-index: 100; - background: white; - } + #addl-options { + position: absolute; + top: 30px; + right: 30px; + background-color: white; + padding: 10px; + cursor: pointer; + width: 200px; + } - .answer-page .names-bank { - min-width: 100%; - justify-content: center; - } - - form { - margin: 0; - } - - .names-bank .name { - margin: 6px; - } - - .names-bank .score { - width: 0; - } - - .answer-page .names-bank .score { - width: auto; - } - - .word-count { - position: absolute; - margin-top: -20px; - } - - .name { - width: 300px; - } - - .card { - width: 300px; - background-size: 300px; - height: 266px; - } - - .answer-page .card { - height: 454px; - } + #addl-options > summary { + list-style: none; + text-align: right; } </style> </head> @@ -277,256 +118,86 @@ ></iframe> </noscript> <!-- End Google Tag Manager (noscript) --> - - <h1><span id="guess-type"></span>: <span id="round-number"></span></h1> - - <div class="play-page"> - <div - class="names-bank" - ondrop="returnDrop(event)" - ondragover="event.preventDefault()" + <h1>Magic the Guessering</h1> + <div class="play-page" style="justify-content: center"> + <form + method="get" + action="guess.html" + style="display: flex; flex-direction: column; align-items: center" > - <div class="score"> - YOUR SCORE - <div>Correct Answers This Round: <span id="score-amount"></span></div> - <div> - Correct Answers In Total: <span id="score-amount-total"></span> - </div> - <div>Overall Percent: <span id="score-percent"></span>%</div> - </div> - <div class="word-count"><span id="words-left"></span></div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-1" + <!-- <input type="radio" id="wrath" name="whichguesser" value="wrath" /> + <label class="radio-label" for="wrath"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0619d670-7b53-4185-a25d-2fab5db1aab5.jpg?1562896185" + /> + <h3>I'll Clean Sweep</h3></label + ><br /> --> + + <input + type="radio" + id="counterspell" + name="whichguesser" + value="counterspell" + checked + /> + <label class="radio-label" for="counterspell"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855" + /> + <h3>Counterspell Guesser</h3></label + ><br /> + + <!-- <input type="radio" id="terror" name="whichguesser" value="terror" /> + <label class="radio-label" for="terror"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dd5d601-aff7-4b7a-ab6c-b89f403af076.jpg?1562905752" + /> + <h3>I'm a Terror-able Guesser</h3></label + ><br /> --> + + <input type="radio" id="burn" name="whichguesser" value="burn" /> + <label class="radio-label" for="burn"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596" + /> + <h3>Match With Hot Singles</h3></label + ><br /> + + <!-- <input type="radio" id="beast" name="whichguesser" value="beast" /> + <label class="radio-label" for="beast"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469" + /> + <h3>Finding Fantastic Beasts</h3></label > - Name 1 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-2" - > - Name 2 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-3" - > - Name 3 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-4" - > - Name 4 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-5" - > - Name 5 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-6" - > - Name 6 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-7" - > - Name 7 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-8" - > - Name 8 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-9" - > - Name 9 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-10" - > - Name 10 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-11" - > - Name 11 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-12" - > - Name 12 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-13" - > - Name 13 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-14" - > - Name 14 - </div> - <div - class="name" - draggable="true" - ondragstart="drag(event)" - onClick="selectName(event)" - id="name-15" - > - Name 15 - </div> - </div> - <form onsubmit="toggleMode(event)"> - <div class="cards-container"> - <div - class="card" - ondrop="drop(event,1)" - ondragover="allowDrop(event,1)" - onclick="dropSelected(event, 1)" - id="card-1" - ></div> - <div - class="card" - ondrop="drop(event,2)" - ondragover="allowDrop(event,2)" - onclick="dropSelected(event, 2)" - id="card-2" - ></div> - <div - class="card" - ondrop="drop(event,3)" - ondragover="allowDrop(event,3)" - onclick="dropSelected(event, 3)" - id="card-3" - ></div> - <div - class="card" - ondrop="drop(event,4)" - ondragover="allowDrop(event,4)" - onclick="dropSelected(event, 4)" - id="card-4" - ></div> - <div - class="card" - ondrop="drop(event,5)" - ondragover="allowDrop(event,5)" - onclick="dropSelected(event, 5)" - id="card-5" - ></div> - <div - class="card" - ondrop="drop(event, 6)" - ondragover="allowDrop(event,6)" - onclick="dropSelected(event,6)" - id="card-6" - ></div> - <div - class="card" - ondrop="drop(event,7)" - ondragover="allowDrop(event,7)" - onclick="dropSelected(event, 7)" - id="card-7" - ></div> - <div - class="card" - ondrop="drop(event,8)" - ondragover="allowDrop(event,8)" - onclick="dropSelected(event, 8)" - id="card-8" - ></div> - <div - class="card" - ondrop="drop(event,9)" - ondragover="allowDrop(event,9)" - onclick="dropSelected(event, 9)" - id="card-9" - ></div> - <div - class="card" - ondrop="drop(event,10)" - ondragover="allowDrop(event,10)" - onclick="dropSelected(event, 10)" - id="card-10" - ></div> - <div - class="card" - ondrop="drop(event,11)" - ondragover="allowDrop(event,11)" - onclick="dropSelected(event, 11)" - id="card-11" - ></div> - <div - class="card" - ondrop="drop(event,12)" - ondragover="allowDrop(event,12)" - onclick="dropSelected(event, 12)" - id="card-12" - ></div> - </div> - <input type="submit" id="submit" value="Submit" /> + <br /> --> + + <details id="addl-options"> + <summary> + <img + src="http://mythicspoiler.com/images/buttons/ustset.png" + style="width: 32px; vertical-align: top" + /> + Options + </summary> + <input type="checkbox" name="digital" id="digital" checked /> + <label for="digital">include digital cards</label> + <br /> + <input type="checkbox" name="un" id="un" checked /> + <label for="un">include un-cards</label> + <br /> + <input type="checkbox" name="original" id="original" /> + <label for="original">restrict to only original printing</label> + </details> + <input type="submit" id="submit" value="Play" /> </form> </div> - <div style="position: absolute; top: 0; left: 0; right: 0; color: grey"> - <form method="get" action="choose.html"> - <input type="submit" id="newGame" value="New Game" /> - </form> - </div> <div style="margin: -40px 0 0; height: 60px"> <a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a> </div> From edee910e2d884d06c1dd4467041bbc502aee5f98 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 18:00:18 -0700 Subject: [PATCH 288/519] Remove other guessing games --- web/public/mtg/index.html | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 8fbd9991..4626312e 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -125,15 +125,6 @@ action="guess.html" style="display: flex; flex-direction: column; align-items: center" > - <!-- <input type="radio" id="wrath" name="whichguesser" value="wrath" /> - <label class="radio-label" for="wrath"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0619d670-7b53-4185-a25d-2fab5db1aab5.jpg?1562896185" - /> - <h3>I'll Clean Sweep</h3></label - ><br /> --> - <input type="radio" id="counterspell" @@ -149,15 +140,6 @@ <h3>Counterspell Guesser</h3></label ><br /> - <!-- <input type="radio" id="terror" name="whichguesser" value="terror" /> - <label class="radio-label" for="terror"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dd5d601-aff7-4b7a-ab6c-b89f403af076.jpg?1562905752" - /> - <h3>I'm a Terror-able Guesser</h3></label - ><br /> --> - <input type="radio" id="burn" name="whichguesser" value="burn" /> <label class="radio-label" for="burn"> <img @@ -167,16 +149,6 @@ <h3>Match With Hot Singles</h3></label ><br /> - <!-- <input type="radio" id="beast" name="whichguesser" value="beast" /> - <label class="radio-label" for="beast"> - <img - class="thumbnail" - src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469" - /> - <h3>Finding Fantastic Beasts</h3></label - > - <br /> --> - <details id="addl-options"> <summary> <img From 260f4641dd89b1c46b61c529e9cc974a7e28cb95 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 18:04:54 -0700 Subject: [PATCH 289/519] Remove alternate versions; add Alex's email --- web/public/mtg/app.js | 8 -------- web/public/mtg/guess.html | 7 ++++++- web/public/mtg/index.html | 7 ++++++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/web/public/mtg/app.js b/web/public/mtg/app.js index 983b8651..fc7711d0 100644 --- a/web/public/mtg/app.js +++ b/web/public/mtg/app.js @@ -57,14 +57,6 @@ function putIntoMapAndFetch(data) { window.console.log(nameList) if (whichGuesser === 'counterspell') { document.getElementById('guess-type').innerText = 'Counterspell Guesser' - } else if (whichGuesser === 'beast') { - document.getElementById('guess-type').innerText = - 'Finding Fantastic Beasts' - } else if (whichGuesser === 'terror') { - document.getElementById('guess-type').innerText = - "I'm a Terror-able Guesser" - } else if (whichGuesser === 'wrath') { - document.getElementById('guess-type').innerText = "I'll Clean Sweep" } else if (whichGuesser === 'burn') { document.getElementById('guess-type').innerText = 'Match With Hot Singles' } diff --git a/web/public/mtg/guess.html b/web/public/mtg/guess.html index f0045f08..882883a7 100644 --- a/web/public/mtg/guess.html +++ b/web/public/mtg/guess.html @@ -548,7 +548,12 @@ href="https://idamayer.com" >Ida Mayer</a > - & Alex Lien 2022 + & + <a + style="color: rgb(0, 146, 156); font-style: italic" + href="mailto:alexlien.alien@gmail.com" + >Alex Lien</a + >, 2022 </div> </body> </html> diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 4626312e..5fd31966 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -191,7 +191,12 @@ href="https://idamayer.com" >Ida Mayer</a > - & Alex Lien 2022 + & + <a + style="color: rgb(0, 146, 156); font-style: italic" + href="mailto:alexlien.alien@gmail.com" + >Alex Lien</a + >, 2022 </div> </body> </html> From aba818a9de766194ca6aafc6a82eafac42e6589a Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 20 Jul 2022 18:05:41 -0700 Subject: [PATCH 290/519] Remove unused jsons --- web/public/mtg/jsons/beast1.json | 1 - web/public/mtg/jsons/beast2.json | 1 - web/public/mtg/jsons/beast3.json | 1 - web/public/mtg/jsons/terror1.json | 1 - web/public/mtg/jsons/terror2.json | 1 - web/public/mtg/jsons/terror3.json | 1 - web/public/mtg/jsons/wrath1.json | 1 - web/public/mtg/jsons/wrath2.json | 1 - web/public/mtg/jsons/wrath3.json | 1 - 9 files changed, 9 deletions(-) delete mode 100644 web/public/mtg/jsons/beast1.json delete mode 100644 web/public/mtg/jsons/beast2.json delete mode 100644 web/public/mtg/jsons/beast3.json delete mode 100644 web/public/mtg/jsons/terror1.json delete mode 100644 web/public/mtg/jsons/terror2.json delete mode 100644 web/public/mtg/jsons/terror3.json delete mode 100644 web/public/mtg/jsons/wrath1.json delete mode 100644 web/public/mtg/jsons/wrath2.json delete mode 100644 web/public/mtg/jsons/wrath3.json diff --git a/web/public/mtg/jsons/beast1.json b/web/public/mtg/jsons/beast1.json deleted file mode 100644 index 6a5b26c0..00000000 --- a/web/public/mtg/jsons/beast1.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Adaptive Snapjaw", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aeromoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Affectionate Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alms Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alpha Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Barkripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Brushhopper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Murkdiver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Scavenger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Swarmsnapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquamoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquus Steed", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arborback Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arboreal Grazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Hybrid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Lancer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Arc-Slogger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Armguard Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ashen Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assault Zeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aura Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Avarax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Axebane Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Null", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Packhunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060"}, "reprint": false, "digital": true, "set_type": "starter"}, {"name": "Baloth Pup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Woodcrasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bannerhide Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Barbarian Outcast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barkhide Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Batterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battlefront Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bazaar Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beacon Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beasts of Bogardan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast Walkers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Berserk Murlodont", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blight-Breath Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blind Creeper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodstoke Howler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blossoming Bogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Bog Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bogstomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Bonethorn Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Book Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boot Nipper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brackish Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Branchsnap Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brontotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broodstar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Bull Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bumbling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Caller of the Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Canopy Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnassid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnivorous Moss-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cavern Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cavern Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cerodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chainflinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chambered Nautilus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chancellor of the Tangle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Charging Binox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Charging Slateback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cliffrunner Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Clockwork Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Coalhauler Swine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colossodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colos Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Copperhoof Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cosmic Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cragplate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crater Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Creature Guy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Crested Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryptic Annelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cultivator Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cystbearer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Darba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathbringer Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathmist Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deepwood Tantiv", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desert Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Displacer Kitten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Domri's Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dreg Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drekavac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dromad Purebred", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drooling Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Durkwood Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthshaking Si", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Elder Gargaroth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electryte", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Enormous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Erithizon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Symbiote", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ettercap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Excavating Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Fangren Firstborn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Hunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Marauder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Pathcutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Guardian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Sovereign", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Feral Hydra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Throwback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ferocious Zheng", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Ferrovore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fledgling Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Overseer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenetic Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenzied Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frondland Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fungal Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Furnace Scamp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fylamarid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Galvanoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Garruk's Companion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Gorehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Harbinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Garruk's Packleader", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast2.json b/web/public/mtg/jsons/beast2.json deleted file mode 100644 index de0f2279..00000000 --- a/web/public/mtg/jsons/beast2.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022"}, "flavor_name": "Anguirus, Armored Killer", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geyser Glider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Giant Warthog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gilded Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glade Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glademuse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Gloomshrieker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowering Rogon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowing Anemone", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gluetius Maximus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Gnarlid Colony", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gnarlid Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Godsire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goretusk Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Graf Mole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grave Sifter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Graxiplon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grazing Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Greater Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Great-Horn Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gristleback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Groffskithur", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grollub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Ragebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guardian Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gulf Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gurzigost", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Helium Squirter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Herald of the Forgotten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Herd Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Herd Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Highcliff Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130"}, "reprint": false, "digital": false, "set_type": "box"}, {"name": "Hollowhenge Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hundroog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunting Moa", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hydroid Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Indrik Stomphowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infernal Spawn of Evil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Infernius Spawnington III, Esq.", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Insatiable Souleater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Intrusive Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Iron-Barb Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ironclad Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jackalope Herd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kalonian Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Kalonian Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Karplusan Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Karstoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kazandu Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kelpie Guide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kezzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kiln Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kjeldoran Frostbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krakilin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kranioceros", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Krosan Cloudscraper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Groundshaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Vorine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Warchief", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kurgadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Grunt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Titan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Warrior", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Whelp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Landscaper Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lazotep Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Leery Fogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leopard-Spotted Jiao", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Lesser Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lexivore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Leyline Prowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Loathsome Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Longhorn Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lullmage's Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Battlement", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Satyr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurching Rotbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Macetail Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Makindi Sliderunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Manglehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Marauding Maulhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Marsh Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Megatherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Meglonoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Mischievous Quanar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mockery of Nature", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Slug", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mold Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mosscoat Goriak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Muck Drubb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murasa Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Murasa Rootgrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nalfeshnee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Naya Soulbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Needleshot Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Demolok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Game Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Noxious Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nucklavee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nullhide Ferox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nulltread Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nylea's Forerunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Obstinate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ondu Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oraxid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oxidda Scrapmelter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Paleoloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pallimud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Petradon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Petravark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phantom Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Phantom Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian Ingester", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Belcher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaguemaw Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Plated Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plated Seastrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaxmanta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Primal Huntbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Primoc Escapee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrid Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quagnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quartzwood Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quicksilver Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quillspike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Qumulox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Raging Kronch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakeclaw Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Rampaging Rendhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast3.json b/web/public/mtg/jsons/beast3.json deleted file mode 100644 index 3bf8f454..00000000 --- a/web/public/mtg/jsons/beast3.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": false, "data": [{"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Ravenous Gigantotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Ravenous Leucrocota", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Razing Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Realm Razer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Relic Sloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Renegade Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Ridgeline Rager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridge Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgescale Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgetop Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Riptide Mangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "River Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Roaring Primadox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rock Badger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ronom Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Root Greevil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rotted Hystrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rumbling Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Sabertooth Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sagu Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctuary Smasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctum Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sandstorm Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Strangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saprazzan Breaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sawtusk Demolisher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Scalpelexis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scragnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Screeching Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sea Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Bruiser", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Plower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shivan Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Shore Snapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Siege Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Sigiled Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silt Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Six-y Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Skittish Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Cutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Ridgeback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slipstream Eel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Thragg", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snarling Undorak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snorting Gahr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spearbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Species Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiked Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spinal Villain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spinebiter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spined Basher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spirespine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Spitting Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sproutback Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spur Grappler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spurred Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Serow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Wildebeests", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stomper Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stonework Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratozeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swarm Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sylvan Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Symbiotic Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tangle Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tenement Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tephraderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terra Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Terra Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbound Primoc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Eater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Nibbler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Thrashing Mudspawn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thrashing Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thresher Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Thundering Tanadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Timbermaw Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanic Bulvox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818"}, "flavor_name": "Godzilla, Primeval Champion", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trapjaw Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Treespring Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trove Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Trusty Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tusked Colossodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undying Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Ursapine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vagrant Plowbeasts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valley Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Venomspout Brackus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vigilant Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Voracious Typhon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vulshok War Boar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "War Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wayward Guide-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Weaver of Lies", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wild Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woodland Bellower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Woodripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Razorback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Wormfang Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Manta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Newt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Turtle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrecking Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wretched Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yoked Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zhur-Taa Ancient", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror1.json b/web/public/mtg/jsons/terror1.json deleted file mode 100644 index 4bbb9a03..00000000 --- a/web/public/mtg/jsons/terror1.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Abrupt Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b1e92b4-6e53-4dba-a572-c67e01965ac5.jpg?1562785076", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b1e92b4-6e53-4dba-a572-c67e01965ac5.jpg?1562785076"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Abrupt Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b10ef54-368c-4841-ab5d-f2e8e1265c83.jpg?1561756631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b10ef54-368c-4841-ab5d-f2e8e1265c83.jpg?1561756631"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Active Volcano", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad402e65-6fac-4005-a2d4-592983df0c30.jpg?1584237356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad402e65-6fac-4005-a2d4-592983df0c30.jpg?1584237356"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aerial Assault", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64d9c182-cbb3-4791-90dd-0e533ddeebda.jpg?1592515927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64d9c182-cbb3-4791-90dd-0e533ddeebda.jpg?1592515927"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Aerial Predation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec3c023c-037e-495a-b7df-32be42a75f36.jpg?1562795050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec3c023c-037e-495a-b7df-32be42a75f36.jpg?1562795050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Afterlife", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fa2ecf9-b53c-4f1d-9028-ca3820d043cb.jpg?1562381856", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fa2ecf9-b53c-4f1d-9028-ca3820d043cb.jpg?1562381856"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Afterlife", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/4644694d-52e6-4d00-8cad-748899eeea84.jpg?1562718804", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/4644694d-52e6-4d00-8cad-748899eeea84.jpg?1562718804"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aftershock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91a26b2-03f8-43f0-a3a4-ff6c5a3690c4.jpg?1587857346", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91a26b2-03f8-43f0-a3a4-ff6c5a3690c4.jpg?1587857346"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Agonizing Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/539ac5e1-4bad-4f70-abac-e70c406bebec.jpg?1562912008", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/539ac5e1-4bad-4f70-abac-e70c406bebec.jpg?1562912008"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Angrath's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708006ba-d494-4093-b108-8249b110831e.jpg?1555041214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Annihilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a3bf039-ecf6-477e-997c-e32c55323c01.jpg?1562909994", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a3bf039-ecf6-477e-997c-e32c55323c01.jpg?1562909994"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Asphyxiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/894f3f5f-586d-45e4-9af7-4de80e44dfae.jpg?1593091866", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/894f3f5f-586d-45e4-9af7-4de80e44dfae.jpg?1593091866"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassinate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40b67839-622d-41c1-b9c7-1a26b021ec78.jpg?1562908402", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40b67839-622d-41c1-b9c7-1a26b021ec78.jpg?1562908402"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassin's Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b80e8fe0-eccb-4268-a6ce-1365c68e6b13.jpg?1562447376", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b80e8fe0-eccb-4268-a6ce-1365c68e6b13.jpg?1562447376"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Assassin's Ink", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a926c10-029d-4e24-8c3f-1808025e30aa.jpg?1654567050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a926c10-029d-4e24-8c3f-1808025e30aa.jpg?1654567050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assassin's Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f796e320-9898-45d4-9d7a-6d35de53c9ab.jpg?1562795619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f796e320-9898-45d4-9d7a-6d35de53c9ab.jpg?1562795619"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Avenging Arrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/696678ff-44dc-4fe4-bf17-024e86cd0220.jpg?1562787572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/696678ff-44dc-4fe4-bf17-024e86cd0220.jpg?1562787572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bake into a Pie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42a4d090-1bb7-4334-ab22-e2527391e79b.jpg?1572490064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42a4d090-1bb7-4334-ab22-e2527391e79b.jpg?1572490064"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast Within", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/76f8a300-44a8-4a70-93d1-64333c13f6f2.jpg?1592752271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/76f8a300-44a8-4a70-93d1-64333c13f6f2.jpg?1592752271"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Beast Within", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce5b6d19-22e3-4f57-8f4d-a17e982286c7.jpg?1562881648", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce5b6d19-22e3-4f57-8f4d-a17e982286c7.jpg?1562881648"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bedevil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81e2b96b-ecf2-4dd9-bc9d-3c46ee8c59e6.jpg?1584831400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81e2b96b-ecf2-4dd9-bc9d-3c46ee8c59e6.jpg?1584831400"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dfff5d3-1433-4a24-83e6-6361a446b974.jpg?1562758881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dfff5d3-1433-4a24-83e6-6361a446b974.jpg?1562758881"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c5db137-33b9-4cea-9193-4e637d2966f1.jpg?1562241441", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c5db137-33b9-4cea-9193-4e637d2966f1.jpg?1562241441"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Befoul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f92cb48d-315b-4877-b615-ffdf275c4d61.jpg?1562947702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f92cb48d-315b-4877-b615-ffdf275c4d61.jpg?1562947702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Betrayal of Flesh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9e2e107-0277-4e5c-81a7-258bb2998f3e.jpg?1562153677", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9e2e107-0277-4e5c-81a7-258bb2998f3e.jpg?1562153677"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodchief's Thirst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/059e8447-6b1c-4651-a734-a8fea2cbf7b2.jpg?1604195360", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/059e8447-6b1c-4651-a734-a8fea2cbf7b2.jpg?1604195360"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blood Curdle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/4184c851-1419-476c-ba9c-9f0cb1137114.jpg?1591226609", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/4184c851-1419-476c-ba9c-9f0cb1137114.jpg?1591226609"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bone Shards", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1ee98955-4c47-4d45-9377-608dfa755337.jpg?1626095299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1ee98955-4c47-4d45-9377-608dfa755337.jpg?1626095299"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74780faa-1c64-4d73-8d09-53b47ba02d7a.jpg?1562922512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74780faa-1c64-4d73-8d09-53b47ba02d7a.jpg?1562922512"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/387eda28-f35b-48b0-ba59-773d82902327.jpg?1592708776", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/387eda28-f35b-48b0-ba59-773d82902327.jpg?1592708776"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Bone Splinters", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4a4b3a3-b7ae-4210-8037-098fdf5808d0.jpg?1562709424", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4a4b3a3-b7ae-4210-8037-098fdf5808d0.jpg?1562709424"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brainspoil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c34fa44f-274e-4914-bbd5-71193f8d2f96.jpg?1598914670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c34fa44f-274e-4914-bbd5-71193f8d2f96.jpg?1598914670"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bright Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3340ffb9-9513-4551-ad64-821600596b2e.jpg?1562553092", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3340ffb9-9513-4551-ad64-821600596b2e.jpg?1562553092"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bring Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e18146f9-369c-41c8-8a1d-7737edd2c18e.jpg?1562940282", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e18146f9-369c-41c8-8a1d-7737edd2c18e.jpg?1562940282"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Broken Visage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/824823fb-5ae1-48b1-bc46-e452afa73cd8.jpg?1562592294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/824823fb-5ae1-48b1-bc46-e452afa73cd8.jpg?1562592294"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Broken Visage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9be199e7-feaa-4f23-b93c-3eab54a02e74.jpg?1562587775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9be199e7-feaa-4f23-b93c-3eab54a02e74.jpg?1562587775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9eb94908-4f4a-487e-87ac-8d5bdefe9983.jpg?1650029788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9eb94908-4f4a-487e-87ac-8d5bdefe9983.jpg?1650029788"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/6201f78e-ff45-4c59-ac85-c8447c14a496.jpg?1631050058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/6201f78e-ff45-4c59-ac85-c8447c14a496.jpg?1631050058"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Broken Wings", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0fc2dfd-85b0-4add-be18-b39549235921.jpg?1604198611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0fc2dfd-85b0-4add-be18-b39549235921.jpg?1604198611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cast Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/aba79021-39af-4e74-beb5-f2f508c865b2.jpg?1653520579", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/aba79021-39af-4e74-beb5-f2f508c865b2.jpg?1653520579"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Cast Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a41150b2-44a6-4e80-8b32-afc6ea744fb3.jpg?1591104816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a41150b2-44a6-4e80-8b32-afc6ea744fb3.jpg?1591104816"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Casualties of War", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08fc5e50-c6f7-41ec-815a-5667eefded78.jpg?1557577078", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08fc5e50-c6f7-41ec-815a-5667eefded78.jpg?1557577078"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Certain Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c67784b3-eb55-452e-b965-f63220b88896.jpg?1576384279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c67784b3-eb55-452e-b965-f63220b88896.jpg?1576384279"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chastise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/1169dab7-8f4c-474d-9289-42765a275376.jpg?1562628717", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/1169dab7-8f4c-474d-9289-42765a275376.jpg?1562628717"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chill to the Bone", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/312505d7-362e-43cf-bd23-28c248a8b7e1.jpg?1593275049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/312505d7-362e-43cf-bd23-28c248a8b7e1.jpg?1593275049"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cinder Cloud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f044c470-50ce-4a6c-b8ab-665357c3c11e.jpg?1562722408"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clear a Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a8f904b-a9a3-4bae-9284-4e9cbe7592ee.jpg?1562920680", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a8f904b-a9a3-4bae-9284-4e9cbe7592ee.jpg?1562920680"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Closing Statement", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/785e6d07-fe40-4723-b963-02da0a0987c7.jpg?1627428302", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/785e6d07-fe40-4723-b963-02da0a0987c7.jpg?1627428302"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Collar the Culprit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdf305b7-d1f7-4770-9201-8f3fb6735cd9.jpg?1572892497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdf305b7-d1f7-4770-9201-8f3fb6735cd9.jpg?1572892497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Collective Effort", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d85a6369-c07f-47d5-8448-72d8ec7e7898.jpg?1576383801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d85a6369-c07f-47d5-8448-72d8ec7e7898.jpg?1576383801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Consign to the Pit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/09991fad-4282-4a17-bfb1-03eaa13502df.jpg?1584830536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/09991fad-4282-4a17-bfb1-03eaa13502df.jpg?1584830536"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Contract Killing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1f20feb-b1ed-4d80-bef9-f3cc44ffb7b0.jpg?1562564388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1f20feb-b1ed-4d80-bef9-f3cc44ffb7b0.jpg?1562564388"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corpsehatch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91c152d-1829-438c-b571-74361e09df62.jpg?1562708566", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91c152d-1829-438c-b571-74361e09df62.jpg?1562708566"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cradle to Grave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3ec275cf-bb4e-4de0-9184-4d53dd87dad3.jpg?1562569856", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3ec275cf-bb4e-4de0-9184-4d53dd87dad3.jpg?1562569856"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crosis's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a586e329-b1e2-4b60-a914-7b9aa2c645c2.jpg?1562929889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a586e329-b1e2-4b60-a914-7b9aa2c645c2.jpg?1562929889"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Crosis's Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b59a9e75-9988-4040-a718-b1655fc20d11.jpg?1562933342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b59a9e75-9988-4040-a718-b1655fc20d11.jpg?1562933342"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cruel Cut", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f99ffe22-4dd8-4787-b6e0-e03dea8ab42a.jpg?1590010389", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f99ffe22-4dd8-4787-b6e0-e03dea8ab42a.jpg?1590010389"}, "reprint": true, "digital": true, "set_type": "memorabilia"}, {"name": "Cruel Revival", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a863ae27-a99a-4a60-ab07-25c1bacec64d.jpg?1562035297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a863ae27-a99a-4a60-ab07-25c1bacec64d.jpg?1562035297"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Cruel Revival", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/245aba23-2abb-4084-b4cb-d06e46de2108.jpg?1562903595", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/245aba23-2abb-4084-b4cb-d06e46de2108.jpg?1562903595"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eae67d98-5167-442b-8586-0b2bcb0c56eb.jpg?1643592488", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eae67d98-5167-442b-8586-0b2bcb0c56eb.jpg?1643592488"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0c4f213-0ea4-44c0-8429-172a317b77f5.jpg?1572893325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0c4f213-0ea4-44c0-8429-172a317b77f5.jpg?1572893325"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Crushing Canopy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a66b0e45-e585-44f3-8d2b-e887330ba138.jpg?1562561563", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a66b0e45-e585-44f3-8d2b-e887330ba138.jpg?1562561563"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crushing Vines", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c59b3653-5a50-48f2-bcf1-ab305ef30902.jpg?1562941671", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c59b3653-5a50-48f2-bcf1-ab305ef30902.jpg?1562941671"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Damn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Daring Demolition", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6378898-50b7-47c9-8c25-dc660606be9f.jpg?1576381626", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6378898-50b7-47c9-8c25-dc660606be9f.jpg?1576381626"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d03720d-b0ca-4892-9ad1-52189f4a30a1.jpg?1562244108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d03720d-b0ca-4892-9ad1-52189f4a30a1.jpg?1562244108"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/922d6c8b-70ae-4db4-bf26-1904e4906211.jpg?1562055426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/922d6c8b-70ae-4db4-bf26-1904e4906211.jpg?1562055426"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f983dcb-b077-465f-a70b-6bd0e425556c.jpg?1562719738", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f983dcb-b077-465f-a70b-6bd0e425556c.jpg?1562719738"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Dark Banishing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7dc2716-ed62-4797-ad2b-227eca5408d0.jpg?1562941556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7dc2716-ed62-4797-ad2b-227eca5408d0.jpg?1562941556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Betrayal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56adf4ea-1b1c-4737-8574-1848ca47d4f3.jpg?1562818301", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56adf4ea-1b1c-4737-8574-1848ca47d4f3.jpg?1562818301"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dark Offering", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3ce0cef9-6de4-4a71-b76a-eb0198387294.jpg?1562909319", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3ce0cef9-6de4-4a71-b76a-eb0198387294.jpg?1562909319"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Dark Withering", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3da58e0d-5877-43c4-b129-993e154b6087.jpg?1562907804", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3da58e0d-5877-43c4-b129-993e154b6087.jpg?1562907804"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deadly Alliance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/007a5c8c-ed0b-4844-9393-a3d25d4ffa1d.jpg?1604195436", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/007a5c8c-ed0b-4844-9393-a3d25d4ffa1d.jpg?1604195436"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deadly Visit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/462fe190-5264-42d8-bd27-23c5aa0c641f.jpg?1572892937", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/462fe190-5264-42d8-bd27-23c5aa0c641f.jpg?1572892937"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Bomb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8a84715-c5dc-4a19-af6a-796c6ee912c2.jpg?1562947604", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8a84715-c5dc-4a19-af6a-796c6ee912c2.jpg?1562947604"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathmark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61268362-f2ba-469d-8e5a-0b8da96e54a5.jpg?1561982272", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61268362-f2ba-469d-8e5a-0b8da96e54a5.jpg?1561982272"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Deathmark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e72e8728-d0a0-4ee5-87c3-092ca94225e0.jpg?1593275062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e72e8728-d0a0-4ee5-87c3-092ca94225e0.jpg?1593275062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Mutation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c643d87-50bc-4380-b1d6-0a465eef5dbf.jpg?1562912876", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c643d87-50bc-4380-b1d6-0a465eef5dbf.jpg?1562912876"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Rattle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8cddafc8-57d6-456e-af58-4b7f45e195d5.jpg?1562923481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8cddafc8-57d6-456e-af58-4b7f45e195d5.jpg?1562923481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death's Caress", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/0643fb9a-8284-4dfc-836a-c2c69ef09f32.jpg?1562896472", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/0643fb9a-8284-4dfc-836a-c2c69ef09f32.jpg?1562896472"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathsprout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d615557-aea8-4057-9fbd-d62dd98edc13.jpg?1557577090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d615557-aea8-4057-9fbd-d62dd98edc13.jpg?1557577090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/7478a471-3bd2-4038-a4eb-70c38a43afa9.jpg?1562596864", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/7478a471-3bd2-4038-a4eb-70c38a43afa9.jpg?1562596864"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decimate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/358bbaf9-8d48-448b-b87f-211344e36e29.jpg?1562864952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/358bbaf9-8d48-448b-b87f-211344e36e29.jpg?1562864952"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Decimate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/912c398a-e49a-4399-ac41-7b1d4328a59d.jpg?1562921956", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/912c398a-e49a-4399-ac41-7b1d4328a59d.jpg?1562921956"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deface", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43df9f41-944e-4cf3-ac80-524eadac221d.jpg?1584830848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43df9f41-944e-4cf3-ac80-524eadac221d.jpg?1584830848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/60473300-0bdc-4e89-87d9-28c8d7b4d83d.jpg?1562787158", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60473300-0bdc-4e89-87d9-28c8d7b4d83d.jpg?1562787158"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defend the Campus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85e4e1b5-77d6-4af4-b22e-6f6b4d129f5d.jpg?1624589309", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85e4e1b5-77d6-4af4-b22e-6f6b4d129f5d.jpg?1624589309"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Defenestrate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df3abdcc-83a8-45c3-9bfd-23f929705018.jpg?1634349688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df3abdcc-83a8-45c3-9bfd-23f929705018.jpg?1634349688"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devour in Shadow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98c80584-b7b5-4dcd-8a00-812b9dd9b1b9.jpg?1562878693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98c80584-b7b5-4dcd-8a00-812b9dd9b1b9.jpg?1562878693"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3f4cfa7-8ee4-4a85-9e6a-65a7541f62c1.jpg?1561852231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dimir Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f6bc1da-3969-4f19-b072-4ed79f906fef.jpg?1562497257"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Disembowel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1edb79d-0031-4dc6-8881-f6d1fe4acba2.jpg?1619741469", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1edb79d-0031-4dc6-8881-f6d1fe4acba2.jpg?1619741469"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed07b708-7232-4b87-b5d9-edaa20a69293.jpg?1555039673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed07b708-7232-4b87-b5d9-edaa20a69293.jpg?1555039673"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79f46ac0-9e2f-4f9f-beee-0a7914475ac1.jpg?1562820257", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79f46ac0-9e2f-4f9f-beee-0a7914475ac1.jpg?1562820257"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Divine Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48444e14-c73b-47d1-9c55-0ff4dc3c6034.jpg?1561978713", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48444e14-c73b-47d1-9c55-0ff4dc3c6034.jpg?1561978713"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/176cdb4b-6ad4-4991-8456-28579640063d.jpg?1562229273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/176cdb4b-6ad4-4991-8456-28579640063d.jpg?1562229273"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e19acff-f3dd-417a-a9ab-ea3e36c1ba61.jpg?1561983934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e19acff-f3dd-417a-a9ab-ea3e36c1ba61.jpg?1561983934"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7e6c0fe2-a82b-42cb-8629-b9f00b7f08e9.jpg?1623780045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7e6c0fe2-a82b-42cb-8629-b9f00b7f08e9.jpg?1623780045"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Doom Blade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37468ade-27b1-4128-9a62-1293ec2aab41.jpg?1561756922", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37468ade-27b1-4128-9a62-1293ec2aab41.jpg?1561756922"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Drag to the Underworld", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91852444-9361-4588-a44f-fb90ba1b30e5.jpg?1581479732", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91852444-9361-4588-a44f-fb90ba1b30e5.jpg?1581479732"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dreadbore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a83945c6-4dc6-4d9a-9bc2-2d4a264e5422.jpg?1562791208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a83945c6-4dc6-4d9a-9bc2-2d4a264e5422.jpg?1562791208"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bf5df5b-164d-4ec2-a5e6-bbaea152e271.jpg?1572490739"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in the Loch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01acd1c1-86b2-4423-9ba7-5b9725c0514f.jpg?1640249448"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Duh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fa5b9b30-4950-4c9c-9ce8-6d271bb7aa01.jpg?1562489857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fa5b9b30-4950-4c9c-9ce8-6d271bb7aa01.jpg?1562489857"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Easy Prey", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/312fb6e4-1eb1-4fbb-b7a4-125829a6e96a.jpg?1591226769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/312fb6e4-1eb1-4fbb-b7a4-125829a6e96a.jpg?1591226769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eaten by Spiders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0efea1b1-f212-4b97-98dd-922f85ab191f.jpg?1592709344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0efea1b1-f212-4b97-98dd-922f85ab191f.jpg?1592709344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eightfold Maze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc8c377a-82c4-46ee-94c2-b970160a3205.jpg?1562257975", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc8c377a-82c4-46ee-94c2-b970160a3205.jpg?1562257975"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Eliminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8eb4087-3a4c-4de8-8e29-f4cd71acb180.jpg?1594736106", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8eb4087-3a4c-4de8-8e29-f4cd71acb180.jpg?1594736106"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Eliminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c55b2b50-ac83-4a78-8f84-580193d1ca0f.jpg?1623780234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c55b2b50-ac83-4a78-8f84-580193d1ca0f.jpg?1623780234"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Enduring Victory", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54fef763-7ee2-4341-9c67-546e4b6710b7.jpg?1562786446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54fef763-7ee2-4341-9c67-546e4b6710b7.jpg?1562786446"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Vortex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe07e496-5070-4116-a91a-a3bbe19c12af.jpg?1562942896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe07e496-5070-4116-a91a-a3bbe19c12af.jpg?1562942896"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eviscerate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62ba90b8-3a30-4058-b8d3-72900b1f4fe0.jpg?1562736723", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62ba90b8-3a30-4058-b8d3-72900b1f4fe0.jpg?1562736723"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Execute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/333123bc-fb66-4b5a-bf55-045d2906c8c3.jpg?1562904481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/333123bc-fb66-4b5a-bf55-045d2906c8c3.jpg?1562904481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Expunge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/0576ffe8-a7b9-479b-8ea0-418b430b1aa1.jpg?1562896134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/0576ffe8-a7b9-479b-8ea0-418b430b1aa1.jpg?1562896134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eyeblight's Ending", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0c08701-7038-4d6b-bbf8-056fd8ffb226.jpg?1562371343", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0c08701-7038-4d6b-bbf8-056fd8ffb226.jpg?1562371343"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/044dc7c2-6198-4526-b79a-f3d8ee7a157a.jpg?1562799109", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/044dc7c2-6198-4526-b79a-f3d8ee7a157a.jpg?1562799109"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5e81649-9954-424c-89d1-f87d73b66047.jpg?1595869185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5e81649-9954-424c-89d1-f87d73b66047.jpg?1595869185"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/5427d8a6-ac9e-4e50-bd39-81713b2ade25.jpg?1607041515", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/5427d8a6-ac9e-4e50-bd39-81713b2ade25.jpg?1607041515"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9a50516-a20f-4e6e-b4f2-0049b673f942.jpg?1599711004", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9a50516-a20f-4e6e-b4f2-0049b673f942.jpg?1599711004"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Fatal Push", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86d1119d-7585-4699-8649-e3743c02d7a9.jpg?1562636837", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86d1119d-7585-4699-8649-e3743c02d7a9.jpg?1562636837"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Fateful Absence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/eca8d6f8-c6f1-437c-99e2-4281eae14a6f.jpg?1634346819", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/eca8d6f8-c6f1-437c-99e2-4281eae14a6f.jpg?1634346819"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1a7dd5e2-b2a5-46ab-a67c-499451706505.jpg?1562610240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1a7dd5e2-b2a5-46ab-a67c-499451706505.jpg?1562610240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/658bf8b7-fbc4-4046-9300-249cdeb87924.jpg?1561757312", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/658bf8b7-fbc4-4046-9300-249cdeb87924.jpg?1561757312"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Feast of Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de07e21e-c12a-47a6-ad2c-ef6fed343407.jpg?1593095705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de07e21e-c12a-47a6-ad2c-ef6fed343407.jpg?1593095705"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92105bc6-b64a-4bdc-99fe-7a2ccdbd4486.jpg?1592713797", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92105bc6-b64a-4bdc-99fe-7a2ccdbd4486.jpg?1592713797"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/302ec21d-bb10-4651-80da-11852768165d.jpg?1559592569", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/302ec21d-bb10-4651-80da-11852768165d.jpg?1559592569"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Feast or Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c185b4d-8da5-4b8a-85f0-5f0622c7bade.jpg?1562769209", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c185b4d-8da5-4b8a-85f0-5f0622c7bade.jpg?1562769209"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feed the Swarm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6b2eba7-862a-4efd-9f65-065fb2070855.jpg?1604195649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6b2eba7-862a-4efd-9f65-065fb2070855.jpg?1604195649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fierce Retribution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/9597b163-5c6b-4f64-b1f1-5f1fa2e23e5d.jpg?1643586258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/9597b163-5c6b-4f64-b1f1-5f1fa2e23e5d.jpg?1643586258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Payment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49a21a8f-9c7b-4ae8-8635-f2ee2151c8de.jpg?1584831505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49a21a8f-9c7b-4ae8-8635-f2ee2151c8de.jpg?1584831505"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Finders, Keepers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f4b7148-e98f-40a4-95e3-ffdd2daa324b.jpg?1562914921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f4b7148-e98f-40a4-95e3-ffdd2daa324b.jpg?1562914921"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Finishing Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b85a552-2119-4d9c-b7c1-c09c2d9f2f38.jpg?1594736130", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b85a552-2119-4d9c-b7c1-c09c2d9f2f38.jpg?1594736130"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Fissure", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa2d778d-d74b-45ec-a86b-5d52ffad6ba5.jpg?1562935207", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa2d778d-d74b-45ec-a86b-5d52ffad6ba5.jpg?1562935207"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flash Flood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5ae88c06-f28c-4fbc-a28c-5eb203a04722.jpg?1562859177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5ae88c06-f28c-4fbc-a28c-5eb203a04722.jpg?1562859177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flesh Allergy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c729525-b954-42dd-9877-f4360d99b961.jpg?1562820900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c729525-b954-42dd-9877-f4360d99b961.jpg?1562820900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flesh to Dust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16b2e842-6c92-47b0-bed4-e0e64485f168.jpg?1562783120", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16b2e842-6c92-47b0-bed4-e0e64485f168.jpg?1562783120"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Foul Play", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e4b75c-e993-4983-8933-977be314bba6.jpg?1634349812", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e4b75c-e993-4983-8933-977be314bba6.jpg?1634349812"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fumarole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa53e9a-0d7c-4d17-b2be-56930edfa2c2.jpg?1562940031", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa53e9a-0d7c-4d17-b2be-56930edfa2c2.jpg?1562940031"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gang Up", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10d01449-3e4e-44ef-90aa-9489c86c57df.jpg?1595438095", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10d01449-3e4e-44ef-90aa-9489c86c57df.jpg?1595438095"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Get the Point", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/821c4ab5-eb75-445a-bbec-e50af54dba7a.jpg?1584831541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/821c4ab5-eb75-445a-bbec-e50af54dba7a.jpg?1584831541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghastly Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9d2bfa3-0499-43ea-a76d-b12fddbc104e.jpg?1562935702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9d2bfa3-0499-43ea-a76d-b12fddbc104e.jpg?1562935702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghostly Visit", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/06f6938a-229a-4521-b5d5-7999ce5fb372.jpg?1562255824", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/06f6938a-229a-4521-b5d5-7999ce5fb372.jpg?1562255824"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Gloomlance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b45bfb2-7c48-4da5-a0fd-29d353221814.jpg?1562832072", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b45bfb2-7c48-4da5-a0fd-29d353221814.jpg?1562832072"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gloomwidow's Feast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59b989b4-692c-4ccb-a290-0ff00abacba9.jpg?1562830513", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59b989b4-692c-4ccb-a290-0ff00abacba9.jpg?1562830513"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Go for the Throat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c6cb231-41df-409c-923e-100319f27ee3.jpg?1562605365", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c6cb231-41df-409c-923e-100319f27ee3.jpg?1562605365"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Go for the Throat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3109aaa-b1e9-4c68-85f0-7515c8eeadc3.jpg?1562636862", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3109aaa-b1e9-4c68-85f0-7515c8eeadc3.jpg?1562636862"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Grim Bounty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b98e0ab1-dea8-492b-a712-2057f2b1d020.jpg?1627704924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b98e0ab1-dea8-492b-a712-2057f2b1d020.jpg?1627704924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grisly Ritual", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/53cdf2ab-3acd-49bd-8273-84c1cfc92883.jpg?1643589817", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/53cdf2ab-3acd-49bd-8273-84c1cfc92883.jpg?1643589817"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grisly Spectacle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c26d0f6e-e7bd-4206-a0da-1c9c203a73f2.jpg?1561844583", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c26d0f6e-e7bd-4206-a0da-1c9c203a73f2.jpg?1561844583"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guiding Bolt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd552f81-1947-47e0-beee-f04e73551055.jpg?1653690524", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd552f81-1947-47e0-beee-f04e73551055.jpg?1653690524"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Hand of Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9761136-9e1c-4d86-98ce-7abe1d8e6a8d.jpg?1562935064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9761136-9e1c-4d86-98ce-7abe1d8e6a8d.jpg?1562935064"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hand of Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27f136b8-52be-49b9-919b-2b9785254350.jpg?1546740328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27f136b8-52be-49b9-919b-2b9785254350.jpg?1546740328"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Hearth Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/caa9ac66-51b7-4aec-92dc-0f0656b0f7fe.jpg?1562278639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/caa9ac66-51b7-4aec-92dc-0f0656b0f7fe.jpg?1562278639"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Heartless Act", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4e6794a-feeb-4fc8-a2ee-38c75c18aaae.jpg?1591226819", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4e6794a-feeb-4fc8-a2ee-38c75c18aaae.jpg?1591226819"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d22dd514-814f-4a62-926d-fef311896c02.jpg?1562879959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d22dd514-814f-4a62-926d-fef311896c02.jpg?1562879959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1b0751e-3a7e-4568-8c64-7429d6829687.jpg?1643589948", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1b0751e-3a7e-4568-8c64-7429d6829687.jpg?1643589948"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/596822f6-dbd4-4cc8-aa50-9331ff42544e.jpg?1562818494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/596822f6-dbd4-4cc8-aa50-9331ff42544e.jpg?1562818494"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed96b05d-b2ca-4c8f-969b-cac9b4562fab.jpg?1636900809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed96b05d-b2ca-4c8f-969b-cac9b4562fab.jpg?1636900809"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Hero's Downfall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/64aa5cbd-98e9-46fc-8de4-64eab7afc90f.jpg?1561757293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/64aa5cbd-98e9-46fc-8de4-64eab7afc90f.jpg?1561757293"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hideous End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b33e6056-00c9-4731-b364-b0214398848d.jpg?1562842860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b33e6056-00c9-4731-b364-b0214398848d.jpg?1562842860"}, "reprint": false, "digital": false, "set_type": "planechase"}, {"name": "Horobi's Whisper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1aad5179-4b73-498e-85c5-1fc363d26223.jpg?1562875751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1aad5179-4b73-498e-85c5-1fc363d26223.jpg?1562875751"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Human Frailty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d1de712-86ac-4c03-be86-2403cd121f66.jpg?1592708908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d1de712-86ac-4c03-be86-2403cd121f66.jpg?1592708908"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Humble the Brute", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c105686-8b45-494a-b9ef-8aa267bb1b5a.jpg?1656286373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c105686-8b45-494a-b9ef-8aa267bb1b5a.jpg?1656286373"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f468338-bb66-4db0-a883-69095566092b.jpg?1562904646", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f468338-bb66-4db0-a883-69095566092b.jpg?1562904646"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0ddbcd23-e206-4a12-968a-3854693d1e60.jpg?1562870987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0ddbcd23-e206-4a12-968a-3854693d1e60.jpg?1562870987"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Impale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfa0c4f7-3497-467d-9453-104fb4b5a0f3.jpg?1555040252", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfa0c4f7-3497-467d-9453-104fb4b5a0f3.jpg?1555040252"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infernal Grasp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/17824929-f131-4b8d-addb-66c25323155e.jpg?1634349911", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/17824929-f131-4b8d-addb-66c25323155e.jpg?1634349911"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inscription of Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/93612079-0b8d-489d-9ae1-3593414a8cee.jpg?1604195857", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/93612079-0b8d-489d-9ae1-3593414a8cee.jpg?1604195857"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Into the Maw of Hell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d188d9b-7a12-4eaf-855b-af4f0204dc5a.jpg?1562830878", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d188d9b-7a12-4eaf-855b-af4f0204dc5a.jpg?1562830878"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Just Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6e5e572-030d-4a41-89e6-e720b49bc131.jpg?1562934537", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6e5e572-030d-4a41-89e6-e720b49bc131.jpg?1562934537"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Kaervek's Purge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a42ef95-92ec-40fe-ab30-a476f012a525.jpg?1562720237"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kill! Destroy!", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49dd5a66-101d-4f88-b1ba-e2368203d408.jpg?1605097368", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49dd5a66-101d-4f88-b1ba-e2368203d408.jpg?1605097368"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Killing Glare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7a4d87d-b844-4f20-8b14-4fd32c53dea5.jpg?1561852883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7a4d87d-b844-4f20-8b14-4fd32c53dea5.jpg?1561852883"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kill Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b0b9a3-8f50-4fba-9978-409f3369afa6.jpg?1650026094", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b0b9a3-8f50-4fba-9978-409f3369afa6.jpg?1650026094"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Kill Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f30d4136-78a3-4760-83af-d365cc97d118.jpg?1562795914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f30d4136-78a3-4760-83af-d365cc97d118.jpg?1562795914"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror2.json b/web/public/mtg/jsons/terror2.json deleted file mode 100644 index 162e6ad0..00000000 --- a/web/public/mtg/jsons/terror2.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Krovikan Rot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/17597c66-0d9f-41af-9160-0d92be88f450.jpg?1593275116", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/17597c66-0d9f-41af-9160-0d92be88f450.jpg?1593275116"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Launch Party", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/53f29821-902e-41bc-97a2-6fc7a710cbdb.jpg?1562786438", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/53f29821-902e-41bc-97a2-6fc7a710cbdb.jpg?1562786438"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lava Flow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89e825e4-98be-49f0-bc5e-c8988118dcef.jpg?1562446890", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89e825e4-98be-49f0-bc5e-c8988118dcef.jpg?1562446890"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Legion's Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/385bea20-c196-4da8-bc3e-36f8d50dcc17.jpg?1562553483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/385bea20-c196-4da8-bc3e-36f8d50dcc17.jpg?1562553483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lethal Scheme", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65864680-9520-4eb3-9774-fa478e54a290.jpg?1650411151", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65864680-9520-4eb3-9774-fa478e54a290.jpg?1650411151"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Lethal Sting", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eaded6bf-2db7-4b1d-93cc-4b7b571cd2de.jpg?1562819094", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eaded6bf-2db7-4b1d-93cc-4b7b571cd2de.jpg?1562819094"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lich's Caress", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32bd3acd-aa62-4708-9336-e3430fd0e541.jpg?1562301277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32bd3acd-aa62-4708-9336-e3430fd0e541.jpg?1562301277"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Liliana's Defeat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f72b028-b9df-40c7-822f-4acc6bdcc719.jpg?1562789479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f72b028-b9df-40c7-822f-4acc6bdcc719.jpg?1562789479"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Liliana's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b231f941-4acb-46f2-81ae-16e5a28e65af.jpg?1596250190", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b231f941-4acb-46f2-81ae-16e5a28e65af.jpg?1596250190"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Liturgy of Blood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/3532105d-c550-4c20-8465-a6a19169efbd.jpg?1562827834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/3532105d-c550-4c20-8465-a6a19169efbd.jpg?1562827834"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb651c3a-cb27-4b73-8eb6-b87d65211097.jpg?1562644898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb651c3a-cb27-4b73-8eb6-b87d65211097.jpg?1562644898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2d85423-ebd8-4a6e-aedf-90e52f918764.jpg?1562940541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2d85423-ebd8-4a6e-aedf-90e52f918764.jpg?1562940541"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Maelstrom Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d351c901-103b-460f-9d01-6e4d4b25cac8.jpg?1561929932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d351c901-103b-460f-9d01-6e4d4b25cac8.jpg?1561929932"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Mage Hunters' Onslaught", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed85140f-f0e0-4ac1-a67f-26d17ff95e31.jpg?1624591129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed85140f-f0e0-4ac1-a67f-26d17ff95e31.jpg?1624591129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Malicious Affliction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6ea704f-a06c-4d3b-80a3-d23f739c74aa.jpg?1561960653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6ea704f-a06c-4d3b-80a3-d23f739c74aa.jpg?1561960653"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Misfortune's Gain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/80abd7c1-8f7a-4279-b76f-251a02624345.jpg?1562257029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/80abd7c1-8f7a-4279-b76f-251a02624345.jpg?1562257029"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Mob", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c216e13-3779-4734-b481-9aad7aba9925.jpg?1562201673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c216e13-3779-4734-b481-9aad7aba9925.jpg?1562201673"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Molten Frame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58356504-e28e-456c-b1d3-e6232f4d78a6.jpg?1562801105", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58356504-e28e-456c-b1d3-e6232f4d78a6.jpg?1562801105"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/38c5e2e8-b781-4265-bce1-98fa25ddd8c3.jpg?1592714339", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/38c5e2e8-b781-4265-bce1-98fa25ddd8c3.jpg?1592714339"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b2c5187-71c7-4801-8a76-339c67322d35.jpg?1593272729", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b2c5187-71c7-4801-8a76-339c67322d35.jpg?1593272729"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/645f4d28-68cb-4386-91b9-c748930d69fa.jpg?1570573674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/645f4d28-68cb-4386-91b9-c748930d69fa.jpg?1570573674"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Mortify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a36a42b0-8216-4c99-a85f-22a520f31fd4.jpg?1561757738", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a36a42b0-8216-4c99-a85f-22a520f31fd4.jpg?1561757738"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdef7fea-2bd0-42a2-96f6-6def18bd7f0c.jpg?1653725816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdef7fea-2bd0-42a2-96f6-6def18bd7f0c.jpg?1653725816"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c13ac76-7cd9-456f-9b89-92bfa07c64c5.jpg?1649362504", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c13ac76-7cd9-456f-9b89-92bfa07c64c5.jpg?1649362504"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f2eb849-b3ab-4d26-86c5-235c8161cf2a.jpg?1576384369", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f2eb849-b3ab-4d26-86c5-235c8161cf2a.jpg?1576384369"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Murder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8676f02-cf1e-4d40-a0c5-6e5a97417898.jpg?1562559978", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8676f02-cf1e-4d40-a0c5-6e5a97417898.jpg?1562559978"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Murderous Compulsion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33b94db1-ac8c-4667-81d5-408df0f30879.jpg?1576384534", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33b94db1-ac8c-4667-81d5-408df0f30879.jpg?1576384534"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murderous Cut", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b2dadff2-883f-4134-a881-be145cdcbd84.jpg?1562792142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b2dadff2-883f-4134-a881-be145cdcbd84.jpg?1562792142"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murderous Spoils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91ece344-c516-449e-ab7c-2e78d4778f02.jpg?1562638187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91ece344-c516-449e-ab7c-2e78d4778f02.jpg?1562638187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mutual Destruction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85ac0b25-80bf-4871-a6f6-5cf4d5b9496e.jpg?1591226898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85ac0b25-80bf-4871-a6f6-5cf4d5b9496e.jpg?1591226898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mythos of Nethroi", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6abc24e1-e721-471a-9efd-547f320675b0.jpg?1591226925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6abc24e1-e721-471a-9efd-547f320675b0.jpg?1591226925"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Neck Snap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc326b79-363e-4c14-86e4-23041f2d6b4f.jpg?1562375861", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc326b79-363e-4c14-86e4-23041f2d6b4f.jpg?1562375861"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Noxious Grasp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e5758cc-1f84-455d-a983-8ec471727eaf.jpg?1592516744", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e5758cc-1f84-455d-a983-8ec471727eaf.jpg?1592516744"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9961562d-cad9-40e5-afae-3ebce77a2260.jpg?1648583418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obscura Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a02b758-65b6-4c25-83b9-de63a1a92b51.jpg?1648583494"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Orim's Thunder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d00bf192-4baf-46ba-947b-a22d07635b04.jpg?1562944526", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d00bf192-4baf-46ba-947b-a22d07635b04.jpg?1562944526"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Orzhov Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8ca44265-5e1b-4fbf-9002-52b2ce9b7448.jpg?1561835927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8ca44265-5e1b-4fbf-9002-52b2ce9b7448.jpg?1561835927"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parting Thoughts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e60b5a1-923c-4c67-ae06-2a498dc46506.jpg?1562393855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e60b5a1-923c-4c67-ae06-2a498dc46506.jpg?1562393855"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41369848-ba9a-40ef-931e-1a65bc979209.jpg?1562434966", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41369848-ba9a-40ef-931e-1a65bc979209.jpg?1562434966"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af7a2719-7910-4601-be88-7b3c249199d3.jpg?1562932043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af7a2719-7910-4601-be88-7b3c249199d3.jpg?1562932043"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb14d3f4-09f3-4113-bdc3-0fd753137f7c.jpg?1562942983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb14d3f4-09f3-4113-bdc3-0fd753137f7c.jpg?1562942983"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Path of Peace", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1f3e1c9-bfad-49a1-b171-6fa344ef2eef.jpg?1562447361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1f3e1c9-bfad-49a1-b171-6fa344ef2eef.jpg?1562447361"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Phthisis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9ba55f16-a37c-4caa-9417-227a06cf4061.jpg?1562927843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9ba55f16-a37c-4caa-9417-227a06cf4061.jpg?1562927843"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pinion Feast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45d6df03-c3c3-42c3-85a4-6fccb0741592.jpg?1562785514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45d6df03-c3c3-42c3-85a4-6fccb0741592.jpg?1562785514"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pistus Strike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1a2918d6-50f7-4bc1-aef2-930a5c84be8d.jpg?1562609919", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1a2918d6-50f7-4bc1-aef2-930a5c84be8d.jpg?1562609919"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pitfall Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/2823d9a5-dd2f-4e6a-8e3d-554c4204aa32.jpg?1562610754", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/2823d9a5-dd2f-4e6a-8e3d-554c4204aa32.jpg?1562610754"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Spores", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d106d56-a688-49cc-8d5d-0279a5a7c0a7.jpg?1562897663", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d106d56-a688-49cc-8d5d-0279a5a7c0a7.jpg?1562897663"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/5469e696-bbf1-43e3-9c25-fe089b36caed.jpg?1636224615", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/5469e696-bbf1-43e3-9c25-fe089b36caed.jpg?1636224615"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4be85ceb-be98-43ce-9565-a72990797437.jpg?1627708161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4be85ceb-be98-43ce-9565-a72990797437.jpg?1627708161"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d884b2f2-946e-4d5d-b8cf-ef035726a188.jpg?1591227840", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d884b2f2-946e-4d5d-b8cf-ef035726a188.jpg?1591227840"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8b2f186-4e04-49cb-a206-257cfb7e9361.jpg?1581480847", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8b2f186-4e04-49cb-a206-257cfb7e9361.jpg?1581480847"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54a0afaa-f99f-4c7a-9fa1-c6a46dfb2a29.jpg?1561480279", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54a0afaa-f99f-4c7a-9fa1-c6a46dfb2a29.jpg?1561480279"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f6acb5b-b087-4cad-b40f-2de37029847c.jpg?1562917482", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f6acb5b-b087-4cad-b40f-2de37029847c.jpg?1562917482"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Plummet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a67bb585-cc4f-4cbc-9a5a-d31df98c07ae.jpg?1562930081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a67bb585-cc4f-4cbc-9a5a-d31df98c07ae.jpg?1562930081"}, "reprint": false, "digital": false, "set_type": "archenemy"}, {"name": "Poison Arrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b7b5f34-c250-484e-9bae-94789b2a87fb.jpg?1562256571", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b7b5f34-c250-484e-9bae-94789b2a87fb.jpg?1562256571"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Poison the Cup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fb94456-5266-47db-b514-a0e17e34b771.jpg?1631048334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fb94456-5266-47db-b514-a0e17e34b771.jpg?1631048334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Polymorph", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fbae8702-a152-4c53-8a76-691a221f2475.jpg?1562722872", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fbae8702-a152-4c53-8a76-691a221f2475.jpg?1562722872"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pongify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cce74a84-4441-4f2e-89d8-df0b096790ed.jpg?1562582099", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cce74a84-4441-4f2e-89d8-df0b096790ed.jpg?1562582099"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Power Word Kill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/395b6ce4-143f-4eed-b565-98aa3d6208ef.jpg?1627705234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/395b6ce4-143f-4eed-b565-98aa3d6208ef.jpg?1627705234"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Power Word Kill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36c71043-1c11-4377-ab33-41d19927143a.jpg?1654010561", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36c71043-1c11-4377-ab33-41d19927143a.jpg?1654010561"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Premature Burial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e96cea6a-fea6-4a6b-84b2-7b57237be96a.jpg?1562944222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e96cea6a-fea6-4a6b-84b2-7b57237be96a.jpg?1562944222"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Price of Fame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b52152-0f7c-4466-9e49-033477028f67.jpg?1572893038", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b52152-0f7c-4466-9e49-033477028f67.jpg?1572893038"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prismatic Wardrobe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/79624ebe-7110-486d-82ff-b64c662dc6de.jpg?1593865843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/79624ebe-7110-486d-82ff-b64c662dc6de.jpg?1593865843"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Public Execution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48188942-d0ba-4503-bd75-c7a5329bb7c8.jpg?1562553248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48188942-d0ba-4503-bd75-c7a5329bb7c8.jpg?1562553248"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Puncturing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5b101264-4994-43b7-9156-228f7d10d2bd.jpg?1576383877", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5b101264-4994-43b7-9156-228f7d10d2bd.jpg?1576383877"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Puncturing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e52d260a-e1ca-4228-855e-2e104b86fd6c.jpg?1562709696", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e52d260a-e1ca-4228-855e-2e104b86fd6c.jpg?1562709696"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Purge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdcbe727-81f0-469e-92f1-0dd9acdb54ea.jpg?1562639281", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdcbe727-81f0-469e-92f1-0dd9acdb54ea.jpg?1562639281"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d43a0b6-2a5c-4959-96ee-6e570949dfed.jpg?1562897570", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d43a0b6-2a5c-4959-96ee-6e570949dfed.jpg?1562897570"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c0aca3e-d91d-4bb7-ba4a-500d93f71718.jpg?1592713790", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c0aca3e-d91d-4bb7-ba4a-500d93f71718.jpg?1592713790"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0a16086c-5a74-45d0-8b38-e832cfbc80f7.jpg?1598917276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0a16086c-5a74-45d0-8b38-e832cfbc80f7.jpg?1598917276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/3882ebea-2864-40ef-a21d-6ba80a0bd417.jpg?1624065750", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/3882ebea-2864-40ef-a21d-6ba80a0bd417.jpg?1624065750"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Putrefy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/236f46d9-276b-4418-a959-39b0963fc525.jpg?1561756786", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/236f46d9-276b-4418-a959-39b0963fc525.jpg?1561756786"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiant's Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28d2718e-c6fc-4961-b094-11f25f1177ff.jpg?1562862779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28d2718e-c6fc-4961-b094-11f25f1177ff.jpg?1562862779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rapid Hybridization", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83557f55-f1ab-4995-9cc1-37be895a59db.jpg?1561834181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83557f55-f1ab-4995-9cc1-37be895a59db.jpg?1561834181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reach of Shadows", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbf9a803-473a-4c38-b352-d47c4fd93d5e.jpg?1562829283", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbf9a803-473a-4c38-b352-d47c4fd93d5e.jpg?1562829283"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reave Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce7ff657-aa44-4336-895a-87518159cef6.jpg?1572490229", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce7ff657-aa44-4336-895a-87518159cef6.jpg?1572490229"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Reave Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db3d5e9d-07e8-43e1-aaf0-1f9e4ed2834a.jpg?1562045144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db3d5e9d-07e8-43e1-aaf0-1f9e4ed2834a.jpg?1562045144"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rebuke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/267185ac-a176-423e-a7f8-ee966d1d9a1e.jpg?1562827636", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/267185ac-a176-423e-a7f8-ee966d1d9a1e.jpg?1562827636"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Regicide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07f56287-91e0-418f-8b57-35c6c30cee33.jpg?1576381853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07f56287-91e0-418f-8b57-35c6c30cee33.jpg?1576381853"}, "reprint": false, "frame_effects": ["draft"], "digital": false, "set_type": "draft_innovation"}, {"name": "Reign of Chaos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/9285b14a-fc8e-457a-b803-202e05be41e5.jpg?1562720487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/9285b14a-fc8e-457a-b803-202e05be41e5.jpg?1562720487"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rend Flesh", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92b300a3-e6a8-4ca9-bb26-03f57b5ff6ec.jpg?1562762516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92b300a3-e6a8-4ca9-bb26-03f57b5ff6ec.jpg?1562762516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/343baad1-dd58-4d64-9b0a-258618094ceb.jpg?1593095328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/343baad1-dd58-4d64-9b0a-258618094ceb.jpg?1593095328"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/3868f7ff-8a84-4153-bf5a-ff001d34e0f0.jpg?1562235914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/3868f7ff-8a84-4153-bf5a-ff001d34e0f0.jpg?1562235914"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/179f50be-6658-42f4-b9b9-c97c7d3f239a.jpg?1562768219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/179f50be-6658-42f4-b9b9-c97c7d3f239a.jpg?1562768219"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reprisal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/839df85a-1aca-4d4b-b327-2778caa6d289.jpg?1562769214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/839df85a-1aca-4d4b-b327-2778caa6d289.jpg?1562769214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Return to the Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95a53144-2ef3-47d9-a176-73d620202df6.jpg?1562827827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95a53144-2ef3-47d9-a176-73d620202df6.jpg?1562827827"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ride Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c021868f-9ab8-4a52-b12e-3cc35c9d67f0.jpg?1576385014", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c021868f-9ab8-4a52-b12e-3cc35c9d67f0.jpg?1576385014"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ride Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bc9a434-9617-4a20-88f0-355b20f2c538.jpg?1562785134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bc9a434-9617-4a20-88f0-355b20f2c538.jpg?1562785134"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rite of the Serpent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/005b9fec-66de-4079-88e0-c7de7e22d18e.jpg?1562781741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/005b9fec-66de-4079-88e0-c7de7e22d18e.jpg?1562781741"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ruinous Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/709ab9cf-eed8-4d73-b10d-c7f6d8750328.jpg?1562921535", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/709ab9cf-eed8-4d73-b10d-c7f6d8750328.jpg?1562921535"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ruinous Path", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a41a241-ee56-486a-9b4d-fb355b5f65b2.jpg?1562133050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a41a241-ee56-486a-9b4d-fb355b5f65b2.jpg?1562133050"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sagittars' Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saltblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/edd1833d-64b0-4c9b-8f6b-1cf15c29d473.jpg?1562585578", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/edd1833d-64b0-4c9b-8f6b-1cf15c29d473.jpg?1562585578"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saw in Half", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05e6a7bc-a35a-4e68-99a0-be264553b5de.jpg?1638258467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05e6a7bc-a35a-4e68-99a0-be264553b5de.jpg?1638258467"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Scorch the Fields", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05c4338d-e5c0-46b4-ab16-1f9aa97b4026.jpg?1562896337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05c4338d-e5c0-46b4-ab16-1f9aa97b4026.jpg?1562896337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Searing Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/76dadfd8-8492-4c55-827c-cd4e6a40ae97.jpg?1562918808", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/76dadfd8-8492-4c55-827c-cd4e6a40ae97.jpg?1562918808"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seize the Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29bf245f-e8e0-4d32-8cd7-06d832609910.jpg?1593272276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29bf245f-e8e0-4d32-8cd7-06d832609910.jpg?1593272276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Severed Strands", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bce654d6-fcf1-40a8-8bdb-5c37e561f7dc.jpg?1572893052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bce654d6-fcf1-40a8-8bdb-5c37e561f7dc.jpg?1572893052"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sever Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df1cb775-3a45-4f2c-9c45-febda6434c59.jpg?1562939859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df1cb775-3a45-4f2c-9c45-febda6434c59.jpg?1562939859"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Sever Soul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2d84fec-18f1-4231-a293-0dc1ff868a40.jpg?1562383023", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2d84fec-18f1-4231-a293-0dc1ff868a40.jpg?1562383023"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sheer Drop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca6e9658-684e-44fd-9c72-c5c3faa9fb1f.jpg?1593095413", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca6e9658-684e-44fd-9c72-c5c3faa9fb1f.jpg?1593095413"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Silverstrike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f27b92a-cde9-41bc-9b23-d83b74b167d4.jpg?1576383889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f27b92a-cde9-41bc-9b23-d83b74b167d4.jpg?1576383889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sip of Hemlock", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/22051427-9b2a-4571-8c9f-ee84d8d0e4d1.jpg?1562815635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/22051427-9b2a-4571-8c9f-ee84d8d0e4d1.jpg?1562815635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skywhaler's Shot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54dd4948-dc79-4fe5-b4a0-fb257058f9dd.jpg?1576381006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54dd4948-dc79-4fe5-b4a0-fb257058f9dd.jpg?1576381006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8ff06c7d-5e78-4bcf-864b-34487f6555b2.jpg?1562088317", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8ff06c7d-5e78-4bcf-864b-34487f6555b2.jpg?1562088317"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter Pact", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42696fdb-de1f-44ae-bef3-b6af068958d0.jpg?1562908356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42696fdb-de1f-44ae-bef3-b6af068958d0.jpg?1562908356"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughter Pact", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc8475bd-bdd4-421c-ace7-c6262f7405ce.jpg?1562932879", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc8475bd-bdd4-421c-ace7-c6262f7405ce.jpg?1562932879"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Slay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/eccda747-2680-4793-8a13-35e49b4de12f.jpg?1562944937", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/eccda747-2680-4793-8a13-35e49b4de12f.jpg?1562944937"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slingbow Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/def592b9-9d8b-4e2d-9b52-e1bc9f4bd019.jpg?1562297661", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/def592b9-9d8b-4e2d-9b52-e1bc9f4bd019.jpg?1562297661"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ff799e40-fd40-4f6a-8fa8-c22d77476168.jpg?1561854361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ff799e40-fd40-4f6a-8fa8-c22d77476168.jpg?1561854361"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/2698f01a-8574-4ae8-9441-a4361b1c29c6.jpg?1562702095", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/2698f01a-8574-4ae8-9441-a4361b1c29c6.jpg?1562702095"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14f165ad-cfe6-4a5d-8073-a70969494855.jpg?1562595916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14f165ad-cfe6-4a5d-8073-a70969494855.jpg?1562595916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c103163-31b7-4d25-aa2c-02ca082ee1bf.jpg?1604193448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c103163-31b7-4d25-aa2c-02ca082ee1bf.jpg?1604193448"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/766aad27-e987-45ab-82aa-e5f44fcc34ef.jpg?1562922992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/766aad27-e987-45ab-82aa-e5f44fcc34ef.jpg?1562922992"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1405bb2e-2204-43ab-82a3-5d0c8537325a.jpg?1562782881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1405bb2e-2204-43ab-82a3-5d0c8537325a.jpg?1562782881"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0103f3b1-88c2-4cbf-a67c-49420f92970f.jpg?1562825351", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0103f3b1-88c2-4cbf-a67c-49420f92970f.jpg?1562825351"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Smite the Monstrous", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/2969e9b5-64d3-401f-9878-32ec283680ab.jpg?1562633742", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/2969e9b5-64d3-401f-9878-32ec283680ab.jpg?1562633742"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Smother", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/09b4deea-c077-46ab-898f-41b3907ecf33.jpg?1562281733", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/09b4deea-c077-46ab-898f-41b3907ecf33.jpg?1562281733"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Smother", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/a/9a8321af-d667-44e7-8c03-3957286604b9.jpg?1562931422", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/a/9a8321af-d667-44e7-8c03-3957286604b9.jpg?1562931422"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snuff Out", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db8b3560-4940-40cc-9797-f909dcb1519b.jpg?1562090223", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db8b3560-4940-40cc-9797-f909dcb1519b.jpg?1562090223"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Snuff Out", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/8/18a3cca1-e50e-49b6-9e1a-f86640e3b177.jpg?1562379436", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/8/18a3cca1-e50e-49b6-9e1a-f86640e3b177.jpg?1562379436"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Reap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2a129e2-bed5-4ee7-b223-851452f72682.jpg?1562942827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2a129e2-bed5-4ee7-b223-851452f72682.jpg?1562942827"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Rend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fa084e1-05c2-4691-b9fe-3e3c717e5c9d.jpg?1562720249", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fa084e1-05c2-4691-b9fe-3e3c717e5c9d.jpg?1562720249"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Harvest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2013a138-f8e2-4a67-91e8-759288d985a7.jpg?1557576556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2013a138-f8e2-4a67-91e8-759288d985a7.jpg?1557576556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiteful Blow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fafaa798-e534-4cd0-b369-9e767a02fe3d.jpg?1593095848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fafaa798-e534-4cd0-b369-9e767a02fe3d.jpg?1593095848"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spread the Sickness", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de42a771-4f5c-4295-b070-8cb857a0279e.jpg?1562615413", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de42a771-4f5c-4295-b070-8cb857a0279e.jpg?1562615413"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Strangling Soot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/6723e552-baf5-4b6a-8af6-843fd8597f6c.jpg?1562916570", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/6723e552-baf5-4b6a-8af6-843fd8597f6c.jpg?1562916570"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stream of Acid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbbf00b3-2a1b-4ad3-8a5b-deec9e08a231.jpg?1562875294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbbf00b3-2a1b-4ad3-8a5b-deec9e08a231.jpg?1562875294"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Sultai Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/993c9028-9b1b-4903-81b2-3cf4f37b7229.jpg?1562790829", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/993c9028-9b1b-4903-81b2-3cf4f37b7229.jpg?1562790829"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sultai Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c72495e-3c03-4dff-b671-47764af5058d.jpg?1562701596", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c72495e-3c03-4dff-b671-47764af5058d.jpg?1562701596"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sungold Barrage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee507688-9890-47c4-bb04-43c51eb48e22.jpg?1634348527", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee507688-9890-47c4-bb04-43c51eb48e22.jpg?1634348527"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Surge of Righteousness", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/114366f3-237f-4f96-b644-5bd82d97b18b.jpg?1562782657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/114366f3-237f-4f96-b644-5bd82d97b18b.jpg?1562782657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cec3a260-6c50-401d-a0ff-bf49a973e1a1.jpg?1562943805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cec3a260-6c50-401d-a0ff-bf49a973e1a1.jpg?1562943805"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Swat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/947b8923-d9d6-4dd8-928b-91be9105ffb4.jpg?1562863743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/947b8923-d9d6-4dd8-928b-91be9105ffb4.jpg?1562863743"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swift Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/904cb2f5-eb62-4416-8236-d2fbeadf1dc4.jpg?1562031231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/904cb2f5-eb62-4416-8236-d2fbeadf1dc4.jpg?1562031231"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Swift Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a90c1ad0-83bd-471c-8d4c-e65bc2abaa18.jpg?1594735305", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a90c1ad0-83bd-471c-8d4c-e65bc2abaa18.jpg?1594735305"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Take Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66fbde22-d98d-4f12-b4d8-1bad2a9878b2.jpg?1562302645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66fbde22-d98d-4f12-b4d8-1bad2a9878b2.jpg?1562302645"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Terashi's Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc5fa34b-95c6-4e02-9e15-3f595f744741.jpg?1562879427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc5fa34b-95c6-4e02-9e15-3f595f744741.jpg?1562879427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminal Agony", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3ddb6d98-3a3a-4332-a64e-97aec71777a4.jpg?1626103523", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3ddb6d98-3a3a-4332-a64e-97aec71777a4.jpg?1626103523"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8af2d815-d8b2-42ff-9889-acbe77a42583.jpg?1593814672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8af2d815-d8b2-42ff-9889-acbe77a42583.jpg?1593814672"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc8acab8-4469-4baa-af2f-a3f49b841a55.jpg?1562644597", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc8acab8-4469-4baa-af2f-a3f49b841a55.jpg?1562644597"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/190ca502-672d-4cc0-b6e0-b9de517058d0.jpg?1562900286", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/190ca502-672d-4cc0-b6e0-b9de517058d0.jpg?1562900286"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54f3c523-09dc-4f2a-9bd9-7614e061de28.jpg?1655823700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54f3c523-09dc-4f2a-9bd9-7614e061de28.jpg?1655823700"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Terminate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfd77920-7dbb-4673-9317-095ce9483878.jpg?1575602242", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfd77920-7dbb-4673-9317-095ce9483878.jpg?1575602242"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d1ccc3b-a6bd-4dc8-b7ba-99172d612106.jpg?1562546519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d1ccc3b-a6bd-4dc8-b7ba-99172d612106.jpg?1562546519"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f41651db-619a-4ab4-86cf-a0d32297dbdf.jpg?1562163040", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f41651db-619a-4ab4-86cf-a0d32297dbdf.jpg?1562163040"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21004958-2c7e-4a55-bc80-411c4d780106.jpg?1559591536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21004958-2c7e-4a55-bc80-411c4d780106.jpg?1559591536"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba9d4863-75f2-4894-8033-e4ffebe0547a.jpg?1561757930", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba9d4863-75f2-4894-8033-e4ffebe0547a.jpg?1561757930"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Tezzeret's Betrayal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d71efa6-5de8-476f-86ce-0790956e574f.jpg?1562932177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d71efa6-5de8-476f-86ce-0790956e574f.jpg?1562932177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thornado", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/eadffd6b-d707-4fc5-a600-44eb9124b195.jpg?1615475425", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/eadffd6b-d707-4fc5-a600-44eb9124b195.jpg?1615475425"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Tidy Conclusion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfcf6849-4fac-41b9-8e70-dc77c4562a42.jpg?1576381900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfcf6849-4fac-41b9-8e70-dc77c4562a42.jpg?1576381900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trip Wire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4eb1e16f-002e-4a81-ba41-cfe41f3a9071.jpg?1634292196", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4eb1e16f-002e-4a81-ba41-cfe41f3a9071.jpg?1634292196"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Triumphant Surge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75d6eb18-a49d-4fa5-a333-78aafbc4abcb.jpg?1581479273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75d6eb18-a49d-4fa5-a333-78aafbc4abcb.jpg?1581479273"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tyrant's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7e2708c-2824-4925-b529-d625deb77924.jpg?1557577324", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7e2708c-2824-4925-b529-d625deb77924.jpg?1557577324"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b41f7cf3-bd76-4184-b694-f565aa5cf3a4.jpg?1562791851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b41f7cf3-bd76-4184-b694-f565aa5cf3a4.jpg?1562791851"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2b4912a-83a2-4870-8fac-81fa79da2830.jpg?1562793639", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2b4912a-83a2-4870-8fac-81fa79da2830.jpg?1562793639"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ultimate Price", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/334e3ffc-a4dc-405c-b6e4-7182f28241fe.jpg?1562639743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/334e3ffc-a4dc-405c-b6e4-7182f28241fe.jpg?1562639743"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Unforge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d369a3da-3424-4984-a50a-59fd9c3d689e.jpg?1562639761", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d369a3da-3424-4984-a50a-59fd9c3d689e.jpg?1562639761"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unholy Hunger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/5994b7b0-3bca-480b-b265-ed269f15c17e.jpg?1562021369", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/5994b7b0-3bca-480b-b265-ed269f15c17e.jpg?1562021369"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16ad8f86-7860-4896-a161-07bf347bbd5b.jpg?1576382889"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unlicensed Disintegration", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74843584-d6b1-4ee6-bedb-999ab0a42bb9.jpg?1562636815"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0e6b9a3b-8a19-4094-8dbb-08a0a9ca04a0.jpg?1643587276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0e6b9a3b-8a19-4094-8dbb-08a0a9ca04a0.jpg?1643587276"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65998e94-15a0-41f1-8288-730b957f81df.jpg?1562825972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65998e94-15a0-41f1-8288-730b957f81df.jpg?1562825972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valorous Stance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f482213a-4e3e-4e13-82a1-88e7d6c4ba2c.jpg?1561758433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f482213a-4e3e-4e13-82a1-88e7d6c4ba2c.jpg?1561758433"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vanquish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27bae717-56c0-4028-b1e7-a445d6a57176.jpg?1562875950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27bae717-56c0-4028-b1e7-a445d6a57176.jpg?1562875950"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Foul", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fdcec06-e33c-4737-b81e-b156d6e3fd77.jpg?1562821391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fdcec06-e33c-4737-b81e-b156d6e3fd77.jpg?1562821391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c15852d4-2c79-4841-bb65-6661d88fdfab.jpg?1604196688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c15852d4-2c79-4841-bb65-6661d88fdfab.jpg?1604196688"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e599ed0b-4b3b-4341-b6ac-7fdfdc6799a3.jpg?1562565757", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e599ed0b-4b3b-4341-b6ac-7fdfdc6799a3.jpg?1562565757"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vendetta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/039fc76d-3b7e-4329-a997-07c25509e421.jpg?1562700700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/039fc76d-3b7e-4329-a997-07c25509e421.jpg?1562700700"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Vendetta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67ced38e-0f33-4bda-8e18-09f6ac03a3d7.jpg?1562381344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67ced38e-0f33-4bda-8e18-09f6ac03a3d7.jpg?1562381344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/011b9836-fee4-4e83-add7-5e13cb1275d6.jpg?1562231350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/011b9836-fee4-4e83-add7-5e13cb1275d6.jpg?1562231350"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a882fbcc-b2b9-44f3-b5cc-56759879f473.jpg?1562257514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a882fbcc-b2b9-44f3-b5cc-56759879f473.jpg?1562257514"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/3209ee48-4485-44fc-b71d-cd6241674e64.jpg?1562906693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/3209ee48-4485-44fc-b71d-cd6241674e64.jpg?1562906693"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91c249b-157c-4f1d-8171-29d1e75b1c9f.jpg?1562447828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91c249b-157c-4f1d-8171-29d1e75b1c9f.jpg?1562447828"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Venomous Vines", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db10359c-1ea8-4453-bc01-f638ad20a5ec.jpg?1562632255", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db10359c-1ea8-4453-bc01-f638ad20a5ec.jpg?1562632255"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/terror3.json b/web/public/mtg/jsons/terror3.json deleted file mode 100644 index 463fb4a7..00000000 --- a/web/public/mtg/jsons/terror3.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": false, "data": [{"name": "Victim of Night", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee4c6135-eee9-43ec-bbe8-76912352dcac.jpg?1562839346", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee4c6135-eee9-43ec-bbe8-76912352dcac.jpg?1562839346"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a1bfefd-dae8-49e9-9d56-cc852e3dc93b.jpg?1562904968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a1bfefd-dae8-49e9-9d56-cc852e3dc93b.jpg?1562904968"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e4978ecd-3c2e-49e2-98e0-0172887e4319.jpg?1628337210", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e4978ecd-3c2e-49e2-98e0-0172887e4319.jpg?1628337210"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "draft_innovation"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97aeb745-5b98-4240-a1a8-861c06d616cc.jpg?1562925629", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97aeb745-5b98-4240-a1a8-861c06d616cc.jpg?1562925629"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6fef34ec-f728-4919-9254-576ed889a654.jpg?1561757378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6fef34ec-f728-4919-9254-576ed889a654.jpg?1561757378"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Vindicate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c2d88dd-813a-4cd5-9a6a-ca6f80564078.jpg?1561756842", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c2d88dd-813a-4cd5-9a6a-ca6f80564078.jpg?1561756842"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Violet Pall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bdfd0fa3-37d2-403e-99fe-8c9e57515e9d.jpg?1562881062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bdfd0fa3-37d2-403e-99fe-8c9e57515e9d.jpg?1562881062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vraska's Stoneglare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27fc4db6-a5f5-4254-ae64-c8eaf2c98030.jpg?1572894308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27fc4db6-a5f5-4254-ae64-c8eaf2c98030.jpg?1572894308"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Walk the Plank", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0038ac6a-318f-44fb-bb64-7ae172c4aca3.jpg?1562549640", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0038ac6a-318f-44fb-bb64-7ae172c4aca3.jpg?1562549640"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Walk the Plank", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d7f3b24f-e2ec-4405-b6f5-147292063b0a.jpg?1562935396", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d7f3b24f-e2ec-4405-b6f5-147292063b0a.jpg?1562935396"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Wallop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45ce5126-e7b1-41ab-9e56-1e12927c4d27.jpg?1562909144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45ce5126-e7b1-41ab-9e56-1e12927c4d27.jpg?1562909144"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Weed Strangle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1f7fb79-19a8-483a-bf91-e687f7da4e9c.jpg?1562366513", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1f7fb79-19a8-483a-bf91-e687f7da4e9c.jpg?1562366513"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wing Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d37ba325-5a14-473b-9def-6a4660a50d7a.jpg?1562248658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d37ba325-5a14-473b-9def-6a4660a50d7a.jpg?1562248658"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wing Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19116d5d-8f2d-4e85-849d-1fbaa67e8cfd.jpg?1562862328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19116d5d-8f2d-4e85-849d-1fbaa67e8cfd.jpg?1562862328"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Winnow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d61748dd-4010-47da-8717-ca0147877057.jpg?1562937982", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d61748dd-4010-47da-8717-ca0147877057.jpg?1562937982"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Witherbloom Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87d5e94b-0b35-4efd-9158-1767dcaea38c.jpg?1624740473", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87d5e94b-0b35-4efd-9158-1767dcaea38c.jpg?1624740473"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrecking Ball", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/1182e0cf-475e-4cb9-a00a-c9a4032f51e4.jpg?1593273836", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/1182e0cf-475e-4cb9-a00a-c9a4032f51e4.jpg?1593273836"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wretched Banquet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bdaf55b-2de3-4c8a-90ae-9c88c9d00fd7.jpg?1562800483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bdaf55b-2de3-4c8a-90ae-9c88c9d00fd7.jpg?1562800483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "You Are Already Dead", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/768727ce-4f84-4527-8d69-3c9b7877b748.jpg?1654567474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/768727ce-4f84-4527-8d69-3c9b7877b748.jpg?1654567474"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath1.json b/web/public/mtg/jsons/wrath1.json deleted file mode 100644 index 267e34b0..00000000 --- a/web/public/mtg/jsons/wrath1.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Aetherize", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33303859-c6e0-4ebd-bb5f-44be7f5d7459.jpg?1561821990", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33303859-c6e0-4ebd-bb5f-44be7f5d7459.jpg?1561821990"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aetherspouts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46f1b48f-6528-46bd-a384-2358af25e500.jpg?1562786278", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46f1b48f-6528-46bd-a384-2358af25e500.jpg?1562786278"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Aggravate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/999f40a7-b723-42e1-83c1-f45a72a26dd4.jpg?1592709004", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/999f40a7-b723-42e1-83c1-f45a72a26dd4.jpg?1592709004"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Akroma's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e33aaf7-7490-4b64-a966-82fbf7ca8686.jpg?1562917166", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e33aaf7-7490-4b64-a966-82fbf7ca8686.jpg?1562917166"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Akroma's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f112edd-1d2f-45ad-aaeb-6c0934d24c1f.jpg?1570203942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f112edd-1d2f-45ad-aaeb-6c0934d24c1f.jpg?1570203942"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Alpha Brawl", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ec168a-3e4f-4527-901a-bc28cc28d125.jpg?1562949045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ec168a-3e4f-4527-901a-bc28cc28d125.jpg?1562949045"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90795891-5e67-47c0-8d52-a5e5c5a9ef81.jpg?1562821425", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90795891-5e67-47c0-8d52-a5e5c5a9ef81.jpg?1562821425"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/dedcbd3b-7e30-44cf-b9b7-1bb32c11ef67.jpg?1655825935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/dedcbd3b-7e30-44cf-b9b7-1bb32c11ef67.jpg?1655825935"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec898bc9-9ab8-4394-8c4c-8d652f313919.jpg?1607042506", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec898bc9-9ab8-4394-8c4c-8d652f313919.jpg?1607042506"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Anger of the Gods", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88f2ca85-de02-4471-b90f-d13ccb93c8bb.jpg?1597250046", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88f2ca85-de02-4471-b90f-d13ccb93c8bb.jpg?1597250046"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Arcbond", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9bc397d1-50a8-46cd-98b2-7104f2241420.jpg?1562828028", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9bc397d1-50a8-46cd-98b2-7104f2241420.jpg?1562828028"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arms of Hadar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db1fd431-8f6d-4ca5-bc0c-53881c500da1.jpg?1653767219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db1fd431-8f6d-4ca5-bc0c-53881c500da1.jpg?1653767219"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bef16a71-5ed2-4f30-a844-c02a0754f679.jpg?1562853529", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bef16a71-5ed2-4f30-a844-c02a0754f679.jpg?1562853529"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8ee73fe8-d52b-43bb-ab91-5545192be676.jpg?1562357897", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8ee73fe8-d52b-43bb-ab91-5545192be676.jpg?1562357897"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Austere Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbbf55bc-6bb3-458a-8cf0-1f603bb2acb3.jpg?1562939169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbbf55bc-6bb3-458a-8cf0-1f603bb2acb3.jpg?1562939169"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Baki's Curse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3261b4c-7963-4ca0-875d-77b7c8571b3f.jpg?1562588703", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3261b4c-7963-4ca0-875d-77b7c8571b3f.jpg?1562588703"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barrage of Boulders", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2eb1a9f7-32ba-48fd-a7f7-788b0ec052c6.jpg?1562784418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2eb1a9f7-32ba-48fd-a7f7-788b0ec052c6.jpg?1562784418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Begin Anew", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d885aedb-2c65-4099-af2e-0a540caf8d33.jpg?1645417110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d885aedb-2c65-4099-af2e-0a540caf8d33.jpg?1645417110"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Bite of the Black Rose", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/254d1363-1204-41d2-9799-34484a3eb211.jpg?1562864493", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/254d1363-1204-41d2-9799-34484a3eb211.jpg?1562864493"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Biting Rain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5ac62d2f-6834-4d98-b69d-bd7b5831d981.jpg?1576384359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5ac62d2f-6834-4d98-b69d-bd7b5831d981.jpg?1576384359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Black Sun's Zenith", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/03bdcf52-50b8-42c0-9665-931d83f5f314.jpg?1562609329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03bdcf52-50b8-42c0-9665-931d83f5f314.jpg?1562609329"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Black Sun's Zenith", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd88131a-2811-4a1f-bb9a-c82e12c1493b.jpg?1561758222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd88131a-2811-4a1f-bb9a-c82e12c1493b.jpg?1561758222"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Blasphemous Act", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/509ce648-fb76-486d-8b39-183e368b7cb7.jpg?1562830111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/509ce648-fb76-486d-8b39-183e368b7cb7.jpg?1562830111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blazing Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3adc0288-acdf-4a99-9bfb-919cae1aeb69.jpg?1591227065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3adc0288-acdf-4a99-9bfb-919cae1aeb69.jpg?1591227065"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Blazing Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba450179-4591-4e8a-b6ca-66cbef1817f2.jpg?1543675486", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba450179-4591-4e8a-b6ca-66cbef1817f2.jpg?1543675486"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodline Culling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/a/fac827f7-a587-4adf-8408-2d9ccd9c1343.jpg?1634349575", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/a/fac827f7-a587-4adf-8408-2d9ccd9c1343.jpg?1634349575"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blood Money", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d45c18c-b8eb-465c-8dfc-fd6da73e25b5.jpg?1653442044", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d45c18c-b8eb-465c-8dfc-fd6da73e25b5.jpg?1653442044"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Blood on the Snow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8606f40-0af4-443b-a413-a88dc3e8f32e.jpg?1631047655", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8606f40-0af4-443b-a413-a88dc3e8f32e.jpg?1631047655"}, "reprint": false, "frame_effects": ["snow"], "digital": false, "set_type": "expansion"}, {"name": "Boiling Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdaab44c-4ce1-43fb-915c-c687fe8559ce.jpg?1562943558", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdaab44c-4ce1-43fb-915c-c687fe8559ce.jpg?1562943558"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bonfire of the Damned", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e60610fe-891d-46de-b556-d03b637dccec.jpg?1592709031"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Bontu's Last Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b4d0102-c0d6-4d50-941a-dd1c3575a3a8.jpg?1562791273", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b4d0102-c0d6-4d50-941a-dd1c3575a3a8.jpg?1562791273"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Borrowing the East Wind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/6/96ba9014-d750-4924-aa6f-8b9f421807f9.jpg?1562257056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/6/96ba9014-d750-4924-aa6f-8b9f421807f9.jpg?1562257056"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Breaking Point", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/765ec2c9-8ffe-488a-bebe-e5dd63825a8c.jpg?1562630501"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath of Darigaaz", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480bb7e3-df03-454d-ada0-592ef8a4a6f0.jpg?1562909692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Breath Weapon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0174e40a-0ef5-4439-91e6-3fc39f482520.jpg?1653596065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0174e40a-0ef5-4439-91e6-3fc39f482520.jpg?1653596065"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Burn Down the House", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20ded7af-8086-465e-a980-3099217d324c.jpg?1634350460", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20ded7af-8086-465e-a980-3099217d324c.jpg?1634350460"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Burning of Xinye", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33a1fe45-52d2-4c50-bedc-eee156ab69c8.jpg?1562256064", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33a1fe45-52d2-4c50-bedc-eee156ab69c8.jpg?1562256064"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "By Invitation Only", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46764e49-64da-4a94-b61c-75e006b2c5a9.jpg?1643585907", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46764e49-64da-4a94-b61c-75e006b2c5a9.jpg?1643585907"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Canopy Surge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e19d68e-7554-4627-a316-beb1f75fa494.jpg?1562904391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e19d68e-7554-4627-a316-beb1f75fa494.jpg?1562904391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cataclysm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/024ae668-a1ae-4020-89c8-acbd8bd0a691.jpg?1593863070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/024ae668-a1ae-4020-89c8-acbd8bd0a691.jpg?1593863070"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cataclysm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3ed0d87b-1ce8-452b-9558-fa1923407f16.jpg?1559618030", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3ed0d87b-1ce8-452b-9558-fa1923407f16.jpg?1559618030"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Catastrophe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/294d21dc-5c76-4449-936f-9b7541d37c86.jpg?1562903769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/294d21dc-5c76-4449-936f-9b7541d37c86.jpg?1562903769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cave-In", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/440d9d26-f304-467d-af79-914cc65f082e.jpg?1562380418"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Celestial Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5fd29cd7-9950-49c0-9e71-d6b0f944292c.jpg?1637627823", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5fd29cd7-9950-49c0-9e71-d6b0f944292c.jpg?1637627823"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Cerebral Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77161159-ee2c-485d-8674-d8590ccc62e1.jpg?1562819165", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77161159-ee2c-485d-8674-d8590ccc62e1.jpg?1562819165"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chain Reaction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/614b9df9-c959-4bdb-91c0-75ae60b724e4.jpg?1567754665", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/614b9df9-c959-4bdb-91c0-75ae60b724e4.jpg?1567754665"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chandra's Flame Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f13b6a7-fa62-4d94-a56c-f2e64c8c1666.jpg?1592518162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f13b6a7-fa62-4d94-a56c-f2e64c8c1666.jpg?1592518162"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Chandra's Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e761acf6-6618-44cc-8f65-1d7ad7e520fe.jpg?1561758344"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Chandra's Ignition", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d4c90de-49aa-43ed-a18a-f7f96268e5eb.jpg?1562027623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d4c90de-49aa-43ed-a18a-f7f96268e5eb.jpg?1562027623"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cinderclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/5516cf97-805f-4a21-a4c6-2d6e55865336.jpg?1604196918", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/5516cf97-805f-4a21-a4c6-2d6e55865336.jpg?1604196918"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Citywide Bust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a995200f-1e9d-4ff3-9e04-4a4309e0e09c.jpg?1572892490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a995200f-1e9d-4ff3-9e04-4a4309e0e09c.jpg?1572892490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Claws of Wirewood", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b94cd33f-40b6-4b11-97a4-8676ef27631e.jpg?1562533774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b94cd33f-40b6-4b11-97a4-8676ef27631e.jpg?1562533774"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cleanse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fbd611b-ac97-4516-bad7-cc9ee4ef74f7.jpg?1591836785", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fbd611b-ac97-4516-bad7-cc9ee4ef74f7.jpg?1591836785"}, "content_warning": true, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cleansing Nova", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/1/11f1b6cd-d89a-4468-a097-7a54efe22f2c.jpg?1625192921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/1/11f1b6cd-d89a-4468-a097-7a54efe22f2c.jpg?1625192921"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Cleansing Nova", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5be8eed7-c033-42cc-bd21-4512db7af66c.jpg?1562302239", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5be8eed7-c033-42cc-bd21-4512db7af66c.jpg?1562302239"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cloudkill", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7c71b2b8-f5ef-4885-9f8d-284fe335d184.jpg?1654365309", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7c71b2b8-f5ef-4885-9f8d-284fe335d184.jpg?1654365309"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Collision of Realms", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49618217-1bbb-498a-a6f0-f269ce7166a6.jpg?1651655330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49618217-1bbb-498a-a6f0-f269ce7166a6.jpg?1651655330"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Conductive Current", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43adef3c-87f0-4db1-9fbb-017c96c815ff.jpg?1645416694", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43adef3c-87f0-4db1-9fbb-017c96c815ff.jpg?1645416694"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Consume the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c94dcaed-55da-41f4-a61f-2a79ef6c1459.jpg?1593095734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c94dcaed-55da-41f4-a61f-2a79ef6c1459.jpg?1593095734"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Consume the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/921ebea0-48bf-4338-9e84-2cd06ffe6f4b.jpg?1562706383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/921ebea0-48bf-4338-9e84-2cd06ffe6f4b.jpg?1562706383"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corpse Explosion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c700eff3-138b-4d4c-ba36-58b98986168c.jpg?1650029916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c700eff3-138b-4d4c-ba36-58b98986168c.jpg?1650029916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Corrosive Gale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/04a13825-ab9b-4ffd-9b59-6198181891b9.jpg?1562875245", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/04a13825-ab9b-4ffd-9b59-6198181891b9.jpg?1562875245"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cosmotronic Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/9/69c5bafa-8cd8-4158-98e0-46dc74c027c0.jpg?1572893121", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/9/69c5bafa-8cd8-4158-98e0-46dc74c027c0.jpg?1572893121"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cower in Fear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf2d53b8-7847-4b94-9711-eca29facccba.jpg?1562559508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf2d53b8-7847-4b94-9711-eca29facccba.jpg?1562559508"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Crippling Fear", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d9bd181-b99f-477e-bcfb-9b78cbf51224.jpg?1631047737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d9bd181-b99f-477e-bcfb-9b78cbf51224.jpg?1631047737"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crush the Weak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/875a20c2-1d17-46ea-b4d2-3e70bc05aae3.jpg?1631049096", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/875a20c2-1d17-46ea-b4d2-3e70bc05aae3.jpg?1631049096"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crux of Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1d45374-a41b-4b3f-a7c8-3eb5ca767cf6.jpg?1648060698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1d45374-a41b-4b3f-a7c8-3eb5ca767cf6.jpg?1648060698"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crux of Fate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3ccea48-ee90-4da8-832d-8c30c98bf1dd.jpg?1623779891", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3ccea48-ee90-4da8-832d-8c30c98bf1dd.jpg?1623779891"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Cry of the Carnarium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/715a14a3-046e-45ca-b943-dd630e5202b7.jpg?1584830546", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/715a14a3-046e-45ca-b943-dd630e5202b7.jpg?1584830546"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Culling Sun", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ec5a956-c846-46b6-91bd-37e4db542280.jpg?1593272635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ec5a956-c846-46b6-91bd-37e4db542280.jpg?1593272635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dakmor Plague", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58b38ef1-5839-4292-91d6-e45698c69a75.jpg?1562915882", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58b38ef1-5839-4292-91d6-e45698c69a75.jpg?1562915882"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Damn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efeae088-9ac5-4d2f-a15c-d8675a471ac5.jpg?1626095400"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/26c68473-70ca-40ba-b5c6-71ec30f88a2c.jpg?1562568132", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/26c68473-70ca-40ba-b5c6-71ec30f88a2c.jpg?1562568132"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dca972d7-fcf8-4ac4-a98b-fffb2fbb4dbc.jpg?1656326586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dca972d7-fcf8-4ac4-a98b-fffb2fbb4dbc.jpg?1656326586"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7fc1d7db-11a3-4ff9-8d27-1fe401053080.jpg?1615223046", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7fc1d7db-11a3-4ff9-8d27-1fe401053080.jpg?1615223046"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c916a119-9eee-440d-90ef-05ab35bf3fbe.jpg?1562935376", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c916a119-9eee-440d-90ef-05ab35bf3fbe.jpg?1562935376"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Damnation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c5823bb-d56d-4bed-ba3f-09bdd93c52dc.jpg?1561757368", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c5823bb-d56d-4bed-ba3f-09bdd93c52dc.jpg?1561757368"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Damning Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5be40c34-6df0-4471-b99b-850ae2be9923.jpg?1650406359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5be40c34-6df0-4471-b99b-850ae2be9923.jpg?1650406359"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2aa98fca-972b-46c2-bdec-6ace35c988d5.jpg?1562610835", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2aa98fca-972b-46c2-bdec-6ace35c988d5.jpg?1562610835"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/b/5bf85d00-52cc-4594-b4fd-5ec424210524.jpg?1623592427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/b/5bf85d00-52cc-4594-b4fd-5ec424210524.jpg?1623592427"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/aea87800-6725-4399-b489-651637e1804a.jpg?1561757821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/aea87800-6725-4399-b489-651637e1804a.jpg?1561757821"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Day of Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6ba873f7-a7a4-44aa-84a6-44501424dc7a.jpg?1561757360", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6ba873f7-a7a4-44aa-84a6-44501424dc7a.jpg?1561757360"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Deadly Tempest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9ca2810-3c1b-43cf-af1e-078015bf3492.jpg?1562708889", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9ca2810-3c1b-43cf-af1e-078015bf3492.jpg?1562708889"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Dead of Winter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f480df6d-e227-4ccb-ad6d-a4ad48a360ad.jpg?1562201599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f480df6d-e227-4ccb-ad6d-a4ad48a360ad.jpg?1562201599"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Deafening Clarion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e115a81-001d-4e17-98af-6a63f2b0967f.jpg?1572893584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e115a81-001d-4e17-98af-6a63f2b0967f.jpg?1572893584"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Cloud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97a0bfb9-859b-4fed-a1c4-1f0924715801.jpg?1562638297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97a0bfb9-859b-4fed-a1c4-1f0924715801.jpg?1562638297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Death Frenzy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92096311-a3fa-41fc-b7a9-71ac2310f7fe.jpg?1562790443", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92096311-a3fa-41fc-b7a9-71ac2310f7fe.jpg?1562790443"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Annihilation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73744717-518c-478e-9da9-201c49124f37.jpg?1562530626", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73744717-518c-478e-9da9-201c49124f37.jpg?1562530626"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Pain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1958a07-fc75-41cd-ac45-d92d49587754.jpg?1562536145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1958a07-fc75-41cd-ac45-d92d49587754.jpg?1562536145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Decree of Pain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/03c37c68-cccf-4309-80c5-828108b942a4.jpg?1569957295", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03c37c68-cccf-4309-80c5-828108b942a4.jpg?1569957295"}, "reprint": true, "digital": false, "set_type": "arsenal"}, {"name": "Delayed Blast Fireball", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e59903e3-a344-4218-9d41-8b19a9bc8311.jpg?1654082475", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e59903e3-a344-4218-9d41-8b19a9bc8311.jpg?1654082475"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Depopulate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c53c1898-9107-4bf8-b249-d0502fb9596d.jpg?1649698259", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c53c1898-9107-4bf8-b249-d0502fb9596d.jpg?1649698259"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Descend upon the Sinful", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c9ff2cbf-a1dc-4cc5-9a5d-8439899d4e87.jpg?1576383726", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c9ff2cbf-a1dc-4cc5-9a5d-8439899d4e87.jpg?1576383726"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desert Sandstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/588ad2bf-405d-4c36-b485-e415c22f2703.jpg?1562256542", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/588ad2bf-405d-4c36-b485-e415c22f2703.jpg?1562256542"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Destructive Force", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1abde258-08e0-4762-8142-38e08a960f9d.jpg?1562452402", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1abde258-08e0-4762-8142-38e08a960f9d.jpg?1562452402"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Devastate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfe7c990-a34b-475e-a612-447c22f998d3.jpg?1562930849"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devastating Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9fffeed0-a5ea-47ac-a7a4-0cc3bb1d408a.jpg?1562631212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9fffeed0-a5ea-47ac-a7a4-0cc3bb1d408a.jpg?1562631212"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Devastation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/1/71cce019-162c-4969-89ac-1cf94148a032.jpg?1562446865", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cce019-162c-4969-89ac-1cf94148a032.jpg?1562446865"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Disaster Radius", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9318ae4a-1084-49d9-b5de-dbe4d80836cb.jpg?1562706406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9318ae4a-1084-49d9-b5de-dbe4d80836cb.jpg?1562706406"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Disorder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6d11422-60a9-4386-8e7f-dd7dcdac58d8.jpg?1562246308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6d11422-60a9-4386-8e7f-dd7dcdac58d8.jpg?1562246308"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Disorder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3fa5ec10-dfea-4e6d-8996-553a4a0eb8a4.jpg?1562908220", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3fa5ec10-dfea-4e6d-8996-553a4a0eb8a4.jpg?1562908220"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Divine Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/446ea3a4-206a-4097-87c1-c04bb7812972.jpg?1562829296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/446ea3a4-206a-4097-87c1-c04bb7812972.jpg?1562829296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Doomskar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/130ee895-1e5e-4f82-bb66-e1275bac75dd.jpg?1631045641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/130ee895-1e5e-4f82-bb66-e1275bac75dd.jpg?1631045641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Do or Die", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05f63cd9-e82b-4cf8-b8ce-f0aa0157692b.jpg?1562896148", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05f63cd9-e82b-4cf8-b8ce-f0aa0157692b.jpg?1562896148"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Draconic Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/657de246-b9fc-47b1-b932-091e9500bb82.jpg?1624591671", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/657de246-b9fc-47b1-b932-091e9500bb82.jpg?1624591671"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in Sorrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/287c7570-8080-43dc-a586-963e15566446.jpg?1593091908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/287c7570-8080-43dc-a586-963e15566446.jpg?1593091908"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drown in Sorrow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/107cdfa4-da15-4610-9b72-e6e6c59deec4.jpg?1630641355", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/107cdfa4-da15-4610-9b72-e6e6c59deec4.jpg?1630641355"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a142f369-8fdd-4dc8-b5d9-3493455cc588.jpg?1562447357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a142f369-8fdd-4dc8-b5d9-3493455cc588.jpg?1562447357"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/547c10ea-8ace-4496-8b99-61863c0cec1b.jpg?1562587287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/547c10ea-8ace-4496-8b99-61863c0cec1b.jpg?1562587287"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dry Spell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/997ea663-40a1-49b7-80f1-2e1febc1b6fa.jpg?1562587769", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/997ea663-40a1-49b7-80f1-2e1febc1b6fa.jpg?1562587769"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Duneblast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e3fba5b-b4cd-4050-b9f0-d8eabe82e7d6.jpg?1562701635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e3fba5b-b4cd-4050-b9f0-d8eabe82e7d6.jpg?1562701635"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Dwarven Catapult", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/c/8c1c6932-638a-4df7-bf9b-8d921f7484d9.jpg?1562921034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/c/8c1c6932-638a-4df7-bf9b-8d921f7484d9.jpg?1562921034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f04dc5c-2764-42d0-974e-6d902222c138.jpg?1562242701"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05126438-e806-43e6-bd81-233b629b4a1b.jpg?1562896224"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/272f65a3-3c0c-417d-b5b6-276a643d643e.jpg?1562446144"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01bde909-899d-4efc-aac5-57b69fa764db.jpg?1562588740"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e68ac362-6cdc-48a6-bdd3-4f8ea32add64.jpg?1559591701"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electrickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ed81ee8-d5e4-4127-876e-9bff81f9c726.jpg?1562787062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ed81ee8-d5e4-4127-876e-9bff81f9c726.jpg?1562787062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Endemic Plague", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/15326971-a53b-45f2-8f1d-1b82935286e1.jpg?1562900082", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/15326971-a53b-45f2-8f1d-1b82935286e1.jpg?1562900082"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "End Hostilities", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/80a53ed7-a7b7-40d8-9239-cf6f205dbc59.jpg?1562789330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/80a53ed7-a7b7-40d8-9239-cf6f205dbc59.jpg?1562789330"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "End the Festivities", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bec748e6-7245-4a71-aeee-cefed8346948.jpg?1643591154", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bec748e6-7245-4a71-aeee-cefed8346948.jpg?1643591154"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Engulf the Shore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/22909767-a088-49ff-83be-37f967d1da3d.jpg?1576384043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/22909767-a088-49ff-83be-37f967d1da3d.jpg?1576384043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Pulse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3e32d1b-e580-4d09-b285-c8d6c5297896.jpg?1625191655", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3e32d1b-e580-4d09-b285-c8d6c5297896.jpg?1625191655"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a978fa0a-a52b-4464-afe3-d9f7bc202e63.jpg?1562553159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a978fa0a-a52b-4464-afe3-d9f7bc202e63.jpg?1562553159"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e1144eb-701d-4716-9051-e8b77480e72d.jpg?1595438077", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e1144eb-701d-4716-9051-e8b77480e72d.jpg?1595438077"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Evacuation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1cb8ae53-a53f-4a0f-94f7-559aca041797.jpg?1562595927", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1cb8ae53-a53f-4a0f-94f7-559aca041797.jpg?1562595927"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Evaporate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3c99939-4854-4e28-a142-4cb7f89fe898.jpg?1562587778", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3c99939-4854-4e28-a142-4cb7f89fe898.jpg?1562587778"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Evincar's Justice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d53f46f-b069-4b34-af4b-98143328c078.jpg?1562054236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d53f46f-b069-4b34-af4b-98143328c078.jpg?1562054236"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a233a244-7f84-4525-b0ce-e10db0a95385.jpg?1562055894", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a233a244-7f84-4525-b0ce-e10db0a95385.jpg?1562055894"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinction Event", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/8725a869-462b-4381-880a-b4bcc63a655b.jpg?1591226783", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/8725a869-462b-4381-880a-b4bcc63a655b.jpg?1591226783"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Extinguish All Hope", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/6895024f-a04b-46cf-b020-df4487d0c758.jpg?1593095692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/6895024f-a04b-46cf-b020-df4487d0c758.jpg?1593095692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Eyeblight Massacre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d73484db-5fd0-4a01-83fd-54748cc21a0f.jpg?1562044208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d73484db-5fd0-4a01-83fd-54748cc21a0f.jpg?1562044208"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ezuri's Predation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d9b4ad1-3d5c-43b6-9284-9ec427936dd2.jpg?1562704058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d9b4ad1-3d5c-43b6-9284-9ec427936dd2.jpg?1562704058"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Falling Star", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2b9983e-20d4-4d12-9e2c-ec6d9a345787.jpg?1562861838", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2b9983e-20d4-4d12-9e2c-ec6d9a345787.jpg?1562861838"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a56410a7-6f99-4bdf-9385-f23571c263c3.jpg?1562929852", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a56410a7-6f99-4bdf-9385-f23571c263c3.jpg?1562929852"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Famine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d6c10ca-f6d6-4322-aa17-7e874cb10bb1.jpg?1562257044", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d6c10ca-f6d6-4322-aa17-7e874cb10bb1.jpg?1562257044"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Farewell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1068723-d1ef-4007-97d9-b10dccdbade4.jpg?1654566260", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1068723-d1ef-4007-97d9-b10dccdbade4.jpg?1654566260"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Farewell", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98c664bc-9585-47a7-9514-b3e30a4e1b59.jpg?1654569820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98c664bc-9585-47a7-9514-b3e30a4e1b59.jpg?1654569820"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Fated Retribution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8158b330-2868-4147-907e-4d86e44cfaad.jpg?1593091437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8158b330-2868-4147-907e-4d86e44cfaad.jpg?1593091437"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fault Line", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/cab4fd0e-9f84-4628-92a7-858ad8064531.jpg?1562937807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feast of Succession", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac83f97d-c8c9-480c-a32c-918035673ab4.jpg?1608909745", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac83f97d-c8c9-480c-a32c-918035673ab4.jpg?1608909745"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Fell the Mighty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4e999d3-c2d7-47dc-81ad-a2baf6cc4757.jpg?1561960243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4e999d3-c2d7-47dc-81ad-a2baf6cc4757.jpg?1561960243"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Festergloom", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3125137-bd18-488e-b45e-6fc23828c5bd.jpg?1562796922", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3125137-bd18-488e-b45e-6fc23828c5bd.jpg?1562796922"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Festering March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c34e6aa-0414-45ba-b6eb-1ac4255d7de8.jpg?1562903995", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c34e6aa-0414-45ba-b6eb-1ac4255d7de8.jpg?1562903995"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Cannonade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/664d21c9-4b6c-4797-845f-7bca79c2b76b.jpg?1562556766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/664d21c9-4b6c-4797-845f-7bca79c2b76b.jpg?1562556766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b61c9bc-16e8-417f-99e7-8bd83d4666c5.jpg?1562706203"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Fiery Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c454a20-8ec8-41d9-b9c3-acaa510d050b.jpg?1593559583"}, "reprint": true, "digital": false, "set_type": "spellbook"}, {"name": "Fight to the Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/5552ca9b-0245-4f91-9646-a5b5443863a2.jpg?1562641354", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/5552ca9b-0245-4f91-9646-a5b5443863a2.jpg?1562641354"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Judgment", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2503e136-031f-498a-b042-4077baebe8f8.jpg?1562876056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2503e136-031f-498a-b042-4077baebe8f8.jpg?1562876056"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Final Revels", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99f3744a-71c4-4a54-9e1c-92420526b792.jpg?1562359766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99f3744a-71c4-4a54-9e1c-92420526b792.jpg?1562359766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Firespout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13454f69-1458-4c03-ab02-bd697a32eb17.jpg?1562826991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13454f69-1458-4c03-ab02-bd697a32eb17.jpg?1562826991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Firespout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fecd098-bbf2-44f1-b9f1-7b93ea660880.jpg?1559966438", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fecd098-bbf2-44f1-b9f1-7b93ea660880.jpg?1559966438"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Fire Tempest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/92334ebe-3d7a-46de-8b91-931e5d56a5a5.jpg?1562447336"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Flamebreak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e1f06f-7c87-4da8-b339-e571e391cab1.jpg?1562637920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flames of the Raze-Boar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/6/16957271-12bb-4031-b476-f7678b753ae3.jpg?1584830878", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/6/16957271-12bb-4031-b476-f7678b753ae3.jpg?1584830878"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flame Sweep", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8e489d6c-2eb2-4914-ae71-c9da55b51d0b.jpg?1586187261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8e489d6c-2eb2-4914-ae71-c9da55b51d0b.jpg?1586187261"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Flame Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e069d90a-e7d9-4967-a872-0dd8a0a9934a.jpg?1562597824"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flaying Tendrils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77899cb2-4d87-4c2d-99ae-1ae75bc5dc86.jpg?1562918962", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77899cb2-4d87-4c2d-99ae-1ae75bc5dc86.jpg?1562918962"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Flaying Tendrils", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10bf8dbf-ae2e-41cd-904c-84b9cca14c27.jpg?1575936034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10bf8dbf-ae2e-41cd-904c-84b9cca14c27.jpg?1575936034"}, "reprint": true, "frame_effects": ["devoid"], "digital": false, "set_type": "promo"}, {"name": "Flowstone Slide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec7b02e1-0a20-4247-ae2a-056c5356f168.jpg?1562632691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec7b02e1-0a20-4247-ae2a-056c5356f168.jpg?1562632691"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Forced March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36eae0e1-7100-449d-a259-7abfcd429117.jpg?1562379925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36eae0e1-7100-449d-a259-7abfcd429117.jpg?1562379925"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fumigate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f00f27a7-9e92-4fbf-baa8-f47a5eee48a6.jpg?1576380863", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f00f27a7-9e92-4fbf-baa8-f47a5eee48a6.jpg?1576380863"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gale Force", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/26c5c233-a373-4ac4-9b99-81ed97df1f9b.jpg?1562758454", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/26c5c233-a373-4ac4-9b99-81ed97df1f9b.jpg?1562758454"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gates Ablaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b574b44-01e1-4197-99bd-57e54aebc5ff.jpg?1584830891", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b574b44-01e1-4197-99bd-57e54aebc5ff.jpg?1584830891"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Golden Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88bb420a-8bf1-4504-b1b5-2d929be978be.jpg?1555040232", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88bb420a-8bf1-4504-b1b5-2d929be978be.jpg?1555040232"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Golgari Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48fce388-eefc-4234-8dd9-1260c1ba97eb.jpg?1562785737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48fce388-eefc-4234-8dd9-1260c1ba97eb.jpg?1562785737"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/9235afe5-0a6b-43c2-921c-18524cf032f1.jpg?1561836885", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/9235afe5-0a6b-43c2-921c-18524cf032f1.jpg?1561836885"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guan Yu's 1,000-Li March", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8fa7526a-7a4e-4b3d-b96e-91f2bbf1c7bd.jpg?1562257048", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8fa7526a-7a4e-4b3d-b96e-91f2bbf1c7bd.jpg?1562257048"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Hail Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7e9d786-4e9b-447b-a5dc-ca117c4961c5.jpg?1562769694", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7e9d786-4e9b-447b-a5dc-ca117c4961c5.jpg?1562769694"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hallowed Burial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c42fad4b-caeb-4aa2-9586-cb26bdec56cd.jpg?1562936481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c42fad4b-caeb-4aa2-9586-cb26bdec56cd.jpg?1562936481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Harsh Mercy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6473b4d-1f59-4216-ace9-f3e5306266fb.jpg?1562937932", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6473b4d-1f59-4216-ace9-f3e5306266fb.jpg?1562937932"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hazardous Conditions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daa9b08b-c56f-480e-874e-069e72d979c8.jpg?1576382835", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daa9b08b-c56f-480e-874e-069e72d979c8.jpg?1576382835"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hellfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/362f1fe9-20af-434c-9957-7a1a564d89e6.jpg?1592364391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/362f1fe9-20af-434c-9957-7a1a564d89e6.jpg?1592364391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hellion Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/6529c92e-c79b-4953-8bd0-50ceae2ce261.jpg?1562704497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/6529c92e-c79b-4953-8bd0-50ceae2ce261.jpg?1562704497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hideous Laughter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/941fd135-1c5a-4650-8faf-dfa2c93ec8c9.jpg?1562762525", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/941fd135-1c5a-4650-8faf-dfa2c93ec8c9.jpg?1562762525"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath2.json b/web/public/mtg/jsons/wrath2.json deleted file mode 100644 index ea785118..00000000 --- a/web/public/mtg/jsons/wrath2.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": true, "data": [{"name": "Holy Light", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3c8a850-bc99-4679-a316-45ecdea696b2.jpg?1592364686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3c8a850-bc99-4679-a316-45ecdea696b2.jpg?1592364686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hostile Takeover", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd7df727-50ea-4ea8-bdb9-d7ef16199d8a.jpg?1649697248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd7df727-50ea-4ea8-bdb9-d7ef16199d8a.jpg?1649697248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hostile Takeover", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8137f134-0148-4df1-b575-ec861192c65c.jpg?1649695787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8137f134-0148-4df1-b575-ec861192c65c.jpg?1649695787"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Hour of Devastation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d420cc12-cfd7-4007-a0c2-b16c8f63a754.jpg?1562816057", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d420cc12-cfd7-4007-a0c2-b16c8f63a754.jpg?1562816057"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hour of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d12768a5-8ee6-407b-87cf-703e69a0c32a.jpg?1568003844", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d12768a5-8ee6-407b-87cf-703e69a0c32a.jpg?1568003844"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Hour of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/bec7a987-1ef2-40aa-a744-92d90b246df4.jpg?1598913735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/bec7a987-1ef2-40aa-a744-92d90b246df4.jpg?1598913735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Howling Gale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9917cf32-0236-4463-9b1d-e8193754ff97.jpg?1562923428", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9917cf32-0236-4463-9b1d-e8193754ff97.jpg?1562923428"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Hurly-Burly", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a6e0b97-c2a9-4cd6-957e-87e9b22f7b48.jpg?1562354283", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a6e0b97-c2a9-4cd6-957e-87e9b22f7b48.jpg?1562354283"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0526077-79b6-40ae-8178-8b97c33a53fb.jpg?1562250875", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0526077-79b6-40ae-8178-8b97c33a53fb.jpg?1562250875"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b4dd722-4729-444a-9d81-e2e93317fbd5.jpg?1562920277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b4dd722-4729-444a-9d81-e2e93317fbd5.jpg?1562920277"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7b97904e-80ba-4d65-808a-a528200430f8.jpg?1562446872", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7b97904e-80ba-4d65-808a-a528200430f8.jpg?1562446872"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8cc6db7-1f40-40e3-a7ea-92f1d05e2e3d.jpg?1562926538", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8cc6db7-1f40-40e3-a7ea-92f1d05e2e3d.jpg?1562926538"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Hurricane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52f5a19f-16e4-4d35-89e1-969ac8202f88.jpg?1559591426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52f5a19f-16e4-4d35-89e1-969ac8202f88.jpg?1559591426"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ichor Explosion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b207e2f-4604-43c5-bb35-a877e35ddd81.jpg?1562875473", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b207e2f-4604-43c5-bb35-a877e35ddd81.jpg?1562875473"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Immolating Gyre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd0b8aee-fbfb-470f-9ac2-64fce0b4b2fb.jpg?1632261825", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd0b8aee-fbfb-470f-9ac2-64fce0b4b2fb.jpg?1632261825"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Incandescent Aria", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77e2ed9e-ee1d-440a-94b4-d4b17d30b800.jpg?1649801687", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77e2ed9e-ee1d-440a-94b4-d4b17d30b800.jpg?1649801687"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incandescent Aria", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63167d77-a8d5-468f-9132-a5000c57901a.jpg?1649801714", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63167d77-a8d5-468f-9132-a5000c57901a.jpg?1649801714"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Incendiary Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/512367a2-f8f6-4c28-9eb3-8e04d2694e4b.jpg?1562348065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incendiary Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0ee44ca0-1989-42fa-8024-b6b3e5c3883c.jpg?1576382098", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0ee44ca0-1989-42fa-8024-b6b3e5c3883c.jpg?1576382098"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Incite Rebellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/628c4a6f-6970-407d-a774-e67bfcdf7ee2.jpg?1561944371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/628c4a6f-6970-407d-a774-e67bfcdf7ee2.jpg?1561944371"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e411b7b5-ab91-410a-af6d-b3a21a8e3b70.jpg?1562249896"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68d04a75-647f-400f-b0dc-c4544f7db2d4.jpg?1562591355"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6b61512-5b24-424c-966f-36b595781e14.jpg?1562934483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Inferno", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3ac1649a-629b-4598-be09-74a57905753f.jpg?1562544107"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb9dd080-5e13-4334-8614-8eec41ae89c2.jpg?1562711058", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb9dd080-5e13-4334-8614-8eec41ae89c2.jpg?1562711058"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7890ba2-aa42-4c8d-bbc1-94fb1d4150fc.jpg?1562938305", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7890ba2-aa42-4c8d-bbc1-94fb1d4150fc.jpg?1562938305"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infest", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9350a640-3f22-478f-b463-6b50cfe766e1.jpg?1561757603", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9350a640-3f22-478f-b463-6b50cfe766e1.jpg?1561757603"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Inflame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/1/e1efad9a-2fcf-4045-8105-bf9f5e79d12c.jpg?1562640129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/1/e1efad9a-2fcf-4045-8105-bf9f5e79d12c.jpg?1562640129"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Inflame", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd7bc4c0-9bfd-444b-b22c-f1b7e1426807.jpg?1562933469", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd7bc4c0-9bfd-444b-b22c-f1b7e1426807.jpg?1562933469"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "In Garruk's Wake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6f2c2f6-d07f-42af-9944-70d3dac8348c.jpg?1562797158", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6f2c2f6-d07f-42af-9944-70d3dac8348c.jpg?1562797158"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "In Garruk's Wake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/987ace55-8f39-4d5e-8604-9e99d065b4d5.jpg?1561757636", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/987ace55-8f39-4d5e-8604-9e99d065b4d5.jpg?1561757636"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Inundate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5047c92-2885-4a7b-b51f-f3e093dca5ad.jpg?1562940048", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5047c92-2885-4a7b-b51f-f3e093dca5ad.jpg?1562940048"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jokulhaups", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d81e479-45b7-4237-a0eb-95245582e87d.jpg?1562591373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d81e479-45b7-4237-a0eb-95245582e87d.jpg?1562591373"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Jokulhaups", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bf0d325-5928-4593-8faa-64ffa414cb48.jpg?1562906050", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bf0d325-5928-4593-8faa-64ffa414cb48.jpg?1562906050"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jund Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a0ddf00-926c-4283-a8b2-daa02fa99b8b.jpg?1562705657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a0ddf00-926c-4283-a8b2-daa02fa99b8b.jpg?1562705657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaervek's Hex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/097910fb-7c48-4535-8ffc-b521d08294b0.jpg?1562717830", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/097910fb-7c48-4535-8ffc-b521d08294b0.jpg?1562717830"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kaya's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5ed140c1-752b-4539-88f2-1fa354049b17.jpg?1584831638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5ed140c1-752b-4539-88f2-1fa354049b17.jpg?1584831638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Killing Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33de2371-175e-4f8a-9636-35f996e3cf24.jpg?1592708920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33de2371-175e-4f8a-9636-35f996e3cf24.jpg?1592708920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Killing Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e40ed6b1-7b92-4ba4-b197-07c3f171a935.jpg?1561758322", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e40ed6b1-7b92-4ba4-b197-07c3f171a935.jpg?1561758322"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Kindle the Carnage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b5dfa91-8f93-41b7-95e9-3374550f1617.jpg?1593273180", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b5dfa91-8f93-41b7-95e9-3374550f1617.jpg?1593273180"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kindred Dominance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9794115a-5509-4d9a-b119-d2b61942e87b.jpg?1562617149", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9794115a-5509-4d9a-b119-d2b61942e87b.jpg?1562617149"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Kirtar's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5a0c4e6-d50e-42e8-b062-8f6ef5950ab7.jpg?1562928851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5a0c4e6-d50e-42e8-b062-8f6ef5950ab7.jpg?1562928851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Klauth's Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/761e1f77-5231-4008-829f-99650b429fb3.jpg?1631585593", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/761e1f77-5231-4008-829f-99650b429fb3.jpg?1631585593"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Kozilek's Return", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72765559-0a78-4aa3-827e-cb4612720991.jpg?1618608556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72765559-0a78-4aa3-827e-cb4612720991.jpg?1618608556"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Languish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3593efa-0a05-4061-9f6e-edd0a5ca9a1f.jpg?1562043520", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3593efa-0a05-4061-9f6e-edd0a5ca9a1f.jpg?1562043520"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Last One Standing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/87e2ee71-293d-452b-89a5-b15990186f5b.jpg?1562922467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/87e2ee71-293d-452b-89a5-b15990186f5b.jpg?1562922467"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lavaball Trap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0d411e1-5488-4818-95a4-9f637efb9be6.jpg?1562616217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0d411e1-5488-4818-95a4-9f637efb9be6.jpg?1562616217"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lavalanche", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/749981d6-78e7-4f53-80a8-f211e61bd532.jpg?1562642149"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Life's Finale", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffd3fbd2-87c7-4f08-baaa-91d61c1114da.jpg?1562883140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffd3fbd2-87c7-4f08-baaa-91d61c1114da.jpg?1562883140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Living Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/e/0e73682a-56a2-4796-9902-a03aaa3815e8.jpg?1562897968", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/e/0e73682a-56a2-4796-9902-a03aaa3815e8.jpg?1562897968"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Living Death", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c820476-fbda-4073-baf6-51e71f45ed58.jpg?1562054465", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c820476-fbda-4073-baf6-51e71f45ed58.jpg?1562054465"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Living End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3be0ff69-d9f3-4b81-b02f-1360e4064aff.jpg?1562907448", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3be0ff69-d9f3-4b81-b02f-1360e4064aff.jpg?1562907448"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Magmaquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac85679e-17c7-4525-8eed-979d04feb8f1.jpg?1562558550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac85679e-17c7-4525-8eed-979d04feb8f1.jpg?1562558550"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Magmaquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1476d42e-6cf8-4612-ae75-b3044d1eebbe.jpg?1605361705", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1476d42e-6cf8-4612-ae75-b3044d1eebbe.jpg?1605361705"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Make Obsolete", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0a96feb-accc-4c30-8ecd-7d9272ebd45b.jpg?1576381736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0a96feb-accc-4c30-8ecd-7d9272ebd45b.jpg?1576381736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Malicious Malfunction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56e7415f-f014-4ece-81db-d8271444d9e9.jpg?1654567289", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56e7415f-f014-4ece-81db-d8271444d9e9.jpg?1654567289"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "March of Souls", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f07dd0f1-b80b-4af0-ae76-907ec55ec7d5.jpg?1562945732", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f07dd0f1-b80b-4af0-ae76-907ec55ec7d5.jpg?1562945732"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Marsh Casualties", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28476d0d-60ea-4d08-890c-0e6502ee3d2a.jpg?1562610765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28476d0d-60ea-4d08-890c-0e6502ee3d2a.jpg?1562610765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Martial Coup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4201385f-6f74-4e3d-aafb-0eff82cb24c1.jpg?1562800634", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4201385f-6f74-4e3d-aafb-0eff82cb24c1.jpg?1562800634"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Martyr's Cry", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2c9f463-d1cc-4f11-aad2-d4a4520aa978.jpg?1562949002", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2c9f463-d1cc-4f11-aad2-d4a4520aa978.jpg?1562949002"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Massacre", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f05f5d93-50d1-4aa6-af05-383a6808345b.jpg?1562632742", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f05f5d93-50d1-4aa6-af05-383a6808345b.jpg?1562632742"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mass Calcify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d24be94-9922-43bb-83c8-98090adc3f32.jpg?1562829041", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d24be94-9922-43bb-83c8-98090adc3f32.jpg?1562829041"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mephitic Vapors", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/675640ba-37e7-4231-8524-87e8b87ea46f.jpg?1572892991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/675640ba-37e7-4231-8524-87e8b87ea46f.jpg?1572892991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Merciless Eviction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9876a4c-714b-47e5-9589-148a623af96a.jpg?1561848654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9876a4c-714b-47e5-9589-148a623af96a.jpg?1561848654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mizzium Mortars", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/544b2931-0af1-4743-b7c1-91e1dc9294d5.jpg?1654120304", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/544b2931-0af1-4743-b7c1-91e1dc9294d5.jpg?1654120304"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Mizzium Mortars", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/4/d4ded88d-2688-4f5e-a8b2-16216cf9c792.jpg?1562793745", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/4/d4ded88d-2688-4f5e-a8b2-16216cf9c792.jpg?1562793745"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mogg Infestation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a91aa6f-cb2f-4aad-9415-bba4eb9b76ca.jpg?1562596412", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a91aa6f-cb2f-4aad-9415-bba4eb9b76ca.jpg?1562596412"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molten Disaster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31e0713c-dbf4-4403-ae69-58fd483e2481.jpg?1562905110"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c48bc86b-df0a-4a9c-8aad-c3ffb742a5ff.jpg?1588005547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c48bc86b-df0a-4a9c-8aad-c3ffb742a5ff.jpg?1588005547"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/9/d9cbdabf-18e3-4c0c-b37b-097aaa650066.jpg?1562090221", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9cbdabf-18e3-4c0c-b37b-097aaa650066.jpg?1562090221"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Mutilate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/6189cab3-1963-4590-9cbc-7ab4a693d7c6.jpg?1562629994", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/6189cab3-1963-4590-9cbc-7ab4a693d7c6.jpg?1562629994"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nature's Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/5950f52a-493e-432e-9175-0272c0edb232.jpg?1562446647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/5950f52a-493e-432e-9175-0272c0edb232.jpg?1562446647"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2569173f-df5e-4518-9fb3-f972210595df.jpg?1580014299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2569173f-df5e-4518-9fb3-f972210595df.jpg?1580014299"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b71315e3-14c1-433b-97be-2cdf99213bba.jpg?1562246310", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b71315e3-14c1-433b-97be-2cdf99213bba.jpg?1562246310"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Nausea", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a10531d8-fc99-4a2b-94b0-97a25521d725.jpg?1562088332", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a10531d8-fc99-4a2b-94b0-97a25521d725.jpg?1562088332"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Necromantic Selection", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/9462680e-b83d-44cc-a7a6-505fbc69ab41.jpg?1561950631", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/9462680e-b83d-44cc-a7a6-505fbc69ab41.jpg?1561950631"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Needle Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/be80dd2d-f595-4d80-84ae-66d3d18e7399.jpg?1562056388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/be80dd2d-f595-4d80-84ae-66d3d18e7399.jpg?1562056388"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Needle Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29a44e44-94b1-4bd2-8e00-6bd2ec07ee4c.jpg?1562446151", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29a44e44-94b1-4bd2-8e00-6bd2ec07ee4c.jpg?1562446151"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Nightmare Unmaking", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95c0ff1b-bd97-4115-8486-62a18bab2610.jpg?1568003505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95c0ff1b-bd97-4115-8486-62a18bab2610.jpg?1568003505"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Nylea's Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/daa2f963-9d16-4224-b24e-b6a79f2b9d75.jpg?1581480794", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/daa2f963-9d16-4224-b24e-b6a79f2b9d75.jpg?1581480794"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Obliterate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cdabde40-2143-4677-b7b4-ea8fbf9b1f25.jpg?1562936357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cdabde40-2143-4677-b7b4-ea8fbf9b1f25.jpg?1562936357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oddly Uneven", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/830d5f87-1c8b-414a-a91e-4805f5bdca54.jpg?1562922623", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/830d5f87-1c8b-414a-a91e-4805f5bdca54.jpg?1562922623"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Olivia's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98893cc1-f502-4ca6-b6c1-e09fa1f4ef7a.jpg?1641600703", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98893cc1-f502-4ca6-b6c1-e09fa1f4ef7a.jpg?1641600703"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Organic Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fea0f8be-c242-49dd-bae3-0b306107ac0b.jpg?1651655197", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fea0f8be-c242-49dd-bae3-0b306107ac0b.jpg?1651655197"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Outbreak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f43c30d9-23a5-4872-925d-3427f5f57995.jpg?1562940897", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f43c30d9-23a5-4872-925d-3427f5f57995.jpg?1562940897"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oversimplify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56eae179-f850-4661-b3f0-4d10be77ed8a.jpg?1629806259", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56eae179-f850-4661-b3f0-4d10be77ed8a.jpg?1629806259"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Overwhelming Forces", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c56c7fb4-8b7b-40fc-879c-76cfb5d417b8.jpg?1562257531", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c56c7fb4-8b7b-40fc-879c-76cfb5d417b8.jpg?1562257531"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Part the Veil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d870e607-1607-46f3-bc9f-925d0164bcf9.jpg?1562764693", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d870e607-1607-46f3-bc9f-925d0164bcf9.jpg?1562764693"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Path of Peril", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0c5449a-d63b-4b22-9432-8f0365c3c4d9.jpg?1643590080", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0c5449a-d63b-4b22-9432-8f0365c3c4d9.jpg?1643590080"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Perish", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/4/e47ace1d-73de-44aa-a3fe-2e2a21ebec79.jpg?1562057337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/4/e47ace1d-73de-44aa-a3fe-2e2a21ebec79.jpg?1562057337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Perplexing Test", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/31f2cbcc-d5b8-4659-ae51-e567c555a743.jpg?1625191389", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/31f2cbcc-d5b8-4659-ae51-e567c555a743.jpg?1625191389"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Pestilent Haze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08b78aa8-a63a-4aa2-bb82-3fbf2595ed7c.jpg?1594736339", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08b78aa8-a63a-4aa2-bb82-3fbf2595ed7c.jpg?1594736339"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Phyrexian Rebirth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/36b7536d-6b0b-4906-ba88-7fcfe9b854ee.jpg?1562610586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/36b7536d-6b0b-4906-ba88-7fcfe9b854ee.jpg?1562610586"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Wind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0d4bd20-7422-45ed-aa76-3ef055c556e7.jpg?1562927896", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0d4bd20-7422-45ed-aa76-3ef055c556e7.jpg?1562927896"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Planar Despair", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/a/3a92d454-3f23-45bf-921f-25b0da4ce138.jpg?1562908776", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/a/3a92d454-3f23-45bf-921f-25b0da4ce138.jpg?1562908776"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Planar Outburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f34f930-a7c6-400d-b6e8-b9908e0f0404.jpg?1562917450", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f34f930-a7c6-400d-b6e8-b9908e0f0404.jpg?1562917450"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Promise of Loyalty", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc21e7d5-3641-47fe-add0-8becf5173e28.jpg?1625191123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc21e7d5-3641-47fe-add0-8becf5173e28.jpg?1625191123"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Psychotic Haze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d3f6cd2-0138-40e7-a975-3f7c68db0d93.jpg?1562630817", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d3f6cd2-0138-40e7-a975-3f7c68db0d93.jpg?1562630817"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Puppet's Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/052b743a-456d-49c3-881e-4f30c7645fa5.jpg?1562378946", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/052b743a-456d-49c3-881e-4f30c7645fa5.jpg?1562378946"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34ec6e8f-a8be-4efe-8082-d807378066b1.jpg?1562905712", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34ec6e8f-a8be-4efe-8082-d807378066b1.jpg?1562905712"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7afce33f-2ead-4943-9655-bff6eaa9fe6b.jpg?1562241054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7afce33f-2ead-4943-9655-bff6eaa9fe6b.jpg?1562241054"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de214247-e5e3-4d8f-935a-797218416be1.jpg?1562448294", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de214247-e5e3-4d8f-935a-797218416be1.jpg?1562448294"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88040748-ad76-4b9a-bd4e-87e5980e9816.jpg?1562920179", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88040748-ad76-4b9a-bd4e-87e5980e9816.jpg?1562920179"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pyroclasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0581322-d901-465e-b22c-cd99ddbb4839.jpg?1561758268", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0581322-d901-465e-b22c-cd99ddbb4839.jpg?1561758268"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiant Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70f4fe69-c541-4320-9074-9c6a3bc70ea3.jpg?1562921619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70f4fe69-c541-4320-9074-9c6a3bc70ea3.jpg?1562921619"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Radiant Flames", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/2548487e-a355-4a05-acbc-3031d98f4289.jpg?1562132516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/2548487e-a355-4a05-acbc-3031d98f4289.jpg?1562132516"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Radiating Lightning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94454128-92f1-475d-abc4-c235f501eeb6.jpg?1562739709", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94454128-92f1-475d-abc4-c235f501eeb6.jpg?1562739709"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rain of Daggers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb09a5bb-9730-43cd-8dea-3842634c9983.jpg?1562939110", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb09a5bb-9730-43cd-8dea-3842634c9983.jpg?1562939110"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rain of Embers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d5391a9-6c30-4f9b-b746-a4427a3e63fc.jpg?1598915805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rancid Earth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23d07a96-85ba-4714-94a5-4a8125954f58.jpg?1562628959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23d07a96-85ba-4714-94a5-4a8125954f58.jpg?1562628959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Reckless Endeavor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba98a4bd-e217-4dba-aee9-315b4f843cdf.jpg?1631585239", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba98a4bd-e217-4dba-aee9-315b4f843cdf.jpg?1631585239"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Reign of Terror", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bd83049-aec1-4911-bc70-39adba04b174.jpg?1587856923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bd83049-aec1-4911-bc70-39adba04b174.jpg?1587856923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Retaliate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58acdda6-6754-46f2-ad68-f1580b8ab0dd.jpg?1562877159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58acdda6-6754-46f2-ad68-f1580b8ab0dd.jpg?1562877159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Retribution of the Meek", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/860b8633-1bfc-426a-8666-5e6a584d4525.jpg?1587857186", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/860b8633-1bfc-426a-8666-5e6a584d4525.jpg?1587857186"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Righteous Fury", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c408f43e-9092-440d-a15f-bef4ad58bcc6.jpg?1562941331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c408f43e-9092-440d-a15f-bef4ad58bcc6.jpg?1562941331"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rising Miasma", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/f/4f9a8e87-3b8b-4dbf-9c1e-0a3290a33a0b.jpg?1562913657", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/f/4f9a8e87-3b8b-4dbf-9c1e-0a3290a33a0b.jpg?1562913657"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ritual of Soot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/269af993-4894-4bf1-b55a-af4d736cb3cc.jpg?1572893045", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/269af993-4894-4bf1-b55a-af4d736cb3cc.jpg?1572893045"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Riveteers Confluence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb15ba71-c3b3-4a9f-b000-bd788514211c.jpg?1650549265", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb15ba71-c3b3-4a9f-b000-bd788514211c.jpg?1650549265"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Rollick of Abandon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f1a80c4-8119-437d-bf5b-549c5679c90a.jpg?1593096073", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f1a80c4-8119-437d-bf5b-549c5679c90a.jpg?1593096073"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rolling Earthquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/c/3c1bf210-ecdb-4b49-8504-51360c269e66.jpg?1562256070"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Rolling Spoil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6c5546f-2429-4099-a9bd-eda3f52779b7.jpg?1598916497", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6c5546f-2429-4099-a9bd-eda3f52779b7.jpg?1598916497"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rolling Temblor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/060ce982-94dd-4b9e-b240-15da297e29f9.jpg?1562825667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/060ce982-94dd-4b9e-b240-15da297e29f9.jpg?1562825667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c3da3f0-bf90-461a-b62d-5c00d5c9aebd.jpg?1562865454", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c3da3f0-bf90-461a-b62d-5c00d5c9aebd.jpg?1562865454"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Rout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/94bc55ed-b89b-4e22-b3f1-4ce0f8d180d7.jpg?1562924999", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/94bc55ed-b89b-4e22-b3f1-4ce0f8d180d7.jpg?1562924999"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db53c1fb-3641-44a3-b0b4-b7b2ba993646.jpg?1562632349", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db53c1fb-3641-44a3-b0b4-b7b2ba993646.jpg?1562632349"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2a2a4e7-3173-4b73-8898-2c668f9eebf9.jpg?1562548310", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2a2a4e7-3173-4b73-8898-2c668f9eebf9.jpg?1562548310"}, "reprint": true, "digital": true, "set_type": "promo"}, {"name": "Sagittars' Volley", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3104cad-e684-4bd7-b26b-5aa862f7a2b3.jpg?1584831248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Savage Alliance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5255da8-8511-48a7-98e5-ba43ca6e8681.jpg?1576384658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5255da8-8511-48a7-98e5-ba43ca6e8681.jpg?1576384658"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99d22b83-381d-47da-b983-8f77d19b0c01.jpg?1562927484", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99d22b83-381d-47da-b983-8f77d19b0c01.jpg?1562927484"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/682ee5a9-2995-4868-b7ea-8735b2aee77e.jpg?1593272763", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/682ee5a9-2995-4868-b7ea-8735b2aee77e.jpg?1593272763"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Savage Twister", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb73313b-d39a-46ab-abfc-76f94a75dfca.jpg?1593014734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb73313b-d39a-46ab-abfc-76f94a75dfca.jpg?1593014734"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scouring Sands", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/273f25fc-9c9f-4b73-a28b-1461d8fcd443.jpg?1593092358", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/273f25fc-9c9f-4b73-a28b-1461d8fcd443.jpg?1593092358"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sculpted Sunburst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d16d8fe-a770-4bbd-bf20-447c0165de5a.jpg?1654291825", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d16d8fe-a770-4bbd-bf20-447c0165de5a.jpg?1654291825"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Seismic Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/b/9b952e4e-c1ed-4455-90d5-46b56478e6b0.jpg?1562790481", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/b/9b952e4e-c1ed-4455-90d5-46b56478e6b0.jpg?1562790481"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seismic Shudder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20365082-6102-4e3b-8791-c9b66846270d.jpg?1562610483", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20365082-6102-4e3b-8791-c9b66846270d.jpg?1562610483"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Seismic Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e55b8ffb-c2e4-4676-9051-ff6c686cad0b.jpg?1654567822"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Settle the Wreckage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9cbd346e-098a-4cf6-a72f-468376fd2e8f.jpg?1562560853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9cbd346e-098a-4cf6-a72f-468376fd2e8f.jpg?1562560853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shadowstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/367c4ad6-973d-47ba-9431-312f9f2996f6.jpg?1562053739", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/367c4ad6-973d-47ba-9431-312f9f2996f6.jpg?1562053739"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shadows' Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52470883-b44d-415b-9324-8074e66f79ae.jpg?1604196514", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52470883-b44d-415b-9324-8074e66f79ae.jpg?1604196514"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shake the Foundations", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b5bd4bdd-3a2a-40d9-9f86-fefe0a462cd2.jpg?1555040519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b5bd4bdd-3a2a-40d9-9f86-fefe0a462cd2.jpg?1555040519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shatter the Sky", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b706977b-db8e-4810-882d-ed3745404489.jpg?1581479244", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b706977b-db8e-4810-882d-ed3745404489.jpg?1581479244"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shrivel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a87c80a1-5818-45fd-9a37-a2ee3396626e.jpg?1562707116", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a87c80a1-5818-45fd-9a37-a2ee3396626e.jpg?1562707116"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sickening Dreams", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/3/9396ac77-9f53-46bd-b126-02441a0f5594.jpg?1562630974", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/3/9396ac77-9f53-46bd-b126-02441a0f5594.jpg?1562630974"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Simoon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/84b1930d-2e4b-472f-98a9-008fd632f3be.jpg?1562921826", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/84b1930d-2e4b-472f-98a9-008fd632f3be.jpg?1562921826"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Simoon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/642d9239-82e0-4696-ad99-10796042d1f8.jpg?1587913163", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/642d9239-82e0-4696-ad99-10796042d1f8.jpg?1587913163"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Single Combat", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce0e7c6a-e628-4327-a16f-2062c5a662df.jpg?1557576066", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce0e7c6a-e628-4327-a16f-2062c5a662df.jpg?1557576066"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyreaping", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40eb76b3-b527-4ed8-8ce3-d3de48562b6e.jpg?1593092666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40eb76b3-b527-4ed8-8ce3-d3de48562b6e.jpg?1593092666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slagstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e318b03-2aad-462b-a2a9-8b6bdf0e93d6.jpg?1562613393"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slash the Ranks", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/0913a5e8-7f77-44f2-a7cf-c8c0d6270a86.jpg?1608909011", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/0913a5e8-7f77-44f2-a7cf-c8c0d6270a86.jpg?1608909011"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Slaughter the Strong", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c9f8aea-0c9a-4686-b551-35e2a72ef701.jpg?1653521934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c9f8aea-0c9a-4686-b551-35e2a72ef701.jpg?1653521934"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Slaughter the Strong", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4217ab21-181e-4c32-97c3-d8bd441287e0.jpg?1555039791", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4217ab21-181e-4c32-97c3-d8bd441287e0.jpg?1555039791"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slice and Dice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59262684-86e3-4485-9e35-202771c3eaa6.jpg?1562916006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59262684-86e3-4485-9e35-202771c3eaa6.jpg?1562916006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Solar Blaze", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb72ba0f-ab3a-41e6-906d-a84039efa0af.jpg?1557577261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb72ba0f-ab3a-41e6-906d-a84039efa0af.jpg?1557577261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Solar Tide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57ce33b6-267f-4ee8-a3f7-f41c619d0cfa.jpg?1562144484", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57ce33b6-267f-4ee8-a3f7-f41c619d0cfa.jpg?1562144484"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soulquake", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b3a7470-b93e-4c3a-ab1c-0a4dd401e95a.jpg?1562641103", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b3a7470-b93e-4c3a-ab1c-0a4dd401e95a.jpg?1562641103"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spectral Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7238c46e-6338-4aca-96f2-934c44c8cc36.jpg?1631233619", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7238c46e-6338-4aca-96f2-934c44c8cc36.jpg?1631233619"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spontaneous Combustion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55d50177-736a-44d6-a2a3-f6892d7037b3.jpg?1562865429", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55d50177-736a-44d6-a2a3-f6892d7037b3.jpg?1562865429"}, "reprint": true, "digital": false, "set_type": "draft_innovation"}, {"name": "Spontaneous Combustion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34e6c04f-9d1a-497b-bc96-a0e48a1c1904.jpg?1562053293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34e6c04f-9d1a-497b-bc96-a0e48a1c1904.jpg?1562053293"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46460e5f-2756-486b-99a6-c3a9a209bfaa.jpg?1594065372", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46460e5f-2756-486b-99a6-c3a9a209bfaa.jpg?1594065372"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5409b54-66ed-4add-bf43-cfeb074b1c50.jpg?1562383517", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5409b54-66ed-4add-bf43-cfeb074b1c50.jpg?1562383517"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Squall", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63c1b2f6-e47f-4f18-a94a-1d08eb009ef3.jpg?1594065383", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63c1b2f6-e47f-4f18-a94a-1d08eb009ef3.jpg?1594065383"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Squall Line", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f368729-a6f2-4bf7-8b06-39c551f0b24a.jpg?1562908127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f368729-a6f2-4bf7-8b06-39c551f0b24a.jpg?1562908127"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Star of Extinction", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/021f57dc-80f3-4ede-99d5-4a44aade44e2.jpg?1562549822", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/021f57dc-80f3-4ede-99d5-4a44aade44e2.jpg?1562549822"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Starstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/439aa3eb-fa1f-46b2-a13a-369b6a88d97c.jpg?1562908992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/439aa3eb-fa1f-46b2-a13a-369b6a88d97c.jpg?1562908992"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Starstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/5/b54d72ba-05ce-4299-a7c3-a9e9f126fffb.jpg?1562937719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/5/b54d72ba-05ce-4299-a7c3-a9e9f126fffb.jpg?1562937719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Steam Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/144a1b4e-d960-4c3a-810b-11a0c78635ad.jpg?1562899291"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stench of Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9a45644-549a-4eaa-8367-b170027bd5a2.jpg?1562770859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9a45644-549a-4eaa-8367-b170027bd5a2.jpg?1562770859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stench of Decay", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4b93845-f17a-4892-a1ce-a4630dced218.jpg?1562770150", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4b93845-f17a-4892-a1ce-a4630dced218.jpg?1562770150"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stick Together", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d77a57a-e30b-46d7-acb8-1d164c7dff78.jpg?1654036983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d77a57a-e30b-46d7-acb8-1d164c7dff78.jpg?1654036983"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Storm's Wrath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4bc9ecd2-7664-471b-90f2-2d0dd1acec80.jpg?1581480444", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4bc9ecd2-7664-471b-90f2-2d0dd1acec80.jpg?1581480444"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Strategy, Schmategy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2996a63-9fb6-4455-906d-13f917a8bb29.jpg?1562799134"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Street Spasm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f599948f-1561-415f-b415-c9c991896704.jpg?1592713487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f599948f-1561-415f-b415-c9c991896704.jpg?1592713487"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Structural Assault", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cbdb50e3-fe15-4431-b9bd-c4de65820734.jpg?1650025267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cbdb50e3-fe15-4431-b9bd-c4de65820734.jpg?1650025267"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sublime Exhalation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac6d4a9e-a7fd-480e-96cf-5cf6d2390189.jpg?1562414946", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac6d4a9e-a7fd-480e-96cf-5cf6d2390189.jpg?1562414946"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Subterranean Tremors", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b9510b8-6601-4116-8713-ff7649c000eb.jpg?1576381980", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b9510b8-6601-4116-8713-ff7649c000eb.jpg?1576381980"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/wrath3.json b/web/public/mtg/jsons/wrath3.json deleted file mode 100644 index 19e27341..00000000 --- a/web/public/mtg/jsons/wrath3.json +++ /dev/null @@ -1 +0,0 @@ -{"has_more": false, "data": [{"name": "Sudden Demise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7217afaa-00e1-45a7-bb7f-66a770487b77.jpg?1562918949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7217afaa-00e1-45a7-bb7f-66a770487b77.jpg?1562918949"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Suffocating Fumes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66b562e4-35df-4aee-848d-ceb4204bbe58.jpg?1591226972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66b562e4-35df-4aee-848d-ceb4204bbe58.jpg?1591226972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sulfurous Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/67511e0e-be09-4f4e-9949-b9ecbdc7f536.jpg?1562916599"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sunscour", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44c726db-a30a-4e76-9fbf-ec6d5cd7a1ba.jpg?1593274832", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44c726db-a30a-4e76-9fbf-ec6d5cd7a1ba.jpg?1593274832"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e9648f9-7a67-4717-bca1-861d1f7fed43.jpg?1562786100", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e9648f9-7a67-4717-bca1-861d1f7fed43.jpg?1562786100"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2b760cc-800a-48a3-97d9-316e1eeafd4c.jpg?1655619437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2b760cc-800a-48a3-97d9-316e1eeafd4c.jpg?1655619437"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Supreme Verdict", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35e3b17c-1af9-4a6d-9cbe-e9d23ea52c53.jpg?1562497060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35e3b17c-1af9-4a6d-9cbe-e9d23ea52c53.jpg?1562497060"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Sweltering Suns", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f11cd406-c6ae-4018-ae45-4e5577aa82ae.jpg?1543675701", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f11cd406-c6ae-4018-ae45-4e5577aa82ae.jpg?1543675701"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swirling Sandstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d757ec3-c15f-4d6e-8e18-36ebae985448.jpg?1562629788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d757ec3-c15f-4d6e-8e18-36ebae985448.jpg?1562629788"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Synthetic Destiny", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6ab025e6-9ee7-45f0-b829-199637eb0038.jpg?1562705395", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6ab025e6-9ee7-45f0-b829-199637eb0038.jpg?1562705395"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Take Down", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f8e702db-8c73-4947-9c13-5dcb50f4efab.jpg?1576382690", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f8e702db-8c73-4947-9c13-5dcb50f4efab.jpg?1576382690"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terminus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/9/0982ea7e-05a4-4e40-98ab-ea9aa6c7342e.jpg?1592708421", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/9/0982ea7e-05a4-4e40-98ab-ea9aa6c7342e.jpg?1592708421"}, "reprint": false, "frame_effects": ["miracle"], "digital": false, "set_type": "expansion"}, {"name": "Thunder of Hooves", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e4f796a-6831-4d83-824d-88fd2148b4c1.jpg?1562932440", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e4f796a-6831-4d83-824d-88fd2148b4c1.jpg?1562932440"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderwave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9531098-63ea-4568-81e9-80e00a5f8995.jpg?1653417329", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9531098-63ea-4568-81e9-80e00a5f8995.jpg?1653417329"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Time Wipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62c59475-6f15-48d2-b105-f49901f20d44.jpg?1557577308", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62c59475-6f15-48d2-b105-f49901f20d44.jpg?1557577308"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Time Wipe", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6ab4b490-67d8-4f13-86cb-858a8012a46a.jpg?1558324717", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6ab4b490-67d8-4f13-86cb-858a8012a46a.jpg?1558324717"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Tivadar's Crusade", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b6da540-6803-47e5-9af0-7ae8e2f84b6c.jpg?1562927916", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b6da540-6803-47e5-9af0-7ae8e2f84b6c.jpg?1562927916"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Torrent of Lava", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19528a24-4968-4742-a2d1-06f94e60f290.jpg?1562718298", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19528a24-4968-4742-a2d1-06f94e60f290.jpg?1562718298"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/db34617f-b04f-4b65-84cf-5c5be1eb7226.jpg?1651951814", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/db34617f-b04f-4b65-84cf-5c5be1eb7226.jpg?1651951814"}, "reprint": true, "digital": false, "set_type": "arsenal"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/564caf57-4ba5-4993-a35e-945699c94eb7.jpg?1562913020", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/564caf57-4ba5-4993-a35e-945699c94eb7.jpg?1562913020"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Toxic Deluge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73731e45-51bb-4188-a54d-fdaa4bdfaf1f.jpg?1599711037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73731e45-51bb-4188-a54d-fdaa4bdfaf1f.jpg?1599711037"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b281c013-b35a-4c4a-aaee-b6f93968485c.jpg?1562246219", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b281c013-b35a-4c4a-aaee-b6f93968485c.jpg?1562246219"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/8531efb1-d77d-451a-8621-424fc278ccf9.jpg?1562381834", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/8531efb1-d77d-451a-8621-424fc278ccf9.jpg?1562381834"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2509285-a88e-4f5c-86c1-c0386da0f0c5.jpg?1562948885", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2509285-a88e-4f5c-86c1-c0386da0f0c5.jpg?1562948885"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Tremor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a9d64665-c1e0-40ab-a358-247f82966379.jpg?1562278171", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a9d64665-c1e0-40ab-a358-247f82966379.jpg?1562278171"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tropical Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd5f473c-e11e-4047-91f9-81b80f0a3562.jpg?1587912688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd5f473c-e11e-4047-91f9-81b80f0a3562.jpg?1587912688"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tsabo's Decree", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c1a0ebd-1add-49e6-b5e6-5b26abb1de88.jpg?1562897461", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c1a0ebd-1add-49e6-b5e6-5b26abb1de88.jpg?1562897461"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Underworld Fires", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0fe616c4-dcb0-4284-ba10-6fbf7cecd217.jpg?1581480512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0fe616c4-dcb0-4284-ba10-6fbf7cecd217.jpg?1581480512"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Urborg Justice", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/39f322ff-0b04-41ce-90cd-9896f941e703.jpg?1562800263", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/39f322ff-0b04-41ce-90cd-9896f941e703.jpg?1562800263"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valiant Endeavor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/4/0445def0-8921-4579-912f-035d9fbce3c0.jpg?1631584806", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/4/0445def0-8921-4579-912f-035d9fbce3c0.jpg?1631584806"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Vampires' Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/20d2d886-13a2-44f1-966a-ec674622fd01.jpg?1643592028", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/20d2d886-13a2-44f1-966a-ec674622fd01.jpg?1643592028"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vampires' Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f4ba693-0323-415d-ad91-c083fbbab7f7.jpg?1645228860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f4ba693-0323-415d-ad91-c083fbbab7f7.jpg?1645228860"}, "flavor_name": "Mysterious Blood Illness", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vanquish the Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e264615c-eb99-4cb3-844a-2b4a94ba5203.jpg?1634348651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e264615c-eb99-4cb3-844a-2b4a94ba5203.jpg?1634348651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Virtue's Ruin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/7854928a-d467-4616-b96b-de7e5fe7303e.jpg?1562446869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/7854928a-d467-4616-b96b-de7e5fe7303e.jpg?1562446869"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0b3e320-a85c-4d92-944e-0a5e78a066a5.jpg?1580015114", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0b3e320-a85c-4d92-944e-0a5e78a066a5.jpg?1580015114"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Void", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62dc1df7-b9db-4f5f-a340-08287cd3d9e5.jpg?1562915020", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62dc1df7-b9db-4f5f-a340-08287cd3d9e5.jpg?1562915020"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Eruption", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a80582b1-09db-45f8-b362-0e5207a5a8e6.jpg?1559591541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a80582b1-09db-45f8-b362-0e5207a5a8e6.jpg?1559591541"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/65536d12-e75c-42b5-b592-a3ad4f550a71.jpg?1592485188"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Volcanic Fallout", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d3a69d2-518d-4b70-a03e-6d02a525f9ad.jpg?1561757550"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Volcanic Spray", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97daab4b-d934-4a3f-a043-f7c9c1dd32bf.jpg?1562923217"}, "reprint": false, "frame_effects": ["tombstone"], "digital": false, "set_type": "expansion"}, {"name": "Volcanic Torrent", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb586da6-670d-4c50-9d9b-f320f1c288d7.jpg?1608910472", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb586da6-670d-4c50-9d9b-f320f1c288d7.jpg?1608910472"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Volcanic Vision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f973e1a6-c6f9-47f5-9bf0-b7fa06959bd4.jpg?1625194143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f973e1a6-c6f9-47f5-9bf0-b7fa06959bd4.jpg?1625194143"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Volcanic Vision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/979da13b-9be6-49cc-a62c-67eeea289612.jpg?1562790292", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/979da13b-9be6-49cc-a62c-67eeea289612.jpg?1562790292"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wail of the Nim", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a8c32faa-c6d1-418a-aed6-ccc5849daa1f.jpg?1562153645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a8c32faa-c6d1-418a-aed6-ccc5849daa1f.jpg?1562153645"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wave of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27d6655d-f55c-4bfc-a9c6-10232ebc707b.jpg?1562392689", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27d6655d-f55c-4bfc-a9c6-10232ebc707b.jpg?1562392689"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Wave of Reckoning", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b101b5e-d478-4686-b3cf-bdc545f089e5.jpg?1562378964", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b101b5e-d478-4686-b3cf-bdc545f089e5.jpg?1562378964"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whelming Wave", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fcabd4c7-093f-4ef6-8b89-b08565c48e3c.jpg?1593091836", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fcabd4c7-093f-4ef6-8b89-b08565c48e3c.jpg?1593091836"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whipflare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a7e6c10-d066-4967-932f-5b6c8d74568b.jpg?1562877860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a7e6c10-d066-4967-932f-5b6c8d74568b.jpg?1562877860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8101bab4-ef93-451a-a24f-e1456c82837c.jpg?1562922208", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8101bab4-ef93-451a-a24f-e1456c82837c.jpg?1562922208"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Widespread Brutality", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97715d22-f432-4f67-b4ea-47b8fe6edca5.jpg?1557577331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97715d22-f432-4f67-b4ea-47b8fe6edca5.jpg?1557577331"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/826fd527-9356-4eec-8542-781116f23eb7.jpg?1562241689", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/826fd527-9356-4eec-8542-781116f23eb7.jpg?1562241689"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72d50972-4549-40cd-9c33-4b341333803f.jpg?1562919111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72d50972-4549-40cd-9c33-4b341333803f.jpg?1562919111"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Wildfire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b69cfcb0-db68-4494-a3e1-7c2ca279fcf5.jpg?1562938018", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b69cfcb0-db68-4494-a3e1-7c2ca279fcf5.jpg?1562938018"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Winds of Abandon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3bb17913-fe4d-4acd-9b75-71f5a90f898b.jpg?1562201278", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3bb17913-fe4d-4acd-9b75-71f5a90f898b.jpg?1562201278"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Winds of Rath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/6/a6d731b2-0113-4fd5-8b78-1aa1064bb4f5.jpg?1562055907", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/6/a6d731b2-0113-4fd5-8b78-1aa1064bb4f5.jpg?1562055907"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Windstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/154dc31c-ac9d-4b78-b92b-e7bacc532915.jpg?1562782948", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/154dc31c-ac9d-4b78-b92b-e7bacc532915.jpg?1562782948"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Windstorm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee3768ec-bb3b-44dc-9fa3-7cb3d3ee9f8c.jpg?1562000543", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee3768ec-bb3b-44dc-9fa3-7cb3d3ee9f8c.jpg?1562000543"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Winter Sky", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af1035f3-3027-4a41-834c-55222b13c2bc.jpg?1562588224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Witch's Vengeance", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbf16457-3444-4130-b220-834b69d9faa3.jpg?1572490276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbf16457-3444-4130-b220-834b69d9faa3.jpg?1572490276"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d223e83-0d3c-459e-96f5-ba9227fe49dd.jpg?1562232378", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d223e83-0d3c-459e-96f5-ba9227fe49dd.jpg?1562232378"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d75d8204-6f9d-4a7a-bb8b-d51ac65a30fa.jpg?1562447853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d75d8204-6f9d-4a7a-bb8b-d51ac65a30fa.jpg?1562447853"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2788d69-6a3a-42f0-8736-cc6b57755ecd.jpg?1559591620", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2788d69-6a3a-42f0-8736-cc6b57755ecd.jpg?1559591620"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0adf3831-93d9-4995-b8c8-0d8c03fee872.jpg?1657809849", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0adf3831-93d9-4995-b8c8-0d8c03fee872.jpg?1657809849"}, "flavor_name": "Shrinking Storm", "reprint": true, "digital": false, "set_type": "box"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a91be77a-bd5b-485f-b5ca-0e6148c236ca.jpg?1619340331", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a91be77a-bd5b-485f-b5ca-0e6148c236ca.jpg?1619340331"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/4351bf97-0b9e-44a5-bb7c-1098a683b18d.jpg?1562908574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/4351bf97-0b9e-44a5-bb7c-1098a683b18d.jpg?1562908574"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/98890cd7-ebd5-4fea-814e-4f612abfe3a5.jpg?1560576455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/98890cd7-ebd5-4fea-814e-4f612abfe3a5.jpg?1560576455"}, "reprint": true, "digital": false, "set_type": "from_the_vault"}, {"name": "Wrath of God", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10c5810f-83f6-43bf-8ece-047be42d7d58.jpg?1561756672", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10c5810f-83f6-43bf-8ece-047be42d7d58.jpg?1561756672"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Yahenni's Expertise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2f28735-122c-45ba-bde5-decfd9b11b32.jpg?1576381752", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2f28735-122c-45ba-bde5-decfd9b11b32.jpg?1576381752"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yahenni's Expertise", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f4ddbb7-b317-44dc-bb3d-52f52c0a8f96.jpg?1562270938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f4ddbb7-b317-44dc-bb3d-52f52c0a8f96.jpg?1562270938"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Yamabushi's Storm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0a5a930d-ae59-47e2-9b98-f703e308b5c0.jpg?1562757474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0a5a930d-ae59-47e2-9b98-f703e308b5c0.jpg?1562757474"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zealous Persecution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07d8ae46-14ec-4878-ba8a-a47d4508c6d7.jpg?1562639500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07d8ae46-14ec-4878-ba8a-a47d4508c6d7.jpg?1562639500"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zealous Persecution", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/00963993-ff4d-4cc6-a7e0-ed8adac40bfd.jpg?1562895154", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/00963993-ff4d-4cc6-a7e0-ed8adac40bfd.jpg?1562895154"}, "reprint": true, "digital": false, "set_type": "box"}]} \ No newline at end of file From 8f5e51a3049d1026b226f1b8e13dab318f7776f1 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 22:13:37 -0500 Subject: [PATCH 291/519] Small FR comments tweaks --- .../feed/feed-answer-comment-group.tsx | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index a48a7e9c..c0b7162c 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -3,8 +3,6 @@ import { Bet } from 'common/bet' import { Comment } from 'common/comment' import React, { useEffect, useState } from 'react' import { Col } from 'web/components/layout/col' -import { Modal } from 'web/components/layout/modal' -import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' @@ -34,7 +32,6 @@ export function FeedAnswerCommentGroup(props: { const { username, avatarUrl, name, text } = answer const [replyToUsername, setReplyToUsername] = useState('') - const [open, setOpen] = useState(false) const [showReply, setShowReply] = useState(false) const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [highlighted, setHighlighted] = useState(false) @@ -104,26 +101,15 @@ export function FeedAnswerCommentGroup(props: { return ( <Col className={'relative flex-1 gap-3'} key={answer.id + 'comment'}> - <Modal open={open} setOpen={setOpen}> - <AnswerBetPanel - answer={answer} - contract={contract} - closePanel={() => setOpen(false)} - className="sm:max-w-84 !rounded-md bg-white !px-8 !py-6" - isModal={true} - /> - </Modal> - <Row className={clsx( - 'mt-4 flex gap-3 space-x-3 transition-all duration-1000', + 'flex gap-3 space-x-3 pt-4 transition-all duration-1000', highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : '' )} id={answerElementId} > - <div className="px-1"> - <Avatar username={username} avatarUrl={avatarUrl} /> - </div> + <Avatar username={username} avatarUrl={avatarUrl} /> + <Col className="min-w-0 flex-1 lg:gap-1"> <div className="text-sm text-gray-500"> <UserLink username={username} name={name} /> answered @@ -135,25 +121,21 @@ export function FeedAnswerCommentGroup(props: { /> </div> - <Col className="align-items justify-between gap-4 sm:flex-row"> + <Col className="align-items justify-between gap-2 sm:flex-row"> <span className="whitespace-pre-line text-lg"> <Linkify text={text} /> </span> - <Row className="items-center justify-center gap-4"> - {isFreeResponseContractPage && ( - <div className={'sm:hidden'}> - <button - className={ - 'text-xs font-bold text-gray-500 hover:underline' - } - onClick={() => scrollAndOpenReplyInput(undefined, answer)} - > - Reply - </button> - </div> - )} - </Row> + {isFreeResponseContractPage && ( + <div className={'sm:hidden'}> + <button + className={'text-xs font-bold text-gray-500 hover:underline'} + onClick={() => scrollAndOpenReplyInput(undefined, answer)} + > + Reply + </button> + </div> + )} </Col> {isFreeResponseContractPage && ( <div className={'justify-initial hidden sm:block'}> From f7151f131d77d9a7cf5b73b9db8022e22f8d8a83 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 20 Jul 2022 22:37:43 -0500 Subject: [PATCH 292/519] Spacing tweak --- web/components/feed/feed-answer-comment-group.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index c0b7162c..aabb1081 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -162,9 +162,9 @@ export function FeedAnswerCommentGroup(props: { /> {showReply && ( - <div className={'ml-6 pt-4'}> + <div className={'ml-6'}> <span - className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" + className="absolute -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200" aria-hidden="true" /> <CommentInput From 7a041fd753daa00cb9a17a3cacc9ca399ece6db4 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 20 Jul 2022 22:45:53 -0700 Subject: [PATCH 293/519] Changing manalinks table UI (#665) From table to card view --- web/components/contract/contract-details.tsx | 9 +- .../contract/contract-info-dialog.tsx | 2 +- web/components/manalink-card.tsx | 262 +++++++++++++----- .../manalinks/create-links-button.tsx | 5 +- web/components/pagination.tsx | 9 +- web/components/share-icon-button.tsx | 49 ++-- web/pages/link/[slug].tsx | 64 +++-- web/pages/links.tsx | 155 ++--------- 8 files changed, 285 insertions(+), 270 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index b4d67520..036311fe 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -11,6 +11,7 @@ import { UserLink } from '../user-page' import { Contract, contractMetrics, + contractPath, contractPool, updateContract, } from 'web/lib/firebase/contracts' @@ -33,6 +34,7 @@ import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' import { exhibitExts } from 'common/util/parse' +import { ENV_CONFIG } from 'common/envs/constants' export type ShowTime = 'resolve-date' | 'close-date' @@ -222,9 +224,12 @@ export function ContractDetails(props: { <div className="whitespace-nowrap">{volumeLabel}</div> </Row> <ShareIconButton - contract={contract} + copyPayload={`https://${ENV_CONFIG.domain}${contractPath(contract)}${ + user?.username && contract.creatorUsername !== user?.username + ? '?referrer=' + user?.username + : '' + }`} toastClassName={'sm:-left-40 -left-24 min-w-[250%]'} - username={user?.username} /> {!disabled && <ContractInfoDialog contract={contract} bets={bets} />} diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index a0c7fcc9..d976253f 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -19,7 +19,7 @@ import { InfoTooltip } from '../info-tooltip' import { DuplicateContractButton } from '../copy-contract-button' export const contractDetailsButtonClassName = - 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' + 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { const { contract, bets } = props diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index b5a79091..b49e1621 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -3,9 +3,13 @@ import { formatMoney } from 'common/util/format' import { fromNow } from 'web/lib/util/time' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' -import { User } from 'web/lib/firebase/users' -import { Button } from './button' - +import { Claim, Manalink } from 'common/manalink' +import { useState } from 'react' +import { ShareIconButton } from './share-icon-button' +import { DotsHorizontalIcon } from '@heroicons/react/solid' +import { contractDetailsButtonClassName } from './contract/contract-info-dialog' +import { useUserById } from 'web/hooks/use-user' +import getManalinkUrl from 'web/get-manalink-url' export type ManalinkInfo = { expiresTime: number | null maxUses: number | null @@ -15,94 +19,202 @@ export type ManalinkInfo = { } export function ManalinkCard(props: { - user: User | null | undefined - className?: string info: ManalinkInfo - isClaiming: boolean - onClaim?: () => void + className?: string + preview?: boolean }) { - const { user, className, isClaiming, info, onClaim } = props + const { className, info, preview = false } = props const { expiresTime, maxUses, uses, amount, message } = info return ( - <div + <Col> + <div + className={clsx( + className, + 'min-h-20 group flex flex-col rounded-xl bg-gradient-to-br shadow-lg transition-all', + getManalinkGradient(info.amount) + )} + > + <Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100"> + <div> + {maxUses != null + ? `${maxUses - uses}/${maxUses} uses left` + : `Unlimited use`} + </div> + <div> + {expiresTime != null + ? `Expires ${fromNow(expiresTime)}` + : 'Never expires'} + </div> + </Col> + + <img + className={clsx( + 'block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12', + preview ? 'my-2' : 'w-1/2 md:mb-6 md:h-1/2' + )} + src="/logo-white.svg" + /> + <Row className="rounded-b-xl bg-white p-4"> + <Col> + <div + className={clsx( + 'mb-1 text-xl text-indigo-500', + getManalinkAmountColor(amount) + )} + > + {formatMoney(amount)} + </div> + <div>{message}</div> + </Col> + </Row> + </div> + </Col> + ) +} + +export function ManalinkCardFromView(props: { + className?: string + link: Manalink + highlightedSlug: string +}) { + const { className, link, highlightedSlug } = props + const { message, amount, expiresTime, maxUses, claims } = link + const [details, setDetails] = useState(false) + + return ( + <Col className={clsx( + 'group z-10 rounded-lg drop-shadow-sm transition-all hover:drop-shadow-lg', className, - 'min-h-20 group flex flex-col rounded-xl bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all' + link.slug === highlightedSlug ? 'animate-pulse' : '' )} > - <Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100"> - <div> - {maxUses != null - ? `${maxUses - uses}/${maxUses} uses left` - : `Unlimited use`} - </div> - <div> - {expiresTime != null - ? `Expires ${fromNow(expiresTime)}` - : 'Never expires'} - </div> - </Col> - - <img - className="mb-6 block self-center transition-all group-hover:rotate-12" - src="/logo-white.svg" - width={200} - height={200} - /> - <Row className="justify-end rounded-b-xl bg-white p-4"> - <Col> - <div className="mb-1 text-xl text-indigo-500"> + <div + className={clsx( + 'relative flex flex-col rounded-t-lg bg-gradient-to-br transition-all', + getManalinkGradient(link.amount) + )} + onClick={() => setDetails(!details)} + > + {details && ( + <ClaimsList + className="absolute h-full w-full bg-white opacity-90" + link={link} + /> + )} + <Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100"> + <div> + {maxUses != null + ? `${maxUses - claims.length}/${maxUses} uses left` + : `Unlimited use`} + </div> + <div> + {expiresTime != null + ? `Expires ${fromNow(expiresTime)}` + : 'Never expires'} + </div> + </Col> + <img + className={clsx('my-auto block w-1/3 select-none self-center py-3')} + src="/logo-white.svg" + /> + </div> + <Col className="w-full rounded-b-lg bg-white px-4 py-2 text-lg"> + <Row className="relative gap-1"> + <div + className={clsx( + 'my-auto mb-1 w-full', + getManalinkAmountColor(amount) + )} + > {formatMoney(amount)} </div> - <div>{message}</div> - </Col> - - <div className="ml-auto"> - <Button onClick={onClaim} disabled={isClaiming}> - {user ? 'Claim' : 'Login'} - </Button> - </div> - </Row> - </div> + <ShareIconButton + toastClassName={'-left-48 min-w-[250%]'} + buttonClassName={'transition-colors'} + onCopyButtonClassName={ + 'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600' + } + copyPayload={getManalinkUrl(link.slug)} + /> + <button + onClick={() => setDetails(!details)} + className={clsx( + contractDetailsButtonClassName, + details + ? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600' + : '' + )} + > + <DotsHorizontalIcon className="h-[24px] w-5" /> + </button> + </Row> + <div className="my-2 text-xs md:text-sm">{message || '\n\n'}</div> + </Col> + </Col> ) } -export function ManalinkCardPreview(props: { - className?: string - info: ManalinkInfo -}) { - const { className, info } = props - const { expiresTime, maxUses, uses, amount, message } = info +function ClaimsList(props: { link: Manalink; className: string }) { + const { link, className } = props return ( - <div - className={clsx( - className, - ' group flex flex-col rounded-lg bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all' - )} - > - <Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100"> - <div> - {maxUses != null - ? `${maxUses - uses}/${maxUses} uses left` - : `Unlimited use`} + <> + <Col className={clsx('px-4 py-2', className)}> + <div className="text-md mb-1 mt-2 w-full font-semibold"> + Claimed by... </div> - <div> - {expiresTime != null - ? `Expires ${fromNow(expiresTime)}` - : 'Never expires'} + <div className="overflow-auto"> + {link.claims.length > 0 ? ( + <> + {link.claims.map((claim) => ( + <Row key={claim.txnId}> + <Claim claim={claim} /> + </Row> + ))} + </> + ) : ( + <div className="h-full"> + No one has claimed this manalink yet! Share your manalink to start + spreading the wealth. + </div> + )} </div> </Col> - - <img - className="my-2 block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12" - src="/logo-white.svg" - /> - <Row className="rounded-b-lg bg-white p-2"> - <Col className="text-md"> - <div className="mb-1 text-indigo-500">{formatMoney(amount)}</div> - <div className="text-xs">{message}</div> - </Col> - </Row> - </div> + </> ) } + +function Claim(props: { claim: Claim }) { + const { claim } = props + const who = useUserById(claim.toId) + return ( + <Row className="my-1 gap-2 text-xs"> + <div>{who?.name || 'Loading...'}</div> + <div className="text-gray-500">{fromNow(claim.claimedTime)}</div> + </Row> + ) +} + +function getManalinkGradient(amount: number) { + if (amount < 20) { + return 'from-indigo-200 via-indigo-500 to-indigo-800' + } else if (amount >= 20 && amount < 50) { + return 'from-fuchsia-200 via-fuchsia-500 to-fuchsia-800' + } else if (amount >= 50 && amount < 100) { + return 'from-rose-100 via-rose-400 to-rose-700' + } else if (amount >= 100) { + return 'from-amber-200 via-amber-500 to-amber-700' + } +} + +function getManalinkAmountColor(amount: number) { + if (amount < 20) { + return 'text-indigo-500' + } else if (amount >= 20 && amount < 50) { + return 'text-fuchsia-600' + } else if (amount >= 50 && amount < 100) { + return 'text-rose-600' + } else if (amount >= 100) { + return 'text-amber-600' + } +} diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index 0d1d603e..25b51bb2 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -4,7 +4,7 @@ import { Col } from '../layout/col' import { Row } from '../layout/row' import { Title } from '../title' import { User } from 'common/user' -import { ManalinkCardPreview, ManalinkInfo } from 'web/components/manalink-card' +import { ManalinkCard, ManalinkInfo } from 'web/components/manalink-card' import { createManalink } from 'web/lib/firebase/manalinks' import { Modal } from 'web/components/layout/modal' import Textarea from 'react-expanding-textarea' @@ -37,6 +37,7 @@ export function CreateLinksButton(props: { message: newManalink.message, }) setHighlightedSlug(slug || '') + setTimeout(() => setHighlightedSlug(''), 3700) }} /> </Col> @@ -191,7 +192,7 @@ function CreateManalinkForm(props: { {finishedCreating && ( <> <Title className="!my-0" text="Manalink Created!" /> - <ManalinkCardPreview className="my-4" info={newManalink} /> + <ManalinkCard className="my-4" info={newManalink} preview /> <Row className={clsx( 'rounded border bg-gray-50 py-2 px-3 text-sm text-gray-500 transition-colors duration-700', diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx index 069ebda7..3f4108bc 100644 --- a/web/components/pagination.tsx +++ b/web/components/pagination.tsx @@ -1,9 +1,12 @@ +import clsx from 'clsx' + export function Pagination(props: { page: number itemsPerPage: number totalItems: number setPage: (page: number) => void scrollToTop?: boolean + className?: string nextTitle?: string prevTitle?: string }) { @@ -15,13 +18,17 @@ export function Pagination(props: { scrollToTop, nextTitle, prevTitle, + className, } = props const maxPage = Math.ceil(totalItems / itemsPerPage) - 1 return ( <nav - className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" + className={clsx( + 'flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6', + className + )} aria-label="Pagination" > <div className="hidden sm:block"> diff --git a/web/components/share-icon-button.tsx b/web/components/share-icon-button.tsx index 507d90c2..4db192a9 100644 --- a/web/components/share-icon-button.tsx +++ b/web/components/share-icon-button.tsx @@ -2,65 +2,48 @@ import React, { useState } from 'react' import { ShareIcon } from '@heroicons/react/outline' import clsx from 'clsx' -import { Contract } from 'common/contract' import { copyToClipboard } from 'web/lib/util/copy' -import { contractPath } from 'web/lib/firebase/contracts' -import { ENV_CONFIG } from 'common/envs/constants' import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog' -import { Group } from 'common/group' -import { groupPath } from 'web/lib/firebase/groups' - -function copyContractWithReferral(contract: Contract, username?: string) { - const postFix = - username && contract.creatorUsername !== username - ? '?referrer=' + username - : '' - copyToClipboard( - `https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}` - ) -} - -// Note: if a user arrives at a /group endpoint with a ?referral= query, they'll be added to the group automatically -function copyGroupWithReferral(group: Group, username?: string) { - const postFix = username ? '?referrer=' + username : '' - copyToClipboard( - `https://${ENV_CONFIG.domain}${groupPath(group.slug)}${postFix}` - ) -} export function ShareIconButton(props: { - contract?: Contract - group?: Group buttonClassName?: string + onCopyButtonClassName?: string toastClassName?: string - username?: string children?: React.ReactNode + iconClassName?: string + copyPayload: string }) { const { - contract, buttonClassName, + onCopyButtonClassName, toastClassName, - username, - group, children, + iconClassName, + copyPayload, } = props const [showToast, setShowToast] = useState(false) return ( <div className="relative z-10 flex-shrink-0"> <button - className={clsx(contractDetailsButtonClassName, buttonClassName)} + className={clsx( + contractDetailsButtonClassName, + buttonClassName, + showToast ? onCopyButtonClassName : '' + )} onClick={() => { - if (contract) copyContractWithReferral(contract, username) - if (group) copyGroupWithReferral(group, username) + copyToClipboard(copyPayload) track('copy share link') setShowToast(true) setTimeout(() => setShowToast(false), 2000) }} > - <ShareIcon className="h-[24px] w-5" aria-hidden="true" /> + <ShareIcon + className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} + aria-hidden="true" + /> {children} </button> diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 8ad9850f..119fec77 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -7,6 +7,8 @@ import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' import { firebaseLogin } from 'web/lib/firebase/users' +import { Row } from 'web/components/layout/row' +import { Button } from 'web/components/button' export default function ClaimPage() { const user = useUser() @@ -28,34 +30,42 @@ export default function ClaimPage() { description="Send mana to anyone via link!" url="/send" /> - <div className="mx-auto max-w-xl"> - <Title text={`Claim M$${manalink.amount} mana`} /> - <ManalinkCard - user={user} - info={info} - isClaiming={claiming} - onClaim={async () => { - setClaiming(true) - try { - if (user == null) { - await firebaseLogin() + <div className="mx-auto max-w-xl px-2"> + <Row className="items-center justify-between"> + <Title text={`Claim M$${manalink.amount} mana`} /> + <div className="my-auto"> + <Button + onClick={async () => { + setClaiming(true) + try { + if (user == null) { + await firebaseLogin() + setClaiming(false) + return + } + if (user?.id == manalink.fromId) { + throw new Error("You can't claim your own manalink.") + } + await claimManalink({ slug: manalink.slug }) + user && router.push(`/${user.username}?claimed-mana=yes`) + } catch (e) { + console.log(e) + const message = + e && e instanceof Object + ? e.toString() + : 'An error occurred.' + setError(message) + } setClaiming(false) - return - } - if (user?.id == manalink.fromId) { - throw new Error("You can't claim your own manalink.") - } - await claimManalink({ slug: manalink.slug }) - user && router.push(`/${user.username}?claimed-mana=yes`) - } catch (e) { - console.log(e) - const message = - e && e instanceof Object ? e.toString() : 'An error occurred.' - setError(message) - } - setClaiming(false) - }} - /> + }} + disabled={claiming} + size="lg" + > + {user ? 'Claim' : 'Login'} + </Button> + </div> + </Row> + <ManalinkCard info={info} /> {error && ( <section className="my-5 text-red-500"> <p>Failed to claim manalink.</p> diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 490f1878..8a2e6767 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -1,7 +1,4 @@ -import clsx from 'clsx' import { useState } from 'react' -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid' -import { Claim, Manalink } from 'common/manalink' import { formatMoney } from 'common/util/format' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' @@ -11,7 +8,6 @@ import { Title } from 'web/components/title' import { Subtitle } from 'web/components/subtitle' import { useUser } from 'web/hooks/use-user' import { useUserManalinks } from 'web/lib/firebase/manalinks' -import { fromNow } from 'web/lib/util/time' import { useUserById } from 'web/hooks/use-user' import { ManalinkTxn } from 'common/txn' import { Avatar } from 'web/components/avatar' @@ -22,8 +18,11 @@ import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' +import { ManalinkCardFromView } from 'web/components/manalink-card' +import { Pagination } from 'web/components/pagination' dayjs.extend(customParseFormat) +const LINKS_PER_PAGE = 24 export const getServerSideProps = redirectIfLoggedOut('/') export function getManalinkUrl(slug: string) { @@ -40,6 +39,10 @@ export default function LinkPage() { (l.maxUses == null || l.claimedUserIds.length < l.maxUses) && (l.expiresTime == null || l.expiresTime > Date.now()) ) + const [page, setPage] = useState(0) + const start = page * LINKS_PER_PAGE + const end = start + LINKS_PER_PAGE + const displayedLinks = unclaimedLinks.slice(start, end) if (user == null) { return null @@ -68,12 +71,30 @@ export default function LinkPage() { don't yet have a Manifold account. </p> <Subtitle text="Your Manalinks" /> - <LinksTable links={unclaimedLinks} highlightedSlug={highlightedSlug} /> + <Col className="grid w-full gap-4 md:grid-cols-2"> + {displayedLinks.map((link) => { + return ( + <ManalinkCardFromView + link={link} + highlightedSlug={highlightedSlug} + /> + ) + })} + </Col> + <Pagination + page={page} + itemsPerPage={LINKS_PER_PAGE} + totalItems={unclaimedLinks.length} + setPage={setPage} + className="mt-4 bg-transparent" + scrollToTop + /> </Col> </Page> ) } +// TODO: either utilize this or get rid of it export function ClaimsList(props: { txns: ManalinkTxn[] }) { const { txns } = props return ( @@ -121,127 +142,3 @@ export function ClaimDescription(props: { txn: ManalinkTxn }) { </div> ) } - -function ClaimTableRow(props: { claim: Claim }) { - const { claim } = props - const who = useUserById(claim.toId) - return ( - <tr> - <td className="px-5 py-2">{who?.name || 'Loading...'}</td> - <td className="px-5 py-2">{`${new Date( - claim.claimedTime - ).toLocaleString()}, ${fromNow(claim.claimedTime)}`}</td> - </tr> - ) -} - -function LinkDetailsTable(props: { link: Manalink }) { - const { link } = props - return ( - <table className="w-full divide-y divide-gray-300 border border-gray-400"> - <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> - <tr> - <th className="px-5 py-2">Claimed by</th> - <th className="px-5 py-2">Time</th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200 bg-white text-sm text-gray-500"> - {link.claims.length ? ( - link.claims.map((claim) => <ClaimTableRow claim={claim} />) - ) : ( - <tr> - <td className="px-5 py-2" colSpan={2}> - No claims yet. - </td> - </tr> - )} - </tbody> - </table> - ) -} - -function LinkTableRow(props: { link: Manalink; highlight: boolean }) { - const { link, highlight } = props - const [expanded, setExpanded] = useState(false) - return ( - <> - <LinkSummaryRow - link={link} - highlight={highlight} - expanded={expanded} - onToggle={() => setExpanded((exp) => !exp)} - /> - {expanded && ( - <tr> - <td className="bg-gray-100 p-3" colSpan={5}> - <LinkDetailsTable link={link} /> - </td> - </tr> - )} - </> - ) -} - -function LinkSummaryRow(props: { - link: Manalink - highlight: boolean - expanded: boolean - onToggle: () => void -}) { - const { link, highlight, expanded, onToggle } = props - const className = clsx( - 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white', - highlight ? 'bg-indigo-100 rounded-lg animate-pulse' : '' - ) - return ( - <tr id={link.slug} key={link.slug} className={className}> - <td className="py-4 pl-5" onClick={onToggle}> - {expanded ? ( - <ChevronUpIcon className="h-5 w-5" /> - ) : ( - <ChevronDownIcon className="h-5 w-5" /> - )} - </td> - - <td className="px-5 py-4 font-medium text-gray-900"> - {formatMoney(link.amount)} - </td> - <td className="px-5 py-4">{getManalinkUrl(link.slug)}</td> - <td className="px-5 py-4">{link.claimedUserIds.length}</td> - <td className="px-5 py-4">{link.maxUses == null ? '∞' : link.maxUses}</td> - <td className="px-5 py-4"> - {link.expiresTime == null ? 'Never' : fromNow(link.expiresTime)} - </td> - </tr> - ) -} - -function LinksTable(props: { links: Manalink[]; highlightedSlug?: string }) { - const { links, highlightedSlug } = props - return links.length == 0 ? ( - <p>You don't currently have any outstanding manalinks.</p> - ) : ( - <div className="overflow-scroll"> - <table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200"> - <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> - <tr> - <th></th> - <th className="px-5 py-3.5">Amount</th> - <th className="px-5 py-3.5">Link</th> - <th className="px-5 py-3.5">Uses</th> - <th className="px-5 py-3.5">Max Uses</th> - <th className="px-5 py-3.5">Expires</th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200 bg-white"> - {links.map((link) => ( - <LinkTableRow - link={link} - highlight={link.slug === highlightedSlug} - /> - ))} - </tbody> - </table> - </div> - ) -} From 2ad72662833d3c6462df34cef554ad119aa9521c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 21 Jul 2022 00:46:56 -0500 Subject: [PATCH 294/519] Fix comment spacing on non-FR --- web/components/feed/feed-comments.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index d5accef0..f4c6eb74 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -70,7 +70,7 @@ export function FeedCommentThread(props: { if (showReply && inputRef) inputRef.focus() }, [inputRef, showReply]) return ( - <div className={'w-full flex-col pr-1'}> + <Col className={'w-full gap-3 pr-1'}> <span className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" aria-hidden="true" @@ -86,7 +86,7 @@ export function FeedCommentThread(props: { scrollAndOpenReplyInput={scrollAndOpenReplyInput} /> {showReply && ( - <div className={'-pb-2 ml-6 flex flex-col pt-5'}> + <Col className={'-pb-2 ml-6'}> <span className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" aria-hidden="true" @@ -106,9 +106,9 @@ export function FeedCommentThread(props: { setReplyToUsername('') }} /> - </div> + </Col> )} - </div> + </Col> ) } From 21c08aed304c028d6038a3e23d8f9af957c91bce Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 21 Jul 2022 00:50:28 -0500 Subject: [PATCH 295/519] Move "Send M$" lower in sidebar More list. --- web/components/nav/sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index ff740540..34bc2135 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -62,9 +62,9 @@ function getMoreNavigation(user?: User | null) { } return [ - { name: 'Send M$', href: '/links' }, { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, { @@ -113,8 +113,8 @@ function getMoreMobileNav() { ...(IS_PRIVATE_MANIFOLD ? [] : [ - { name: 'Send M$', href: '/links' }, { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, ]), { name: 'Leaderboards', href: '/leaderboards' }, From 8aa360c853784db32e32a4ec2a34a238013b0116 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 21 Jul 2022 00:52:11 -0500 Subject: [PATCH 296/519] Move leaderboards up in mobile nav --- web/components/nav/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 34bc2135..9486a97b 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -110,6 +110,7 @@ const signedInMobileNavigation = [ function getMoreMobileNav() { return [ + { name: 'Leaderboards', href: '/leaderboards' }, ...(IS_PRIVATE_MANIFOLD ? [] : [ @@ -117,7 +118,6 @@ function getMoreMobileNav() { { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, ]), - { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Sign out', href: '#', From 03858e4a8cafc694a446b33285c1581816ef7be4 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 21 Jul 2022 00:38:26 -0700 Subject: [PATCH 297/519] Make a React context to be the source of truth for authenticated user (#675) * Make a React context to manage the logged in user events * Remove unnecessary new user creation promise machinery * Slight refactoring to auth context code * Improvements in response to James feedback --- web/components/auth-context.tsx | 77 ++++++++++++++++++++++++++++++ web/components/bets-list.tsx | 6 +-- web/components/feed/feed-items.tsx | 4 +- web/hooks/use-algo-feed.ts | 2 +- web/hooks/use-seen-contracts.ts | 8 ++-- web/hooks/use-user.ts | 24 ++-------- web/lib/firebase/tracking.ts | 20 +++----- web/lib/firebase/users.ts | 52 +------------------- web/pages/_app.tsx | 9 ++-- 9 files changed, 106 insertions(+), 96 deletions(-) create mode 100644 web/components/auth-context.tsx diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx new file mode 100644 index 00000000..fcc3de39 --- /dev/null +++ b/web/components/auth-context.tsx @@ -0,0 +1,77 @@ +import { createContext, useEffect } from 'react' +import { User } from 'common/user' +import { onIdTokenChanged } from 'firebase/auth' +import { + auth, + listenForUser, + getUser, + setCachedReferralInfoForUser, +} from 'web/lib/firebase/users' +import { deleteAuthCookies, setAuthCookies } from 'web/lib/firebase/auth' +import { createUser } from 'web/lib/firebase/api' +import { randomString } from 'common/util/random' +import { identifyUser, setUserProperty } from 'web/lib/service/analytics' +import { useStateCheckEquality } from 'web/hooks/use-state-check-equality' + +// Either we haven't looked up the logged in user yet (undefined), or we know +// the user is not logged in (null), or we know the user is logged in (User). +type AuthUser = undefined | null | User + +const CACHED_USER_KEY = 'CACHED_USER_KEY' + +const ensureDeviceToken = () => { + let deviceToken = localStorage.getItem('device-token') + if (!deviceToken) { + deviceToken = randomString() + localStorage.setItem('device-token', deviceToken) + } + return deviceToken +} + +export const AuthContext = createContext<AuthUser>(null) + +export function AuthProvider({ children }: any) { + const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(undefined) + + useEffect(() => { + const cachedUser = localStorage.getItem(CACHED_USER_KEY) + setAuthUser(cachedUser && JSON.parse(cachedUser)) + }, [setAuthUser]) + + useEffect(() => { + return onIdTokenChanged(auth, async (fbUser) => { + if (fbUser) { + let user = await getUser(fbUser.uid) + if (!user) { + const deviceToken = ensureDeviceToken() + user = (await createUser({ deviceToken })) as User + } + setAuthUser(user) + // Persist to local storage, to reduce login blink next time. + // Note: Cap on localStorage size is ~5mb + localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user)) + setCachedReferralInfoForUser(user) + setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) + } else { + // User logged out; reset to null + setAuthUser(null) + localStorage.removeItem(CACHED_USER_KEY) + deleteAuthCookies() + } + }) + }, [setAuthUser]) + + const authUserId = authUser?.id + const authUsername = authUser?.username + useEffect(() => { + if (authUserId && authUsername) { + identifyUser(authUserId) + setUserProperty('username', authUsername) + return listenForUser(authUserId, setAuthUser) + } + }, [authUserId, authUsername, setAuthUser]) + + return ( + <AuthContext.Provider value={authUser}>{children}</AuthContext.Provider> + ) +} diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 2114ec2b..a306a020 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -78,10 +78,10 @@ export function BetsList(props: { const getTime = useTimeSinceFirstRender() useEffect(() => { - if (bets && contractsById) { - trackLatency('portfolio', getTime()) + if (bets && contractsById && signedInUser) { + trackLatency(signedInUser.id, 'portfolio', getTime()) } - }, [bets, contractsById, getTime]) + }, [signedInUser, bets, contractsById, getTime]) if (!bets || !contractsById) { return <LoadingIndicator /> diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index ff5f5440..ea8302b8 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -23,6 +23,7 @@ import BetRow from '../bet-row' import { Avatar } from '../avatar' import { ActivityItem } from './activity-items' import { useSaveSeenContract } from 'web/hooks/use-seen-contracts' +import { useUser } from 'web/hooks/use-user' import { trackClick } from 'web/lib/firebase/tracking' import { DAY_MS } from 'common/util/time' import NewContractBadge from '../new-contract-badge' @@ -118,6 +119,7 @@ export function FeedQuestion(props: { const { volumeLabel } = contractMetrics(contract) const isBinary = outcomeType === 'BINARY' const isNew = createdTime > Date.now() - DAY_MS && !isResolved + const user = useUser() return ( <div className={'flex gap-2'}> @@ -149,7 +151,7 @@ export function FeedQuestion(props: { href={ props.contractPath ? props.contractPath : contractPath(contract) } - onClick={() => trackClick(contract.id)} + onClick={() => user && trackClick(user.id, contract.id)} className="text-lg text-indigo-700 sm:text-xl" > {question} diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index fde50e80..e195936f 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -25,7 +25,7 @@ export const useAlgoFeed = ( getDefaultFeed().then((feed) => setAllFeed(feed)) } else setAllFeed(feed) - trackLatency('feed', getTime()) + trackLatency(user.id, 'feed', getTime()) console.log('"all" feed load time', getTime()) }) diff --git a/web/hooks/use-seen-contracts.ts b/web/hooks/use-seen-contracts.ts index 501e7b0c..d21ca84c 100644 --- a/web/hooks/use-seen-contracts.ts +++ b/web/hooks/use-seen-contracts.ts @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import { Contract } from 'common/contract' import { trackView } from 'web/lib/firebase/tracking' import { useIsVisible } from './use-is-visible' +import { useUser } from './use-user' export const useSeenContracts = () => { const [seenContracts, setSeenContracts] = useState<{ @@ -21,18 +22,19 @@ export const useSaveSeenContract = ( contract: Contract ) => { const isVisible = useIsVisible(elem) + const user = useUser() useEffect(() => { - if (isVisible) { + if (isVisible && user) { const newSeenContracts = { ...getSeenContracts(), [contract.id]: Date.now(), } localStorage.setItem(key, JSON.stringify(newSeenContracts)) - trackView(contract.id) + trackView(user.id, contract.id) } - }, [isVisible, contract]) + }, [isVisible, user, contract]) } const key = 'feed-seen-contracts' diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index e04a69ca..4c492d6c 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { QueryClient } from 'react-query' @@ -6,32 +6,14 @@ import { doc, DocumentData } from 'firebase/firestore' import { PrivateUser } from 'common/user' import { getUser, - listenForLogin, listenForPrivateUser, - listenForUser, User, users, } from 'web/lib/firebase/users' -import { useStateCheckEquality } from './use-state-check-equality' -import { identifyUser, setUserProperty } from 'web/lib/service/analytics' +import { AuthContext } from 'web/components/auth-context' export const useUser = () => { - const [user, setUser] = useStateCheckEquality<User | null | undefined>( - undefined - ) - - useEffect(() => listenForLogin(setUser), [setUser]) - - useEffect(() => { - if (user) { - identifyUser(user.id) - setUserProperty('username', user.username) - - return listenForUser(user.id, setUser) - } - }, [user, setUser]) - - return user + return useContext(AuthContext) } export const usePrivateUser = (userId?: string) => { diff --git a/web/lib/firebase/tracking.ts b/web/lib/firebase/tracking.ts index f6ad3aa8..d1828e01 100644 --- a/web/lib/firebase/tracking.ts +++ b/web/lib/firebase/tracking.ts @@ -2,16 +2,9 @@ import { doc, collection, setDoc } from 'firebase/firestore' import { db } from './init' import { ClickEvent, LatencyEvent, View } from 'common/tracking' -import { listenForLogin, User } from './users' -let user: User | null = null -if (typeof window !== 'undefined') { - listenForLogin((u) => (user = u)) -} - -export async function trackView(contractId: string) { - if (!user) return - const ref = doc(collection(db, 'private-users', user.id, 'views')) +export async function trackView(userId: string, contractId: string) { + const ref = doc(collection(db, 'private-users', userId, 'views')) const view: View = { contractId, @@ -21,9 +14,8 @@ export async function trackView(contractId: string) { return await setDoc(ref, view) } -export async function trackClick(contractId: string) { - if (!user) return - const ref = doc(collection(db, 'private-users', user.id, 'events')) +export async function trackClick(userId: string, contractId: string) { + const ref = doc(collection(db, 'private-users', userId, 'events')) const clickEvent: ClickEvent = { type: 'click', @@ -35,11 +27,11 @@ export async function trackClick(contractId: string) { } export async function trackLatency( + userId: string, type: 'feed' | 'portfolio', latency: number ) { - if (!user) return - const ref = doc(collection(db, 'private-users', user.id, 'latency')) + const ref = doc(collection(db, 'private-users', userId, 'latency')) const latencyEvent: LatencyEvent = { type, diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 89852851..481f86de 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -15,15 +15,10 @@ import { } from 'firebase/firestore' import { getAuth } from 'firebase/auth' import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage' -import { - onIdTokenChanged, - GoogleAuthProvider, - signInWithPopup, -} from 'firebase/auth' +import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth' import { zip } from 'lodash' import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' -import { createUser } from './api' import { coll, getValue, @@ -37,13 +32,11 @@ import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' import { addUserToGroupViaId } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' -import { randomString } from 'common/util/random' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' dayjs.extend(utc) import { track } from '@amplitude/analytics-browser' -import { deleteAuthCookies, setAuthCookies } from './auth' export const users = coll<User>('users') export const privateUsers = coll<PrivateUser>('private-users') @@ -97,7 +90,6 @@ export function listenForPrivateUser( return listenForValue<PrivateUser>(userRef, setPrivateUser) } -const CACHED_USER_KEY = 'CACHED_USER_KEY' const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY' @@ -130,7 +122,7 @@ export function writeReferralInfo( local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId) } -async function setCachedReferralInfoForUser(user: User | null) { +export async function setCachedReferralInfoForUser(user: User | null) { if (!user || user.referredByUserId) return // if the user wasn't created in the last minute, don't bother const now = dayjs().utc() @@ -181,46 +173,6 @@ async function setCachedReferralInfoForUser(user: User | null) { local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } -// used to avoid weird race condition -let createUserPromise: Promise<User> | undefined = undefined - -export function listenForLogin(onUser: (user: User | null) => void) { - const local = safeLocalStorage() - const cachedUser = local?.getItem(CACHED_USER_KEY) - onUser(cachedUser && JSON.parse(cachedUser)) - - return onIdTokenChanged(auth, async (fbUser) => { - if (fbUser) { - let user: User | null = await getUser(fbUser.uid) - if (!user) { - if (createUserPromise == null) { - const local = safeLocalStorage() - let deviceToken = local?.getItem('device-token') - if (!deviceToken) { - deviceToken = randomString() - local?.setItem('device-token', deviceToken) - } - createUserPromise = createUser({ deviceToken }).then((r) => r as User) - } - user = await createUserPromise - } - onUser(user) - - // Persist to local storage, to reduce login blink next time. - // Note: Cap on localStorage size is ~5mb - local?.setItem(CACHED_USER_KEY, JSON.stringify(user)) - setCachedReferralInfoForUser(user) - setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) - } else { - // User logged out; reset to null - onUser(null) - createUserPromise = undefined - local?.removeItem(CACHED_USER_KEY) - deleteAuthCookies() - } - }) -} - export async function firebaseLogin() { const provider = new GoogleAuthProvider() return signInWithPopup(auth, provider) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index d081bc9a..52316eb0 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -5,6 +5,7 @@ import Head from 'next/head' import Script from 'next/script' import { usePreserveScroll } from 'web/hooks/use-preserve-scroll' import { QueryClient, QueryClientProvider } from 'react-query' +import { AuthProvider } from 'web/components/auth-context' function firstLine(msg: string) { return msg.replace(/\r?\n.*/s, '') @@ -78,9 +79,11 @@ function MyApp({ Component, pageProps }: AppProps) { /> </Head> - <QueryClientProvider client={queryClient}> - <Component {...pageProps} /> - </QueryClientProvider> + <AuthProvider> + <QueryClientProvider client={queryClient}> + <Component {...pageProps} /> + </QueryClientProvider> + </AuthProvider> </> ) } From 6603effd1bb691dceeb5213cdbaaf425c78cccb9 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 21 Jul 2022 01:16:21 -0700 Subject: [PATCH 298/519] Use https for hotlinked image Editing main from my phone, fingers crossed --- web/public/mtg/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 5fd31966..62849462 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -152,7 +152,7 @@ <details id="addl-options"> <summary> <img - src="http://mythicspoiler.com/images/buttons/ustset.png" + src="https://mythicspoiler.com/images/buttons/ustset.png" style="width: 32px; vertical-align: top" /> Options From 96e9f749d24f717d7571cdbf820a2583d4738caa Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 21 Jul 2022 12:45:47 -0500 Subject: [PATCH 299/519] track search categories --- web/components/contract-search.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 730b113f..8eb7df6e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -22,7 +22,7 @@ import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useUser } from 'web/hooks/use-user' import { useFollows } from 'web/hooks/use-follows' -import { trackCallback } from 'web/lib/service/analytics' +import { track, trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { Group, NEW_USER_GROUP_SLUGS } from 'common/group' @@ -111,8 +111,14 @@ export function ContractSearch(props: { querySortOptions?.defaultFilter ?? 'open' ) const pillsEnabled = !additionalFilter + const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) + const selectFilter = (pill: string | undefined) => () => { + setPillFilter(pill) + track('select search category', { category: pill ?? 'all' }) + } + const { filters, numericFilters } = useMemo(() => { let filters = [ filter === 'open' ? 'isResolved:false' : '', @@ -191,7 +197,7 @@ export function ContractSearch(props: { className="!select !select-bordered" value={filter} onChange={(e) => setFilter(e.target.value as filter)} - onBlur={trackCallback('select search filter')} + onBlur={trackCallback('select search filter', { filter })} > <option value="open">Open</option> <option value="closed">Closed</option> @@ -204,7 +210,7 @@ export function ContractSearch(props: { classNames={{ select: '!select !select-bordered', }} - onBlur={trackCallback('select search sort')} + onBlur={trackCallback('select search sort', { sort })} /> )} <Configure @@ -222,14 +228,14 @@ export function ContractSearch(props: { <PillButton key={'all'} selected={pillFilter === undefined} - onSelect={() => setPillFilter(undefined)} + onSelect={selectFilter(undefined)} > All </PillButton> <PillButton key={'personal'} selected={pillFilter === 'personal'} - onSelect={() => setPillFilter('personal')} + onSelect={selectFilter('personal')} > For you </PillButton> @@ -237,7 +243,7 @@ export function ContractSearch(props: { <PillButton key={'your-bets'} selected={pillFilter === 'your-bets'} - onSelect={() => setPillFilter('your-bets')} + onSelect={selectFilter('your-bets')} > Your bets </PillButton> @@ -247,7 +253,7 @@ export function ContractSearch(props: { <PillButton key={slug} selected={pillFilter === slug} - onSelect={() => setPillFilter(slug)} + onSelect={selectFilter(slug)} > {name} </PillButton> From 91bec9c9964056b5894cc709e14454cd5534f1df Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:43:10 -0500 Subject: [PATCH 300/519] Referrals page (#676) * Referrals page added to sidebar; useSaveReferral; InfoBox * text color * eslint * migrate useUsers hook to react-query (#674) * Remove bet button from free response comments * Make answer replies more closely spaced together * Host Ida and Alex's MTG Guesser game (#656) * Copy over code from Mtg Guesser * Run Prettier * CSS Tweaks: Hover feedback, button positioning * Hide all but counterspell & burn, for now * Move to /mtg directory * Fix prettierignore * smaller jsons (#673) limited burn to only red cards and also added limited json files to only have fields needed to play * Add Ida's tweak to card position Co-authored-by: marsteralex <bob.masteralex@gmail.com> * Adjust card positioning * Make the select screen index.html * Remove other guessing games * Remove alternate versions; add Alex's email * Remove unused jsons * Small FR comments tweaks * Spacing tweak * Changing manalinks table UI (#665) From table to card view * Fix comment spacing on non-FR * Move "Send M$" lower in sidebar More list. * Move leaderboards up in mobile nav * eslint * prettier Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com> Co-authored-by: James Grugett <jahooma@gmail.com> Co-authored-by: Austin Chen <akrolsmir@gmail.com> Co-authored-by: marsteralex <bob.masteralex@gmail.com> Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com> --- web/components/info-box.tsx | 30 +++++++++++++ web/components/nav/sidebar.tsx | 2 + web/hooks/use-save-referral.ts | 27 +++++++++++ web/pages/[username]/[contractSlug].tsx | 17 +++---- web/pages/group/[...slugs]/index.tsx | 22 ++++----- web/pages/home.tsx | 3 ++ web/pages/index.tsx | 4 ++ web/pages/referrals.tsx | 57 ++++++++++++++++++++++++ web/public/logo-flapping-with-money.gif | Bin 0 -> 293878 bytes 9 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 web/components/info-box.tsx create mode 100644 web/hooks/use-save-referral.ts create mode 100644 web/pages/referrals.tsx create mode 100644 web/public/logo-flapping-with-money.gif diff --git a/web/components/info-box.tsx b/web/components/info-box.tsx new file mode 100644 index 00000000..34f65089 --- /dev/null +++ b/web/components/info-box.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx' +import { InformationCircleIcon } from '@heroicons/react/solid' + +import { Linkify } from './linkify' + +export function InfoBox(props: { + title: string + text: string + className?: string +}) { + const { title, text, className } = props + return ( + <div className={clsx('rounded-md bg-gray-50 p-4', className)}> + <div className="flex"> + <div className="flex-shrink-0"> + <InformationCircleIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </div> + <div className="ml-3"> + <h3 className="text-sm font-medium text-black">{title}</h3> + <div className="mt-2 text-sm text-black"> + <Linkify text={text} /> + </div> + </div> + </div> + </div> + ) +} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 9486a97b..b7117a20 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -63,6 +63,7 @@ function getMoreNavigation(user?: User | null) { return [ { name: 'Leaderboards', href: '/leaderboards' }, + { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, @@ -114,6 +115,7 @@ function getMoreMobileNav() { ...(IS_PRIVATE_MANIFOLD ? [] : [ + { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, diff --git a/web/hooks/use-save-referral.ts b/web/hooks/use-save-referral.ts new file mode 100644 index 00000000..788268b0 --- /dev/null +++ b/web/hooks/use-save-referral.ts @@ -0,0 +1,27 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +import { User, writeReferralInfo } from 'web/lib/firebase/users' + +export const useSaveReferral = ( + user?: User | null, + options?: { + defaultReferrer?: string + contractId?: string + groupId?: string + } +) => { + const router = useRouter() + + useEffect(() => { + const { referrer } = router.query as { + referrer?: string + } + + const actualReferrer = referrer || options?.defaultReferrer + + if (!user && router.isReady && actualReferrer) { + writeReferralInfo(actualReferrer, options?.contractId, options?.groupId) + } + }, [user, router, options]) +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 17453770..11d9af9c 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -10,7 +10,7 @@ import { useUser } from 'web/hooks/use-user' import { ResolutionPanel } from 'web/components/resolution-panel' import { Title } from 'web/components/title' import { Spacer } from 'web/components/layout/spacer' -import { listUsers, User, writeReferralInfo } from 'web/lib/firebase/users' +import { listUsers, User } from 'web/lib/firebase/users' import { Contract, getContractFromSlug, @@ -43,9 +43,9 @@ import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' -import { useRouter } from 'next/router' import { useLiquidity } from 'web/hooks/use-liquidity' import { richTextToString } from 'common/util/parse' +import { useSaveReferral } from 'web/hooks/use-save-referral' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -157,15 +157,10 @@ export function ContractPageContent( const ogCardProps = getOpenGraphProps(contract) - const router = useRouter() - - useEffect(() => { - const { referrer } = router.query as { - referrer?: string - } - if (!user && router.isReady) - writeReferralInfo(contract.creatorUsername, contract.id, referrer) - }, [user, contract, router]) + useSaveReferral(user, { + defaultReferrer: contract.creatorUsername, + contractId: contract.id, + }) const rightSidebar = hasSidePanel ? ( <Col className="gap-4"> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 0d38580c..90f39e83 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -14,12 +14,7 @@ import { } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' -import { - firebaseLogin, - getUser, - User, - writeReferralInfo, -} from 'web/lib/firebase/users' +import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' @@ -34,7 +29,7 @@ import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { CreateQuestionButton } from 'web/components/create-question-button' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' @@ -53,6 +48,7 @@ import { searchInAny } from 'common/util/parse' import { useWindowSize } from 'web/hooks/use-window-size' import { CopyLinkButton } from 'web/components/copy-link-button' import { ENV_CONFIG } from 'common/envs/constants' +import { useSaveReferral } from 'web/hooks/use-save-referral' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -155,13 +151,11 @@ export default function GroupPage(props: { const messages = useCommentsOnGroup(group?.id) const user = useUser() - useEffect(() => { - const { referrer } = router.query as { - referrer?: string - } - if (!user && router.isReady) - writeReferralInfo(creator.username, undefined, referrer, group?.id) - }, [user, creator, group, router]) + + useSaveReferral(user, { + defaultReferrer: creator.username, + groupId: group?.id, + }) const { width } = useWindowSize() const chatDisabled = !group || group.chatDisabled diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 53bb6ec9..61003895 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -12,6 +12,7 @@ import { getContractFromSlug } from 'web/lib/firebase/contracts' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' +import { useSaveReferral } from 'web/hooks/use-save-referral' export const getServerSideProps = redirectIfLoggedOut('/') @@ -21,6 +22,8 @@ const Home = () => { const router = useRouter() useTracking('view home') + useSaveReferral() + return ( <> <Page suspend={!!contract}> diff --git a/web/pages/index.tsx b/web/pages/index.tsx index d9ff7f51..fd5cf382 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -7,6 +7,7 @@ import { LandingPagePanel } from 'web/components/landing-page-panel' import { Col } from 'web/components/layout/col' import { ManifoldLogo } from 'web/components/nav/manifold-logo' import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth' +import { useSaveReferral } from 'web/hooks/use-save-referral' export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { // These hardcoded markets will be shown in the frontpage for signed-out users: @@ -32,6 +33,9 @@ export default function Home(props: { hotContracts: Contract[] }) { // on this page and they log in -- in the future we will make some cleaner way const user = useUser() const router = useRouter() + + useSaveReferral() + useEffect(() => { if (user != null) { router.replace('/home') diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx new file mode 100644 index 00000000..c879afaa --- /dev/null +++ b/web/pages/referrals.tsx @@ -0,0 +1,57 @@ +import { Col } from 'web/components/layout/col' +import { SEO } from 'web/components/SEO' +import { Title } from 'web/components/title' +import { useUser } from 'web/hooks/use-user' +import { Page } from 'web/components/page' +import { useTracking } from 'web/hooks/use-tracking' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' +import { REFERRAL_AMOUNT } from 'common/user' +import { CopyLinkButton } from 'web/components/copy-link-button' +import { ENV_CONFIG } from 'common/envs/constants' +import { InfoBox } from 'web/components/info-box' + +export const getServerSideProps = redirectIfLoggedOut('/') + +export default function ReferralsPage() { + const user = useUser() + + useTracking('view referrals') + + const url = `https://${ENV_CONFIG.domain}?referrer=${user?.username}` + + return ( + <Page> + <SEO title="Referrals" description="" url="/add-funds" /> + + <Col className="items-center"> + <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md"> + <Title className="!mt-0" text="Referrals" /> + <img + className="mb-6 block -scale-x-100 self-center" + src="/logo-flapping-with-money.gif" + width={200} + height={200} + /> + + <div className={'mb-4'}> + Invite new users to Manifold and get M${REFERRAL_AMOUNT} if they + sign up! + </div> + + <CopyLinkButton + url={url} + tracking="copy referral link" + buttonClassName="btn-md rounded-l-none" + toastClassName={'-left-28 mt-1'} + /> + + <InfoBox + title="FYI" + className="mt-4 max-w-md" + text="You can also earn the referral bonus from sharing the link to any market or group you've created!" + /> + </Col> + </Col> + </Page> + ) +} diff --git a/web/public/logo-flapping-with-money.gif b/web/public/logo-flapping-with-money.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ef936a42bdada3f461bf31a43179a059aab2b28 GIT binary patch literal 293878 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⁡Rp`98DR|q#0YBa9?!G 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 literal 0 HcmV?d00001 From daca6ef482ee1f82dea1d30e6d6cf03ae7742965 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 21 Jul 2022 15:33:21 -0600 Subject: [PATCH 301/519] Bonus=>10 --- common/numeric-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/numeric-constants.ts b/common/numeric-constants.ts index 46885668..f399aa5a 100644 --- a/common/numeric-constants.ts +++ b/common/numeric-constants.ts @@ -3,4 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005 export const NUMERIC_GRAPH_COLOR = '#5fa5f9' export const NUMERIC_TEXT_COLOR = 'text-blue-500' -export const UNIQUE_BETTOR_BONUS_AMOUNT = 5 +export const UNIQUE_BETTOR_BONUS_AMOUNT = 10 From 80b27fdf6ebecd6e22ab88fe02d1a9cace72cede Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 21 Jul 2022 17:23:55 -0500 Subject: [PATCH 302/519] Put leadeboards back in sidebar --- web/components/nav/sidebar.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index b7117a20..e6ce8575 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -40,6 +40,8 @@ function getNavigation() { icon: NotificationsIcon, }, + { name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon }, + ...(IS_PRIVATE_MANIFOLD ? [] : [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]), @@ -53,7 +55,6 @@ function getMoreNavigation(user?: User | null) { if (!user) { return [ - { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Charity', href: '/charity' }, { name: 'Blog', href: 'https://news.manifold.markets' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, @@ -62,7 +63,6 @@ function getMoreNavigation(user?: User | null) { } return [ - { name: 'Leaderboards', href: '/leaderboards' }, { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, @@ -79,7 +79,6 @@ function getMoreNavigation(user?: User | null) { const signedOutNavigation = [ { name: 'Home', href: '/home', icon: HomeIcon }, { name: 'Explore', href: '/markets', icon: SearchIcon }, - { name: 'Charity', href: '/charity', icon: HeartIcon }, { name: 'About', href: 'https://docs.manifold.markets/$how-to', @@ -99,6 +98,7 @@ const signedOutMobileNavigation = [ ] const signedInMobileNavigation = [ + { name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon }, ...(IS_PRIVATE_MANIFOLD ? [] : [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]), @@ -111,7 +111,6 @@ const signedInMobileNavigation = [ function getMoreMobileNav() { return [ - { name: 'Leaderboards', href: '/leaderboards' }, ...(IS_PRIVATE_MANIFOLD ? [] : [ From cded3f50ffc0557ef6b28f77259ef9bc2a14a2d3 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 21 Jul 2022 18:17:02 -0500 Subject: [PATCH 303/519] "question" => "market" (controversial!) --- web/components/create-question-button.tsx | 2 +- web/lib/firebase/groups.ts | 2 +- web/pages/create.tsx | 3 +++ web/pages/group/[...slugs]/index.tsx | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index f2371d11..277816fa 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -21,7 +21,7 @@ export const CreateQuestionButton = (props: { {user ? ( <Link href={`/create${query ? query : ''}`} passHref> <button className={clsx(gradient, createButtonStyle)}> - {overrideText ? overrideText : 'Create a question'} + {overrideText ? overrideText : 'Create a market'} </button> </Link> ) : ( diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index fc028642..5a031ca7 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -24,7 +24,7 @@ export function groupPath( groupSlug: string, subpath?: | 'edit' - | 'questions' + | 'markets' | 'about' | typeof GROUP_CHAT_SLUG | 'leaderboards' diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 78ad8d19..1271730f 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -29,6 +29,7 @@ import { User } from 'common/user' import { TextEditor, useTextEditor } from 'web/components/editor' import { Checkbox } from 'web/components/checkbox' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' +import { Title } from 'web/components/title' export const getServerSideProps = redirectIfLoggedOut('/') @@ -64,6 +65,8 @@ export default function Create() { <Page> <div className="mx-auto w-full max-w-2xl"> <div className="rounded-lg px-6 py-4 sm:py-0"> + <Title className="!mt-0" text="Create a market" /> + <form> <div className="form-control w-full"> <label className="label"> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 90f39e83..06f043e7 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -109,7 +109,7 @@ export async function getStaticPaths() { const groupSubpages = [ undefined, GROUP_CHAT_SLUG, - 'questions', + 'markets', 'leaderboards', 'about', ] as const @@ -226,9 +226,9 @@ export default function GroupPage(props: { }, ]), { - title: 'Questions', + title: 'Markets', content: questionsTab, - href: groupPath(group.slug, 'questions'), + href: groupPath(group.slug, 'markets'), }, { title: 'Leaderboards', From 4b4734531fce5b983c18ebb1124c2f7ce7e5b4bb Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 21 Jul 2022 17:08:09 -0700 Subject: [PATCH 304/519] refactor createNotif - put ?: args in object (#681) --- functions/src/create-notification.ts | 24 +++++++++++++------ functions/src/market-close-notifications.ts | 2 +- functions/src/on-create-answer.ts | 2 +- functions/src/on-create-bet.ts | 11 ++++----- .../src/on-create-comment-on-contract.ts | 6 ++--- functions/src/on-create-contract.ts | 2 +- functions/src/on-create-group.ts | 10 ++++---- .../src/on-create-liquidity-provision.ts | 2 +- functions/src/on-follow-user.ts | 4 +--- functions/src/on-update-contract.ts | 4 ++-- 10 files changed, 36 insertions(+), 31 deletions(-) diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index bf2dd28a..7cc05760 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -29,12 +29,22 @@ export const createNotification = async ( sourceUser: User, idempotencyKey: string, sourceText: string, - sourceContract?: Contract, - relatedSourceType?: notification_source_types, - relatedUserId?: string, - sourceSlug?: string, - sourceTitle?: string + miscData?: { + contract?: Contract + relatedSourceType?: notification_source_types + relatedUserId?: string + slug?: string + title?: string + } ) => { + const { + contract: sourceContract, + relatedSourceType, + relatedUserId, + slug, + title, + } = miscData ?? {} + const shouldGetNotification = ( userId: string, userToReasonTexts: user_to_reason_texts @@ -70,8 +80,8 @@ export const createNotification = async ( sourceContractCreatorUsername: sourceContract?.creatorUsername, sourceContractTitle: sourceContract?.question, sourceContractSlug: sourceContract?.slug, - sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, - sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, + sourceSlug: slug ? slug : sourceContract?.slug, + sourceTitle: title ? title : sourceContract?.question, } await notificationRef.set(removeUndefinedProps(notification)) }) diff --git a/functions/src/market-close-notifications.ts b/functions/src/market-close-notifications.ts index ee9952bf..f31674a1 100644 --- a/functions/src/market-close-notifications.ts +++ b/functions/src/market-close-notifications.ts @@ -64,7 +64,7 @@ async function sendMarketCloseEmails() { user, 'closed' + contract.id.slice(6, contract.id.length), contract.closeTime?.toString() ?? new Date().toString(), - contract + { contract } ) } } diff --git a/functions/src/on-create-answer.ts b/functions/src/on-create-answer.ts index 78fd1399..af4690b0 100644 --- a/functions/src/on-create-answer.ts +++ b/functions/src/on-create-answer.ts @@ -28,6 +28,6 @@ export const onCreateAnswer = functions.firestore answerCreator, eventId, answer.text, - contract + { contract } ) }) diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index fc2e0053..4e10875e 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -134,12 +134,11 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( fromUser, eventId + '-bonus', result.txn.amount + '', - contract, - undefined, - // No need to set the user id, we'll use the contract creator id - undefined, - contract.slug, - contract.question + { + contract, + slug: contract.slug, + title: contract.question, + } ) } } diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index f7839b44..8d841ac0 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -68,7 +68,7 @@ export const onCreateCommentOnContract = functions ? 'answer' : undefined - const relatedUser = comment.replyToCommentId + const relatedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId @@ -79,9 +79,7 @@ export const onCreateCommentOnContract = functions commentCreator, eventId, comment.text, - contract, - relatedSourceType, - relatedUser + { contract, relatedSourceType, relatedUserId } ) const recipientUserIds = uniq([ diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 28682793..a43beda7 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -21,6 +21,6 @@ export const onCreateContract = functions.firestore contractCreator, eventId, richTextToString(contract.description as JSONContent), - contract + { contract } ) }) diff --git a/functions/src/on-create-group.ts b/functions/src/on-create-group.ts index 1d041c04..47618d7a 100644 --- a/functions/src/on-create-group.ts +++ b/functions/src/on-create-group.ts @@ -20,11 +20,11 @@ export const onCreateGroup = functions.firestore groupCreator, eventId, group.about, - undefined, - undefined, - memberId, - group.slug, - group.name + { + relatedUserId: memberId, + slug: group.slug, + title: group.name, + } ) } }) diff --git a/functions/src/on-create-liquidity-provision.ts b/functions/src/on-create-liquidity-provision.ts index d55b2be4..ba17f3e7 100644 --- a/functions/src/on-create-liquidity-provision.ts +++ b/functions/src/on-create-liquidity-provision.ts @@ -26,6 +26,6 @@ export const onCreateLiquidityProvision = functions.firestore liquidityProvider, eventId, liquidity.amount.toString(), - contract + { contract } ) }) diff --git a/functions/src/on-follow-user.ts b/functions/src/on-follow-user.ts index ad85f4d3..9a6e6dce 100644 --- a/functions/src/on-follow-user.ts +++ b/functions/src/on-follow-user.ts @@ -30,9 +30,7 @@ export const onFollowUser = functions.firestore followingUser, eventId, '', - undefined, - undefined, - follow.userId + { relatedUserId: follow.userId } ) }) diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index 4674bd82..2042f726 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -36,7 +36,7 @@ export const onUpdateContract = functions.firestore contractUpdater, eventId, resolutionText, - contract + { contract } ) } else if ( previousValue.closeTime !== contract.closeTime || @@ -62,7 +62,7 @@ export const onUpdateContract = functions.firestore contractUpdater, eventId, sourceText, - contract + { contract } ) } }) From 7474c0a0fd8c77b87caffc290e5526c2792361a8 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:22:17 -0700 Subject: [PATCH 305/519] Inga/manalinks pagination bug (#678) * manalink pagination fix * also fixed new manalink timing out bug --- web/components/manalink-card.tsx | 106 +++++++++--------- .../manalinks/create-links-button.tsx | 2 +- web/pages/links.tsx | 55 ++++++--- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index b49e1621..51880f5d 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -27,10 +27,10 @@ export function ManalinkCard(props: { const { expiresTime, maxUses, uses, amount, message } = info return ( <Col> - <div + <Col className={clsx( className, - 'min-h-20 group flex flex-col rounded-xl bg-gradient-to-br shadow-lg transition-all', + 'min-h-20 group rounded-lg bg-gradient-to-br drop-shadow-sm transition-all', getManalinkGradient(info.amount) )} > @@ -54,20 +54,18 @@ export function ManalinkCard(props: { )} src="/logo-white.svg" /> - <Row className="rounded-b-xl bg-white p-4"> - <Col> - <div - className={clsx( - 'mb-1 text-xl text-indigo-500', - getManalinkAmountColor(amount) - )} - > - {formatMoney(amount)} - </div> - <div>{message}</div> - </Col> + <Row className="rounded-b-lg bg-white p-4"> + <div + className={clsx( + 'mb-1 text-xl text-indigo-500', + getManalinkAmountColor(amount) + )} + > + {formatMoney(amount)} + </div> </Row> - </div> + </Col> + <div className="text-md mt-2 mb-4 text-gray-500">{message}</div> </Col> ) } @@ -79,48 +77,48 @@ export function ManalinkCardFromView(props: { }) { const { className, link, highlightedSlug } = props const { message, amount, expiresTime, maxUses, claims } = link - const [details, setDetails] = useState(false) + const [showDetails, setShowDetails] = useState(false) return ( - <Col - className={clsx( - 'group z-10 rounded-lg drop-shadow-sm transition-all hover:drop-shadow-lg', - className, - link.slug === highlightedSlug ? 'animate-pulse' : '' - )} - > - <div + <Col> + <Col className={clsx( - 'relative flex flex-col rounded-t-lg bg-gradient-to-br transition-all', - getManalinkGradient(link.amount) + 'group z-10 rounded-lg drop-shadow-sm transition-all hover:drop-shadow-lg', + className, + link.slug === highlightedSlug ? 'shadow-md shadow-indigo-400' : '' )} - onClick={() => setDetails(!details)} > - {details && ( - <ClaimsList - className="absolute h-full w-full bg-white opacity-90" - link={link} + <Col + className={clsx( + 'relative rounded-t-lg bg-gradient-to-br transition-all', + getManalinkGradient(link.amount) + )} + onClick={() => setShowDetails(!showDetails)} + > + {showDetails && ( + <ClaimsList + className="absolute h-full w-full bg-white opacity-90" + link={link} + /> + )} + <Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100"> + <div> + {maxUses != null + ? `${maxUses - claims.length}/${maxUses} uses left` + : `Unlimited use`} + </div> + <div> + {expiresTime != null + ? `Expires ${fromNow(expiresTime)}` + : 'Never expires'} + </div> + </Col> + <img + className={clsx('my-auto block w-1/3 select-none self-center py-3')} + src="/logo-white.svg" /> - )} - <Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100"> - <div> - {maxUses != null - ? `${maxUses - claims.length}/${maxUses} uses left` - : `Unlimited use`} - </div> - <div> - {expiresTime != null - ? `Expires ${fromNow(expiresTime)}` - : 'Never expires'} - </div> </Col> - <img - className={clsx('my-auto block w-1/3 select-none self-center py-3')} - src="/logo-white.svg" - /> - </div> - <Col className="w-full rounded-b-lg bg-white px-4 py-2 text-lg"> - <Row className="relative gap-1"> + <Row className="relative w-full gap-1 rounded-b-lg bg-white px-4 py-2 text-lg"> <div className={clsx( 'my-auto mb-1 w-full', @@ -138,10 +136,10 @@ export function ManalinkCardFromView(props: { copyPayload={getManalinkUrl(link.slug)} /> <button - onClick={() => setDetails(!details)} + onClick={() => setShowDetails(!showDetails)} className={clsx( contractDetailsButtonClassName, - details + showDetails ? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600' : '' )} @@ -149,8 +147,10 @@ export function ManalinkCardFromView(props: { <DotsHorizontalIcon className="h-[24px] w-5" /> </button> </Row> - <div className="my-2 text-xs md:text-sm">{message || '\n\n'}</div> </Col> + <div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm"> + {message || ''} + </div> </Col> ) } diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index 25b51bb2..656aff29 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -37,7 +37,6 @@ export function CreateLinksButton(props: { message: newManalink.message, }) setHighlightedSlug(slug || '') - setTimeout(() => setHighlightedSlug(''), 3700) }} /> </Col> @@ -165,6 +164,7 @@ function CreateManalinkForm(props: { <label className="label">Message</label> <Textarea placeholder={defaultMessage} + maxLength={200} className="input input-bordered resize-none" autoFocus value={newManalink.message} diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 8a2e6767..0f91d70c 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -20,6 +20,7 @@ import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import { ManalinkCardFromView } from 'web/components/manalink-card' import { Pagination } from 'web/components/pagination' +import { Manalink } from 'common/manalink' dayjs.extend(customParseFormat) const LINKS_PER_PAGE = 24 @@ -39,10 +40,6 @@ export default function LinkPage() { (l.maxUses == null || l.claimedUserIds.length < l.maxUses) && (l.expiresTime == null || l.expiresTime > Date.now()) ) - const [page, setPage] = useState(0) - const start = page * LINKS_PER_PAGE - const end = start + LINKS_PER_PAGE - const displayedLinks = unclaimedLinks.slice(start, end) if (user == null) { return null @@ -71,15 +68,43 @@ export default function LinkPage() { don't yet have a Manifold account. </p> <Subtitle text="Your Manalinks" /> + <ManalinksDisplay + unclaimedLinks={unclaimedLinks} + highlightedSlug={highlightedSlug} + /> + </Col> + </Page> + ) +} + +function ManalinksDisplay(props: { + unclaimedLinks: Manalink[] + highlightedSlug: string +}) { + const { unclaimedLinks, highlightedSlug } = props + const [page, setPage] = useState(0) + const start = page * LINKS_PER_PAGE + const end = start + LINKS_PER_PAGE + const displayedLinks = unclaimedLinks.slice(start, end) + + if (unclaimedLinks.length === 0) { + return ( + <p className="text-gray-500"> + You don't have any unclaimed manalinks. Send some more to spread the + wealth! + </p> + ) + } else { + return ( + <> <Col className="grid w-full gap-4 md:grid-cols-2"> - {displayedLinks.map((link) => { - return ( - <ManalinkCardFromView - link={link} - highlightedSlug={highlightedSlug} - /> - ) - })} + {displayedLinks.map((link) => ( + <ManalinkCardFromView + key={link.slug + link.createdTime} + link={link} + highlightedSlug={highlightedSlug} + /> + ))} </Col> <Pagination page={page} @@ -89,9 +114,9 @@ export default function LinkPage() { className="mt-4 bg-transparent" scrollToTop /> - </Col> - </Page> - ) + </> + ) + } } // TODO: either utilize this or get rid of it From ca5ca9b2b8981887407a59072ec37a4cc5c9dfe8 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 21 Jul 2022 21:39:06 -0500 Subject: [PATCH 306/519] Refactor: Move ContractLeaderboard to its own file --- .../contract/contract-leaderboard.tsx | 141 ++++++++++++++++++ web/pages/[username]/[contractSlug].tsx | 140 +---------------- 2 files changed, 147 insertions(+), 134 deletions(-) create mode 100644 web/components/contract/contract-leaderboard.tsx diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx new file mode 100644 index 00000000..0623b6d7 --- /dev/null +++ b/web/components/contract/contract-leaderboard.tsx @@ -0,0 +1,141 @@ +import { Bet } from 'common/bet' +import { Comment } from 'common/comment' +import { resolvedPayout } from 'common/calculate' +import { Contract } from 'common/contract' +import { formatMoney } from 'common/util/format' +import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash' +import { useState, useMemo, useEffect } from 'react' +import { CommentTipMap } from 'web/hooks/use-tip-txns' +import { useUserById } from 'web/hooks/use-user' +import { listUsers, User } from 'web/lib/firebase/users' +import { FeedBet } from '../feed/feed-bets' +import { FeedComment } from '../feed/feed-comments' +import { Spacer } from '../layout/spacer' +import { Leaderboard } from '../leaderboard' +import { Title } from '../title' + +export function ContractLeaderboard(props: { + contract: Contract + bets: Bet[] +}) { + const { contract, bets } = props + const [users, setUsers] = useState<User[]>() + + const { userProfits, top5Ids } = useMemo(() => { + // Create a map of userIds to total profits (including sales) + const openBets = bets.filter((bet) => !bet.isSold && !bet.sale) + const betsByUser = groupBy(openBets, 'userId') + + const userProfits = mapValues(betsByUser, (bets) => + sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) + ) + // Find the 5 users with the most profits + const top5Ids = Object.entries(userProfits) + .sort(([_i1, p1], [_i2, p2]) => p2 - p1) + .filter(([, p]) => p > 0) + .slice(0, 5) + .map(([id]) => id) + return { userProfits, top5Ids } + }, [contract, bets]) + + useEffect(() => { + if (top5Ids.length > 0) { + listUsers(top5Ids).then((users) => { + const sortedUsers = sortBy(users, (user) => -userProfits[user.id]) + setUsers(sortedUsers) + }) + } + }, [userProfits, top5Ids]) + + return users && users.length > 0 ? ( + <Leaderboard + title="🏅 Top bettors" + users={users || []} + columns={[ + { + header: 'Total profit', + renderCell: (user) => formatMoney(userProfits[user.id] || 0), + }, + ]} + className="mt-12 max-w-sm" + /> + ) : null +} + +export function ContractTopTrades(props: { + contract: Contract + bets: Bet[] + comments: Comment[] + tips: CommentTipMap +}) { + const { contract, bets, comments, tips } = props + const commentsById = keyBy(comments, 'id') + const betsById = keyBy(bets, 'id') + + // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit + // Otherwise, we record the profit at resolution time + const profitById: Record<string, number> = {} + for (const bet of bets) { + if (bet.sale) { + const originalBet = betsById[bet.sale.betId] + const profit = bet.sale.amount - originalBet.amount + profitById[bet.id] = profit + profitById[originalBet.id] = profit + } else { + profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount + } + } + + // Now find the betId with the highest profit + const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id + const topBettor = useUserById(betsById[topBetId]?.userId) + + // And also the commentId of the comment with the highest profit + const topCommentId = sortBy( + comments, + (c) => c.betId && -profitById[c.betId] + )[0]?.id + + return ( + <div className="mt-12 max-w-sm"> + {topCommentId && profitById[topCommentId] > 0 && ( + <> + <Title text="💬 Proven correct" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedComment + contract={contract} + comment={commentsById[topCommentId]} + tips={tips[topCommentId]} + betsBySameUser={[betsById[topCommentId]]} + truncate={false} + smallAvatar={false} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {commentsById[topCommentId].userName} made{' '} + {formatMoney(profitById[topCommentId] || 0)}! + </div> + <Spacer h={16} /> + </> + )} + + {/* If they're the same, only show the comment; otherwise show both */} + {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && ( + <> + <Title text="💸 Smartest money" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedBet + contract={contract} + bet={betsById[topBetId]} + hideOutcome={false} + smallAvatar={false} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}! + </div> + </> + )} + </div> + ) +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 11d9af9c..43dd0ad7 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useState } from 'react' import { ArrowLeftIcon } from '@heroicons/react/outline' -import { keyBy, sortBy, groupBy, sumBy, mapValues } from 'lodash' import { useContractWithPreload } from 'web/hooks/use-contract' import { ContractOverview } from 'web/components/contract/contract-overview' @@ -8,9 +7,7 @@ import { BetPanel } from 'web/components/bet-panel' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' import { ResolutionPanel } from 'web/components/resolution-panel' -import { Title } from 'web/components/title' import { Spacer } from 'web/components/layout/spacer' -import { listUsers, User } from 'web/lib/firebase/users' import { Contract, getContractFromSlug, @@ -24,28 +21,26 @@ import { Comment, listAllComments } from 'web/lib/firebase/comments' import Custom404 from '../404' import { AnswersPanel } from 'web/components/answers/answers-panel' import { fromPropz, usePropz } from 'web/hooks/use-propz' -import { Leaderboard } from 'web/components/leaderboard' -import { resolvedPayout } from 'common/calculate' -import { formatMoney } from 'common/util/format' -import { useUserById } from 'web/hooks/use-user' import { ContractTabs } from 'web/components/contract/contract-tabs' import { contractTextDetails } from 'web/components/contract/contract-details' import { useWindowSize } from 'web/hooks/use-window-size' import Confetti from 'react-confetti' import { NumericBetPanel } from '../../components/numeric-bet-panel' import { NumericResolutionPanel } from '../../components/numeric-resolution-panel' -import { FeedComment } from 'web/components/feed/feed-comments' -import { FeedBet } from 'web/components/feed/feed-bets' import { useIsIframe } from 'web/hooks/use-is-iframe' import ContractEmbedPage from '../embed/[username]/[contractSlug]' import { useBets } from 'web/hooks/use-bets' import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' -import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' +import { useTipTxns } from 'web/hooks/use-tip-txns' import { useLiquidity } from 'web/hooks/use-liquidity' import { richTextToString } from 'common/util/parse' import { useSaveReferral } from 'web/hooks/use-save-referral' +import { + ContractLeaderboard, + ContractTopTrades, +} from 'web/components/contract/contract-leaderboard' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -262,129 +257,6 @@ export function ContractPageContent( ) } -function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) { - const { contract, bets } = props - const [users, setUsers] = useState<User[]>() - - const { userProfits, top5Ids } = useMemo(() => { - // Create a map of userIds to total profits (including sales) - const openBets = bets.filter((bet) => !bet.isSold && !bet.sale) - const betsByUser = groupBy(openBets, 'userId') - - const userProfits = mapValues(betsByUser, (bets) => - sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) - ) - // Find the 5 users with the most profits - const top5Ids = Object.entries(userProfits) - .sort(([_i1, p1], [_i2, p2]) => p2 - p1) - .filter(([, p]) => p > 0) - .slice(0, 5) - .map(([id]) => id) - return { userProfits, top5Ids } - }, [contract, bets]) - - useEffect(() => { - if (top5Ids.length > 0) { - listUsers(top5Ids).then((users) => { - const sortedUsers = sortBy(users, (user) => -userProfits[user.id]) - setUsers(sortedUsers) - }) - } - }, [userProfits, top5Ids]) - - return users && users.length > 0 ? ( - <Leaderboard - title="🏅 Top bettors" - users={users || []} - columns={[ - { - header: 'Total profit', - renderCell: (user) => formatMoney(userProfits[user.id] || 0), - }, - ]} - className="mt-12 max-w-sm" - /> - ) : null -} - -function ContractTopTrades(props: { - contract: Contract - bets: Bet[] - comments: Comment[] - tips: CommentTipMap -}) { - const { contract, bets, comments, tips } = props - const commentsById = keyBy(comments, 'id') - const betsById = keyBy(bets, 'id') - - // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit - // Otherwise, we record the profit at resolution time - const profitById: Record<string, number> = {} - for (const bet of bets) { - if (bet.sale) { - const originalBet = betsById[bet.sale.betId] - const profit = bet.sale.amount - originalBet.amount - profitById[bet.id] = profit - profitById[originalBet.id] = profit - } else { - profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount - } - } - - // Now find the betId with the highest profit - const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id - const topBettor = useUserById(betsById[topBetId]?.userId) - - // And also the commentId of the comment with the highest profit - const topCommentId = sortBy( - comments, - (c) => c.betId && -profitById[c.betId] - )[0]?.id - - return ( - <div className="mt-12 max-w-sm"> - {topCommentId && profitById[topCommentId] > 0 && ( - <> - <Title text="💬 Proven correct" className="!mt-0" /> - <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> - <FeedComment - contract={contract} - comment={commentsById[topCommentId]} - tips={tips[topCommentId]} - betsBySameUser={[betsById[topCommentId]]} - truncate={false} - smallAvatar={false} - /> - </div> - <div className="mt-2 text-sm text-gray-500"> - {commentsById[topCommentId].userName} made{' '} - {formatMoney(profitById[topCommentId] || 0)}! - </div> - <Spacer h={16} /> - </> - )} - - {/* If they're the same, only show the comment; otherwise show both */} - {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && ( - <> - <Title text="💸 Smartest money" className="!mt-0" /> - <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> - <FeedBet - contract={contract} - bet={betsById[topBetId]} - hideOutcome={false} - smallAvatar={false} - /> - </div> - <div className="mt-2 text-sm text-gray-500"> - {topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}! - </div> - </> - )} - </div> - ) -} - const getOpenGraphProps = (contract: Contract) => { const { resolution, From 23b704ffe0460422468573fa5c7bbabc1a6afb95 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 21 Jul 2022 21:51:20 -0500 Subject: [PATCH 307/519] Fix excessive bottom margin on chart --- web/components/contract/contract-prob-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index bafb84fe..98440ec8 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -151,7 +151,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { enableGridX={!!width && width >= 800} enableArea areaBaselineValue={isBinary || isLogScale ? 0 : contract.min} - margin={{ top: 20, right: 20, bottom: 65, left: 40 }} + margin={{ top: 20, right: 20, bottom: 25, left: 40 }} animate={false} sliceTooltip={SliceTooltip} /> From 3b953a7c216a9cc57e13d00d0280a89be7fd9c9e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 00:57:56 -0500 Subject: [PATCH 308/519] Range limit orders (#655) * Prototype range limit order UI * Conditionally show YES or NO max payout * Range bet executes both bets immediately. * Validate lowLimitProb < highLimitProb * Show error if low limit is higher than high limit * Update range order UI * Revert "Validate lowLimitProb < highLimitProb" This reverts commit c261fc274360711baf1033f12907971a96b13548. * Revert "Range bet executes both bets immediately." This reverts commit 30b95d75d9ee52ed2f49ae2af3edced161b21aeb. * Buy panel only non-limit orders * Bet choice => outcome * More iterating on range UI * betChoice => outcome * Lighten placeholder text --- common/calculate-cpmm.ts | 1 + common/new-bet.ts | 28 +- web/components/amount-input.tsx | 2 +- web/components/bet-panel.tsx | 511 +++++++++++++++++++++------ web/components/bucket-input.tsx | 5 +- web/components/number-input.tsx | 11 +- web/components/probability-input.tsx | 51 ++- 7 files changed, 487 insertions(+), 122 deletions(-) diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 493b5fa9..b5153355 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -123,6 +123,7 @@ export function calculateCpmmAmountToProb( prob: number, outcome: 'YES' | 'NO' ) { + if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity if (outcome === 'NO') prob = 1 - prob // First, find an upper bound that leads to a more extreme probability than prob. diff --git a/common/new-bet.ts b/common/new-bet.ts index f484b9f7..ea0b011d 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,4 +1,4 @@ -import { sortBy, sumBy } from 'lodash' +import { sortBy, sum, sumBy } from 'lodash' import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' import { @@ -239,6 +239,32 @@ export const getBinaryCpmmBetInfo = ( } } +export const getBinaryBetStats = ( + outcome: 'YES' | 'NO', + betAmount: number, + contract: CPMMBinaryContract | PseudoNumericContract, + limitProb: number, + unfilledBets: LimitBet[] +) => { + const { newBet } = getBinaryCpmmBetInfo( + outcome, + betAmount ?? 0, + contract, + limitProb, + unfilledBets as LimitBet[] + ) + const remainingMatched = + ((newBet.orderAmount ?? 0) - newBet.amount) / + (outcome === 'YES' ? limitProb : 1 - limitProb) + const currentPayout = newBet.shares + remainingMatched + + const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 + + const totalFees = sum(Object.values(newBet.fees)) + + return { currentPayout, currentReturn, totalFees, newBet } +} + export const getNewBinaryDpmBetInfo = ( outcome: 'YES' | 'NO', amount: number, diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index a31957cb..426a9371 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -41,7 +41,7 @@ export function AmountInput(props: { <span className="bg-gray-200 text-sm">{label}</span> <input className={clsx( - 'input input-bordered max-w-[200px] text-lg', + 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400', error && 'input-error', inputClassName )} diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 0cbee7b5..7d2b1e5a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -13,32 +13,27 @@ import { formatPercent, formatWithCommas, } from 'common/util/format' -import { getBinaryCpmmBetInfo } from 'common/new-bet' +import { getBinaryBetStats, getBinaryCpmmBetInfo } from 'common/new-bet' import { User } from 'web/lib/firebase/users' import { Bet, LimitBet } from 'common/bet' import { APIError, placeBet } from 'web/lib/firebase/api' import { sellShares } from 'web/lib/firebase/api' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' -import { BinaryOutcomeLabel } from './outcome-label' +import { BinaryOutcomeLabel, HigherLabel, LowerLabel } from './outcome-label' import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' -import { - getFormattedMappedValue, - getPseudoProbability, -} from 'common/pseudo-numeric' +import { getFormattedMappedValue, getMappedValue } from 'common/pseudo-numeric' import { SellRow } from './sell-row' import { useSaveBinaryShares } from './use-save-binary-shares' import { SignUpPrompt } from './sign-up-prompt' import { isIOS } from 'web/lib/util/device' -import { ProbabilityInput } from './probability-input' +import { ProbabilityOrNumericInput } from './probability-input' import { track } from 'web/lib/service/analytics' -import { removeUndefinedProps } from 'common/util/object' import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBets } from './limit-bets' -import { BucketInput } from './bucket-input' import { PillButton } from './buttons/pill-button' import { YesNoSelector } from './yes-no-selector' @@ -73,12 +68,17 @@ export function BetPanel(props: { setIsLimitOrder={setIsLimitOrder} /> <BuyPanel + hidden={isLimitOrder} + contract={contract} + user={user} + unfilledBets={unfilledBets} + /> + <LimitOrderPanel + hidden={!isLimitOrder} contract={contract} user={user} - isLimitOrder={isLimitOrder} unfilledBets={unfilledBets} /> - <SignUpPrompt /> </Col> {unfilledBets.length > 0 && ( @@ -120,14 +120,20 @@ export function SimpleBetPanel(props: { setIsLimitOrder={setIsLimitOrder} /> <BuyPanel + hidden={isLimitOrder} contract={contract} user={user} unfilledBets={unfilledBets} selected={selected} onBuySuccess={onBetSuccess} - isLimitOrder={isLimitOrder} /> - + <LimitOrderPanel + hidden={!isLimitOrder} + contract={contract} + user={user} + unfilledBets={unfilledBets} + onBuySuccess={onBetSuccess} + /> <SignUpPrompt /> </Col> @@ -142,21 +148,17 @@ function BuyPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract user: User | null | undefined unfilledBets: Bet[] - isLimitOrder?: boolean + hidden: boolean selected?: 'YES' | 'NO' onBuySuccess?: () => void }) { - const { contract, user, unfilledBets, isLimitOrder, selected, onBuySuccess } = - props + const { contract, user, unfilledBets, hidden, selected, onBuySuccess } = props const initialProb = getProbability(contract) const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' - const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected) + const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>(selected) const [betAmount, setBetAmount] = useState<number | undefined>(undefined) - const [limitProb, setLimitProb] = useState<number | undefined>( - Math.round(100 * initialProb) - ) const [error, setError] = useState<string | undefined>() const [isSubmitting, setIsSubmitting] = useState(false) const [wasSubmitted, setWasSubmitted] = useState(false) @@ -171,7 +173,7 @@ function BuyPanel(props: { }, [selected, focusAmountInput]) function onBetChoice(choice: 'YES' | 'NO') { - setBetChoice(choice) + setOutcome(choice) setWasSubmitted(false) focusAmountInput() } @@ -179,29 +181,22 @@ function BuyPanel(props: { function onBetChange(newAmount: number | undefined) { setWasSubmitted(false) setBetAmount(newAmount) - if (!betChoice) { - setBetChoice('YES') + if (!outcome) { + setOutcome('YES') } } async function submitBet() { if (!user || !betAmount) return - if (isLimitOrder && limitProb === undefined) return - - const limitProbScaled = - isLimitOrder && limitProb !== undefined ? limitProb / 100 : undefined setError(undefined) setIsSubmitting(true) - placeBet( - removeUndefinedProps({ - amount: betAmount, - outcome: betChoice, - contractId: contract.id, - limitProb: limitProbScaled, - }) - ) + placeBet({ + outcome, + amount: betAmount, + contractId: contract.id, + }) .then((r) => { console.log('placed bet. Result:', r) setIsSubmitting(false) @@ -225,21 +220,18 @@ function BuyPanel(props: { slug: contract.slug, contractId: contract.id, amount: betAmount, - outcome: betChoice, - isLimitOrder, - limitProb: limitProbScaled, + outcome, + isLimitOrder: false, }) } const betDisabled = isSubmitting || !betAmount || error - const limitProbFrac = (limitProb ?? 0) / 100 - const { newPool, newP, newBet } = getBinaryCpmmBetInfo( - betChoice ?? 'YES', + outcome ?? 'YES', betAmount ?? 0, contract, - isLimitOrder ? limitProbFrac : undefined, + undefined, unfilledBets as LimitBet[] ) @@ -247,11 +239,7 @@ function BuyPanel(props: { const probStayedSame = formatPercent(resultProb) === formatPercent(initialProb) - const remainingMatched = isLimitOrder - ? ((newBet.orderAmount ?? 0) - newBet.amount) / - (betChoice === 'YES' ? limitProbFrac : 1 - limitProbFrac) - : 0 - const currentPayout = newBet.shares + remainingMatched + const currentPayout = newBet.shares const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturnPercent = formatPercent(currentReturn) @@ -261,14 +249,14 @@ function BuyPanel(props: { const format = getFormattedMappedValue(contract) return ( - <> + <Col className={hidden ? 'hidden' : ''}> <div className="my-3 text-left text-sm text-gray-500"> {isPseudoNumeric ? 'Direction' : 'Outcome'} </div> <YesNoSelector className="mb-4" btnClassName="flex-1" - selected={betChoice} + selected={outcome} onSelect={(choice) => onBetChoice(choice)} isPseudoNumeric={isPseudoNumeric} /> @@ -283,61 +271,21 @@ function BuyPanel(props: { disabled={isSubmitting} inputRef={inputRef} /> - {isLimitOrder && ( - <> - <Row className="my-3 items-center gap-2 text-left text-sm text-gray-500"> - Limit {isPseudoNumeric ? 'value' : 'probability'} - <InfoTooltip - text={`Bet ${betChoice === 'NO' ? 'down' : 'up'} to this ${ - isPseudoNumeric ? 'value' : 'probability' - } and wait to match other bets.`} - /> - </Row> - {isPseudoNumeric ? ( - <BucketInput - contract={contract} - onBucketChange={(value) => - setLimitProb( - value === undefined - ? undefined - : 100 * - getPseudoProbability( - value, - contract.min, - contract.max, - contract.isLogScale - ) - ) - } - isSubmitting={isSubmitting} - /> - ) : ( - <ProbabilityInput - inputClassName="w-full max-w-none" - prob={limitProb} - onChange={setLimitProb} - disabled={isSubmitting} - /> - )} - </> - )} <Col className="mt-3 w-full gap-3"> - {!isLimitOrder && ( - <Row className="items-center justify-between text-sm"> - <div className="text-gray-500"> - {isPseudoNumeric ? 'Estimated value' : 'Probability'} + <Row className="items-center justify-between text-sm"> + <div className="text-gray-500"> + {isPseudoNumeric ? 'Estimated value' : 'Probability'} + </div> + {probStayedSame ? ( + <div>{format(initialProb)}</div> + ) : ( + <div> + {format(initialProb)} + <span className="mx-2">→</span> + {format(resultProb)} </div> - {probStayedSame ? ( - <div>{format(initialProb)}</div> - ) : ( - <div> - {format(initialProb)} - <span className="mx-2">→</span> - {format(resultProb)} - </div> - )} - </Row> - )} + )} + </Row> <Row className="items-center justify-between gap-2 text-sm"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> @@ -346,7 +294,7 @@ function BuyPanel(props: { 'Max payout' ) : ( <> - Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} /> + Payout if <BinaryOutcomeLabel outcome={outcome ?? 'YES'} /> </> )} </div> @@ -365,6 +313,353 @@ function BuyPanel(props: { <Spacer h={8} /> + {user && ( + <button + className={clsx( + 'btn flex-1', + betDisabled + ? 'btn-disabled' + : outcome === 'YES' + ? 'btn-primary' + : 'border-none bg-red-400 hover:bg-red-500', + isSubmitting ? 'loading' : '' + )} + onClick={betDisabled ? undefined : submitBet} + > + {isSubmitting ? 'Submitting...' : 'Submit bet'} + </button> + )} + + {wasSubmitted && <div className="mt-4">Bet submitted!</div>} + </Col> + ) +} + +function LimitOrderPanel(props: { + contract: CPMMBinaryContract | PseudoNumericContract + user: User | null | undefined + unfilledBets: Bet[] + hidden: boolean + onBuySuccess?: () => void +}) { + const { contract, user, unfilledBets, hidden, onBuySuccess } = props + + const initialProb = getProbability(contract) + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' + + const [betAmount, setBetAmount] = useState<number | undefined>(undefined) + const [lowLimitProb, setLowLimitProb] = useState<number | undefined>() + const [highLimitProb, setHighLimitProb] = useState<number | undefined>() + const betChoice = 'YES' + const [error, setError] = useState<string | undefined>() + const [isSubmitting, setIsSubmitting] = useState(false) + const [wasSubmitted, setWasSubmitted] = useState(false) + + const rangeError = + lowLimitProb !== undefined && + highLimitProb !== undefined && + lowLimitProb >= highLimitProb + + const outOfRangeError = + (lowLimitProb !== undefined && + (lowLimitProb <= 0 || lowLimitProb >= 100)) || + (highLimitProb !== undefined && + (highLimitProb <= 0 || highLimitProb >= 100)) + + const initialLow = initialProb * 0.9 + const initialHigh = initialProb + (1 - initialProb) * 0.1 + const lowPlaceholder = Math.round( + isPseudoNumeric ? getMappedValue(contract)(initialLow) : initialLow * 100 + ).toString() + const highPlaceholder = Math.round( + isPseudoNumeric ? getMappedValue(contract)(initialHigh) : initialHigh * 100 + ).toString() + + const hasYesLimitBet = lowLimitProb !== undefined && !!betAmount + const hasNoLimitBet = highLimitProb !== undefined && !!betAmount + const hasTwoBets = hasYesLimitBet && hasNoLimitBet + + const betDisabled = + isSubmitting || + !betAmount || + rangeError || + outOfRangeError || + error || + (!hasYesLimitBet && !hasNoLimitBet) + + const yesLimitProb = + lowLimitProb === undefined ? undefined : lowLimitProb / 100 + const noLimitProb = + highLimitProb === undefined ? undefined : highLimitProb / 100 + + const shares = + yesLimitProb !== undefined && noLimitProb !== undefined + ? Math.min( + (betAmount ?? 0) / yesLimitProb, + (betAmount ?? 0) / (1 - noLimitProb) + ) + : (betAmount ?? 0) / (yesLimitProb ?? 1 - (noLimitProb ?? 1)) + + const yesAmount = shares * (yesLimitProb ?? 1) + const noAmount = shares * (1 - (noLimitProb ?? 1)) + + const profitIfBothFilled = shares - (yesAmount + noAmount) + + function onBetChange(newAmount: number | undefined) { + setWasSubmitted(false) + setBetAmount(newAmount) + } + + async function submitBet() { + if (!user || betDisabled) return + + setError(undefined) + setIsSubmitting(true) + + const betsPromise = hasTwoBets + ? Promise.all([ + placeBet({ + outcome: 'YES', + amount: yesAmount, + limitProb: yesLimitProb, + contractId: contract.id, + }), + placeBet({ + outcome: 'NO', + amount: noAmount, + limitProb: noLimitProb, + contractId: contract.id, + }), + ]) + : placeBet({ + outcome: hasYesLimitBet ? 'YES' : 'NO', + amount: betAmount, + contractId: contract.id, + limitProb: hasYesLimitBet ? yesLimitProb : noLimitProb, + }) + + betsPromise + .catch((e) => { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error placing bet') + } + setIsSubmitting(false) + }) + .then((r) => { + console.log('placed bet. Result:', r) + setIsSubmitting(false) + setWasSubmitted(true) + setBetAmount(undefined) + if (onBuySuccess) onBuySuccess() + }) + + if (hasYesLimitBet) { + track('bet', { + location: 'bet panel', + outcomeType: contract.outcomeType, + slug: contract.slug, + contractId: contract.id, + amount: yesAmount, + outcome: 'YES', + limitProb: yesLimitProb, + isLimitOrder: true, + isRangeOrder: hasTwoBets, + }) + } + if (hasNoLimitBet) { + track('bet', { + location: 'bet panel', + outcomeType: contract.outcomeType, + slug: contract.slug, + contractId: contract.id, + amount: noAmount, + outcome: 'NO', + limitProb: noLimitProb, + isLimitOrder: true, + isRangeOrder: hasTwoBets, + }) + } + } + + const { + currentPayout: yesPayout, + currentReturn: yesReturn, + totalFees: yesFees, + newBet: yesBet, + } = getBinaryBetStats( + 'YES', + yesAmount, + contract, + Math.min(yesLimitProb ?? initialLow, 0.999), + unfilledBets as LimitBet[] + ) + const yesReturnPercent = formatPercent(yesReturn) + + const { + currentPayout: noPayout, + currentReturn: noReturn, + totalFees: noFees, + newBet: noBet, + } = getBinaryBetStats( + 'NO', + noAmount, + contract, + Math.max(noLimitProb ?? initialHigh, 0.01), + unfilledBets as LimitBet[] + ) + const noReturnPercent = formatPercent(noReturn) + + return ( + <Col className={hidden ? 'hidden' : ''}> + <div className="my-3 text-sm text-gray-500"> + Bet when the {isPseudoNumeric ? 'value' : 'probability'} reaches Low + and/or High limit. + </div> + + <Row className="items-center gap-4"> + <Col className="gap-2"> + <div className="ml-1 text-sm text-gray-500">Low</div> + <ProbabilityOrNumericInput + contract={contract} + prob={lowLimitProb} + setProb={setLowLimitProb} + isSubmitting={isSubmitting} + placeholder={lowPlaceholder} + /> + </Col> + <Col className="gap-2"> + <div className="ml-1 text-sm text-gray-500">High</div> + <ProbabilityOrNumericInput + contract={contract} + prob={highLimitProb} + setProb={setHighLimitProb} + isSubmitting={isSubmitting} + placeholder={highPlaceholder} + /> + </Col> + </Row> + + {rangeError && ( + <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> + Low limit must be less than High limit + </div> + )} + {outOfRangeError && ( + <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> + Limit is out of range + </div> + )} + + <div className="my-3 text-left text-sm text-gray-500"> + Max amount<span className="ml-1 text-red-500">*</span> + </div> + <BuyAmountInput + inputClassName="w-full max-w-none" + amount={betAmount} + onChange={onBetChange} + error={error} + setError={setError} + disabled={isSubmitting} + /> + + <Col className="mt-3 w-full gap-3"> + {(hasTwoBets || (hasYesLimitBet && yesBet.amount !== 0)) && ( + <Row className="items-center justify-between gap-2 text-sm"> + <div className="whitespace-nowrap text-gray-500"> + {isPseudoNumeric ? ( + <HigherLabel /> + ) : ( + <BinaryOutcomeLabel outcome={'YES'} /> + )}{' '} + current fill + </div> + <div className="mr-2 whitespace-nowrap"> + {formatMoney(yesBet.amount)} of{' '} + {formatMoney(yesBet.orderAmount ?? 0)} + </div> + </Row> + )} + {(hasTwoBets || (hasNoLimitBet && noBet.amount !== 0)) && ( + <Row className="items-center justify-between gap-2 text-sm"> + <div className="whitespace-nowrap text-gray-500"> + {isPseudoNumeric ? ( + <LowerLabel /> + ) : ( + <BinaryOutcomeLabel outcome={'NO'} /> + )}{' '} + current fill + </div> + <div className="mr-2 whitespace-nowrap"> + {formatMoney(noBet.amount)} of{' '} + {formatMoney(noBet.orderAmount ?? 0)} + </div> + </Row> + )} + {hasTwoBets && ( + <Row className="items-center justify-between gap-2 text-sm"> + <div className="whitespace-nowrap text-gray-500"> + Profit if both orders filled + </div> + <div className="mr-2 whitespace-nowrap"> + {formatMoney(profitIfBothFilled)} + </div> + </Row> + )} + {hasYesLimitBet && !hasTwoBets && ( + <Row className="items-center justify-between gap-2 text-sm"> + <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> + <div> + {isPseudoNumeric ? ( + 'Max payout' + ) : ( + <> + Max <BinaryOutcomeLabel outcome={'YES'} /> payout + </> + )} + </div> + <InfoTooltip + text={`Includes ${formatMoneyWithDecimals(yesFees)} in fees`} + /> + </Row> + <div> + <span className="mr-2 whitespace-nowrap"> + {formatMoney(yesPayout)} + </span> + (+{yesReturnPercent}) + </div> + </Row> + )} + {hasNoLimitBet && !hasTwoBets && ( + <Row className="items-center justify-between gap-2 text-sm"> + <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> + <div> + {isPseudoNumeric ? ( + 'Max payout' + ) : ( + <> + Max <BinaryOutcomeLabel outcome={'NO'} /> payout + </> + )} + </div> + <InfoTooltip + text={`Includes ${formatMoneyWithDecimals(noFees)} in fees`} + /> + </Row> + <div> + <span className="mr-2 whitespace-nowrap"> + {formatMoney(noPayout)} + </span> + (+{noReturnPercent}) + </div> + </Row> + )} + </Col> + + {(hasYesLimitBet || hasNoLimitBet) && <Spacer h={8} />} + {user && ( <button className={clsx( @@ -380,16 +675,12 @@ function BuyPanel(props: { > {isSubmitting ? 'Submitting...' - : isLimitOrder - ? 'Submit order' - : 'Submit bet'} + : `Submit order${hasTwoBets ? 's' : ''}`} </button> )} - {wasSubmitted && ( - <div className="mt-4">{isLimitOrder ? 'Order' : 'Bet'} submitted!</div> - )} - </> + {wasSubmitted && <div className="mt-4">Order submitted!</div>} + </Col> ) } diff --git a/web/components/bucket-input.tsx b/web/components/bucket-input.tsx index 195032dc..19dacd65 100644 --- a/web/components/bucket-input.tsx +++ b/web/components/bucket-input.tsx @@ -9,8 +9,9 @@ export function BucketInput(props: { contract: NumericContract | PseudoNumericContract isSubmitting?: boolean onBucketChange: (value?: number, bucket?: string) => void + placeholder?: string }) { - const { contract, isSubmitting, onBucketChange } = props + const { contract, isSubmitting, onBucketChange, placeholder } = props const [numberString, setNumberString] = useState('') @@ -39,7 +40,7 @@ export function BucketInput(props: { error={undefined} disabled={isSubmitting} numberString={numberString} - label="Value" + placeholder={placeholder} /> ) } diff --git a/web/components/number-input.tsx b/web/components/number-input.tsx index d7159fab..0b48df6e 100644 --- a/web/components/number-input.tsx +++ b/web/components/number-input.tsx @@ -9,8 +9,8 @@ export function NumberInput(props: { numberString: string onChange: (newNumberString: string) => void error: string | undefined - label: string disabled?: boolean + placeholder?: string className?: string inputClassName?: string // Needed to focus the amount input @@ -21,8 +21,8 @@ export function NumberInput(props: { numberString, onChange, error, - label, disabled, + placeholder, className, inputClassName, inputRef, @@ -32,16 +32,17 @@ export function NumberInput(props: { return ( <Col className={className}> <label className="input-group"> - <span className="bg-gray-200 text-sm">{label}</span> <input className={clsx( - 'input input-bordered max-w-[200px] text-lg', + 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400', error && 'input-error', inputClassName )} ref={inputRef} type="number" - placeholder="0" + pattern="[0-9]*" + inputMode="numeric" + placeholder={placeholder ?? '0'} maxLength={9} value={numberString} disabled={disabled} diff --git a/web/components/probability-input.tsx b/web/components/probability-input.tsx index 15f73799..cc8b9259 100644 --- a/web/components/probability-input.tsx +++ b/web/components/probability-input.tsx @@ -1,4 +1,7 @@ import clsx from 'clsx' +import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' +import { getPseudoProbability } from 'common/pseudo-numeric' +import { BucketInput } from './bucket-input' import { Col } from './layout/col' import { Spacer } from './layout/spacer' @@ -6,10 +9,12 @@ export function ProbabilityInput(props: { prob: number | undefined onChange: (newProb: number | undefined) => void disabled?: boolean + placeholder?: string className?: string inputClassName?: string }) { - const { prob, onChange, disabled, className, inputClassName } = props + const { prob, onChange, disabled, placeholder, className, inputClassName } = + props const onProbChange = (str: string) => { let prob = parseInt(str.replace(/\D/g, '')) @@ -27,7 +32,7 @@ export function ProbabilityInput(props: { <label className="input-group"> <input className={clsx( - 'input input-bordered max-w-[200px] text-lg', + 'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400', inputClassName )} type="number" @@ -35,7 +40,7 @@ export function ProbabilityInput(props: { min={1} pattern="[0-9]*" inputMode="numeric" - placeholder="0" + placeholder={placeholder ?? '0'} maxLength={2} value={prob ?? ''} disabled={disabled} @@ -47,3 +52,43 @@ export function ProbabilityInput(props: { </Col> ) } + +export function ProbabilityOrNumericInput(props: { + contract: CPMMBinaryContract | PseudoNumericContract + prob: number | undefined + setProb: (prob: number | undefined) => void + isSubmitting: boolean + placeholder?: string +}) { + const { contract, prob, setProb, isSubmitting, placeholder } = props + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' + + return isPseudoNumeric ? ( + <BucketInput + contract={contract} + onBucketChange={(value) => + setProb( + value === undefined + ? undefined + : 100 * + getPseudoProbability( + value, + contract.min, + contract.max, + contract.isLogScale + ) + ) + } + isSubmitting={isSubmitting} + placeholder={placeholder} + /> + ) : ( + <ProbabilityInput + inputClassName="w-full max-w-none" + prob={prob} + onChange={setProb} + disabled={isSubmitting} + placeholder={placeholder} + /> + ) +} From 08fd27cb26fb38da1e95a68a823ac86f71948bff Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 22 Jul 2022 00:03:16 -0700 Subject: [PATCH 309/519] Make main login/logout buttons reload server side props (#677) * Set cookies in auth handler before looking up user * Make sidebar logout button trigger SSR reload * Make sidebar login button trigger SSR reload --- web/components/auth-context.tsx | 4 ++-- web/components/create-question-button.tsx | 9 ++++++++- web/components/nav/sidebar.tsx | 13 ++++++++++--- web/lib/firebase/users.ts | 2 +- web/pages/index.tsx | 14 -------------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index fcc3de39..653368b6 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -41,6 +41,7 @@ export function AuthProvider({ children }: any) { useEffect(() => { return onIdTokenChanged(auth, async (fbUser) => { if (fbUser) { + setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) let user = await getUser(fbUser.uid) if (!user) { const deviceToken = ensureDeviceToken() @@ -51,12 +52,11 @@ export function AuthProvider({ children }: any) { // Note: Cap on localStorage size is ~5mb localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user)) setCachedReferralInfoForUser(user) - setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) } else { // User logged out; reset to null + deleteAuthCookies() setAuthUser(null) localStorage.removeItem(CACHED_USER_KEY) - deleteAuthCookies() } }) }, [setAuthUser]) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index 277816fa..1b8ac11e 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -1,4 +1,5 @@ import Link from 'next/link' +import { useRouter } from 'next/router' import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' @@ -16,6 +17,7 @@ export const CreateQuestionButton = (props: { 'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700' const { user, overrideText, className, query } = props + const router = useRouter() return ( <div className={clsx('flex justify-center', className)}> {user ? ( @@ -26,7 +28,12 @@ export const CreateQuestionButton = (props: { </Link> ) : ( <button - onClick={firebaseLogin} + onClick={async () => { + // login, and then reload the page, to hit any SSR redirect (e.g. + // redirecting from / to /home for logged in users) + await firebaseLogin() + router.replace(router.asPath) + }} className={clsx(gradient, createButtonStyle)} > Sign in diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index e6ce8575..581dd5fa 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -11,7 +11,7 @@ import { } from '@heroicons/react/outline' import clsx from 'clsx' import Link from 'next/link' -import { useRouter } from 'next/router' +import Router, { useRouter } from 'next/router' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { firebaseLogout, User } from 'web/lib/firebase/users' import { ManifoldLogo } from './manifold-logo' @@ -31,6 +31,13 @@ import { setNotificationsAsSeen } from 'web/pages/notifications' import { PrivateUser } from 'common/user' import { useWindowSize } from 'web/hooks/use-window-size' +const logout = async () => { + // log out, and then reload the page, in case SSR wants to boot them out + // of whatever logged-in-only area of the site they might be in + await withTracking(firebaseLogout, 'sign out')() + await Router.replace(Router.asPath) +} + function getNavigation() { return [ { name: 'Home', href: '/home', icon: HomeIcon }, @@ -71,7 +78,7 @@ function getMoreNavigation(user?: User | null) { { name: 'Sign out', href: '#', - onClick: withTracking(firebaseLogout, 'sign out'), + onClick: logout, }, ] } @@ -122,7 +129,7 @@ function getMoreMobileNav() { { name: 'Sign out', href: '#', - onClick: withTracking(firebaseLogout, 'sign out'), + onClick: logout, }, ] } diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 481f86de..4f618586 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -179,7 +179,7 @@ export async function firebaseLogin() { } export async function firebaseLogout() { - auth.signOut() + await auth.signOut() } const storage = getStorage(app) diff --git a/web/pages/index.tsx b/web/pages/index.tsx index fd5cf382..c7e81d97 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,6 +1,3 @@ -import React, { useEffect } from 'react' -import { useRouter } from 'next/router' -import { useUser } from 'web/hooks/use-user' import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts' import { Page } from 'web/components/page' import { LandingPagePanel } from 'web/components/landing-page-panel' @@ -29,19 +26,8 @@ export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { export default function Home(props: { hotContracts: Contract[] }) { const { hotContracts } = props - // for now this redirect in the component is how we handle the case where they are - // on this page and they log in -- in the future we will make some cleaner way - const user = useUser() - const router = useRouter() - useSaveReferral() - useEffect(() => { - if (user != null) { - router.replace('/home') - } - }, [router, user]) - return ( <Page> <div className="px-4 pt-2 md:mt-0 lg:hidden"> From bfb11339ca4272a3858624ae9492c64a9a0ca332 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 08:12:40 -0600 Subject: [PATCH 310/519] Convert world and culture categories --- common/categories.ts | 2 -- functions/src/scripts/convert-categories.ts | 24 +++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/common/categories.ts b/common/categories.ts index 672f3200..f302e3f2 100644 --- a/common/categories.ts +++ b/common/categories.ts @@ -31,10 +31,8 @@ export const EXCLUDED_CATEGORIES: category[] = [ 'manifold', 'personal', 'covid', - 'culture', 'gaming', 'crypto', - 'world', ] export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES) diff --git a/functions/src/scripts/convert-categories.ts b/functions/src/scripts/convert-categories.ts index 8fe90807..7b291202 100644 --- a/functions/src/scripts/convert-categories.ts +++ b/functions/src/scripts/convert-categories.ts @@ -1,13 +1,8 @@ import * as admin from 'firebase-admin' import { initAdmin } from './script-init' -initAdmin() - import { getValues, isProd } from '../utils' -import { - CATEGORIES_GROUP_SLUG_POSTFIX, - DEFAULT_CATEGORIES, -} from 'common/categories' +import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' import { Group } from 'common/group' import { uniq } from 'lodash' import { Contract } from 'common/contract' @@ -18,9 +13,11 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID, } from 'common/antes' +initAdmin() + const adminFirestore = admin.firestore() -async function convertCategoriesToGroups() { +const addGroupIdToContracts = async () => { const groups = await getValues<Group>(adminFirestore.collection('groups')) const contracts = await getValues<Contract>( adminFirestore.collection('contracts') @@ -38,8 +35,10 @@ async function convertCategoriesToGroups() { }) } } +} - for (const category of Object.values(DEFAULT_CATEGORIES)) { +const convertCategoriesToGroupsInternal = async (categories: string[]) => { + for (const category of categories) { const markets = await getValues<Contract>( adminFirestore .collection('contracts') @@ -77,7 +76,7 @@ async function convertCategoriesToGroups() { createdTime: Date.now(), anyoneCanJoin: true, memberIds: [manifoldAccount], - about: 'Official group for all things related to ' + category, + about: 'Default group for all things related to ' + category, mostRecentActivityTime: Date.now(), contractIds: markets.map((market) => market.id), chatDisabled: true, @@ -103,6 +102,13 @@ async function convertCategoriesToGroups() { } } +async function convertCategoriesToGroups() { + // await addGroupIdToContracts() + // const defaultCategories = Object.values(DEFAULT_CATEGORIES) + const moreCategories = ['world', 'culture'] + await convertCategoriesToGroupsInternal(moreCategories) +} + if (require.main === module) { convertCategoriesToGroups() .then(() => process.exit()) From 83cb0a61304a5473bbebfc51d8abe590c174ac14 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 08:19:06 -0600 Subject: [PATCH 311/519] Allow clickable username in welcome message --- functions/src/create-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 1fd23894..1f413b6d 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -159,7 +159,7 @@ const addUserToDefaultGroups = async (user: User) => { id: welcomeCommentDoc.id, groupId: group.id, userId: manifoldAccount, - text: `Welcome, ${user.name} (@${user.username})!`, + text: `Welcome, @${user.username} aka ${user.name}!`, createdTime: Date.now(), userName: 'Manifold Markets', userUsername: MANIFOLD_USERNAME, From 87170894e2245fc733a2dbcda3721df242036cbe Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Fri, 22 Jul 2022 09:12:01 -0700 Subject: [PATCH 312/519] Suppress eslint warning for script --- functions/src/scripts/convert-categories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/src/scripts/convert-categories.ts b/functions/src/scripts/convert-categories.ts index 7b291202..d559bf92 100644 --- a/functions/src/scripts/convert-categories.ts +++ b/functions/src/scripts/convert-categories.ts @@ -17,6 +17,7 @@ initAdmin() const adminFirestore = admin.firestore() +// eslint-disable-next-line @typescript-eslint/no-unused-vars const addGroupIdToContracts = async () => { const groups = await getValues<Group>(adminFirestore.collection('groups')) const contracts = await getValues<Contract>( From 7cace82b83a6ebb2bae29f4aab643502fef497ca Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Fri, 22 Jul 2022 09:12:23 -0700 Subject: [PATCH 313/519] Render iframes inside the rich text editor (#682) * Try embedding iframes in tiptap * When iframe code is pasted, inject it into the editor * Code cleanups and comments * Remove clsx dependency Cuz it doesn't exist in `common` anyways * Rename to tiptap-iframe --- common/util/parse.ts | 2 + common/util/tiptap-iframe.ts | 92 ++++++++++++++++++++++++++++++++++++ web/components/editor.tsx | 18 +++++-- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 common/util/tiptap-iframe.ts diff --git a/common/util/parse.ts b/common/util/parse.ts index 30dcb952..cdaa6a6c 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -20,6 +20,7 @@ import { Text } from '@tiptap/extension-text' // other tiptap extensions import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' +import Iframe from './tiptap-iframe' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -80,6 +81,7 @@ export const exhibitExts = [ Image, Link, + Iframe, ] // export const exhibitExts = [StarterKit as unknown as Extension, Image] diff --git a/common/util/tiptap-iframe.ts b/common/util/tiptap-iframe.ts new file mode 100644 index 00000000..5af63d2f --- /dev/null +++ b/common/util/tiptap-iframe.ts @@ -0,0 +1,92 @@ +// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts + +import { Node } from '@tiptap/core' + +export interface IframeOptions { + allowFullscreen: boolean + HTMLAttributes: { + [key: string]: any + } +} + +declare module '@tiptap/core' { + interface Commands<ReturnType> { + iframe: { + setIframe: (options: { src: string }) => ReturnType + } + } +} + +// These classes style the outer wrapper and the inner iframe; +// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue +const wrapperClasses = 'relative h-auto w-full overflow-hidden' +const iframeClasses = 'absolute top-0 left-0 h-full w-full' + +export default Node.create<IframeOptions>({ + name: 'iframe', + + group: 'block', + + atom: true, + + addOptions() { + return { + allowFullscreen: true, + HTMLAttributes: { + class: 'iframe-wrapper' + ' ' + wrapperClasses, + // Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in: + style: 'padding-bottom: 20rem;', + }, + } + }, + + addAttributes() { + return { + src: { + default: null, + }, + frameborder: { + default: 0, + }, + allowfullscreen: { + default: this.options.allowFullscreen, + parseHTML: () => this.options.allowFullscreen, + }, + } + }, + + parseHTML() { + return [{ tag: 'iframe' }] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'div', + this.options.HTMLAttributes, + [ + 'iframe', + { + ...HTMLAttributes, + class: HTMLAttributes.class + ' ' + iframeClasses, + }, + ], + ] + }, + + addCommands() { + return { + setIframe: + (options: { src: string }) => + ({ tr, dispatch }) => { + const { selection } = tr + const node = this.type.create(options) + + if (dispatch) { + tr.replaceRangeWith(selection.from, selection.to, node) + } + + return true + }, + } + }, +}) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 7063fa42..d64dcc78 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -19,6 +19,7 @@ import { useMutation } from 'react-query' import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' +import Iframe from 'common/util/tiptap-iframe' const proseClass = clsx( 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', @@ -56,6 +57,7 @@ export function useTextEditor(props: { class: clsx('no-underline !text-indigo-700', linkClass), }, }), + Iframe, ], content: defaultValue, }) @@ -69,12 +71,20 @@ export function useTextEditor(props: { (file) => file.type.startsWith('image') ) - if (!imageFiles.length) { - return // if no files pasted, use default paste handler + if (imageFiles.length) { + event.preventDefault() + upload.mutate(imageFiles) } - event.preventDefault() - upload.mutate(imageFiles) + // If the pasted content is iframe code, directly inject it + const text = event.clipboardData?.getData('text/plain').trim() ?? '' + const isValidIframe = /^<iframe.*<\/iframe>$/.test(text) + if (isValidIframe) { + editor.chain().insertContent(text).run() + return true // Prevent the code from getting pasted as text + } + + return // Otherwise, use default paste handler }, }, }) From 624df763931543fa0d628f2815cb47f2b04bcad9 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 11:24:15 -0500 Subject: [PATCH 314/519] search: sort by liquidity; remove oldest --- web/components/contract-search.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 8eb7df6e..fca1b272 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -39,11 +39,12 @@ const indexPrefix = ENV === 'DEV' ? 'dev-' : '' const sortIndexes = [ { label: 'Newest', value: indexPrefix + 'contracts-newest' }, - { label: 'Oldest', value: indexPrefix + 'contracts-oldest' }, + // { label: 'Oldest', value: indexPrefix + 'contracts-oldest' }, { label: 'Most popular', value: indexPrefix + 'contracts-score' }, { label: 'Most traded', value: indexPrefix + 'contracts-most-traded' }, { label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' }, { label: 'Last updated', value: indexPrefix + 'contracts-last-updated' }, + { label: 'Subsidy', value: indexPrefix + 'contracts-liquidity' }, { label: 'Close date', value: indexPrefix + 'contracts-close-date' }, { label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' }, ] From de53a13c8479dba76934e3410ba611d038a8e2bf Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 11:25:48 -0500 Subject: [PATCH 315/519] fix referrals seo --- web/pages/referrals.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx index c879afaa..f50c2e2b 100644 --- a/web/pages/referrals.tsx +++ b/web/pages/referrals.tsx @@ -21,7 +21,12 @@ export default function ReferralsPage() { return ( <Page> - <SEO title="Referrals" description="" url="/add-funds" /> + <SEO + title="Referrals" + description={`Manifold's referral program. Invite new users to Manifold and get M${REFERRAL_AMOUNT} if they + sign up!`} + url="/referrals" + /> <Col className="items-center"> <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md"> From 2c80133856de6c8362d02d80ae03935800da785f Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 11:56:03 -0500 Subject: [PATCH 316/519] add SEO tags to everything --- web/pages/charity/[charitySlug].tsx | 11 +++++++++-- web/pages/charity/index.tsx | 6 ++++++ web/pages/create.tsx | 6 ++++++ web/pages/groups.tsx | 6 ++++++ web/pages/leaderboards.tsx | 6 ++++++ web/pages/markets.tsx | 2 +- 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index 2cefa13b..da3141d2 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -1,6 +1,9 @@ import { sortBy, sumBy, uniqBy } from 'lodash' import clsx from 'clsx' import React, { useEffect, useRef, useState } from 'react' +import Image from 'next/image' +import Confetti from 'react-confetti' + import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' @@ -16,11 +19,10 @@ import { useRouter } from 'next/router' import Custom404 from '../404' import { useCharityTxns } from 'web/hooks/use-charity-txns' import { useWindowSize } from 'web/hooks/use-window-size' -import Confetti from 'react-confetti' import { Donation } from 'web/components/charity/feed-items' -import Image from 'next/image' import { manaToUSD } from '../../../common/util/format' import { track } from 'web/lib/service/analytics' +import { SEO } from 'web/components/SEO' export default function CharityPageWrapper() { const router = useRouter() @@ -63,6 +65,11 @@ function CharityPage(props: { charity: Charity }) { /> } > + <SEO + title={name} + description={description} + url="/groups" + /> {showConfetti && ( <Confetti width={width ? width : 500} diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index b1cfc353..d416726b 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -23,6 +23,7 @@ import { searchInAny } from 'common/util/parse' import { getUser } from 'web/lib/firebase/users' import { SiteLink } from 'web/components/site-link' import { User } from 'common/user' +import { SEO } from 'web/components/SEO' export async function getStaticProps() { const txns = await getAllCharityTxns() @@ -114,6 +115,11 @@ export default function Charity(props: { return ( <Page> + <SEO + title="Manifold for Charity" + description="Donate your prediction market earnings to charity on Manifold." + url="/charity" + /> <Col className="w-full rounded px-4 py-6 sm:px-8 xl:w-[125%]"> <Col className=""> <Title className="!mt-0" text="Manifold for Charity" /> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 1271730f..00e49f80 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -30,6 +30,7 @@ import { TextEditor, useTextEditor } from 'web/components/editor' import { Checkbox } from 'web/components/checkbox' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { Title } from 'web/components/title' +import { SEO } from 'web/components/SEO' export const getServerSideProps = redirectIfLoggedOut('/') @@ -63,6 +64,11 @@ export default function Create() { return ( <Page> + <SEO + title="Create a market" + description="Create a play-money prediction market on any question." + url="/create" + /> <div className="mx-auto w-full max-w-2xl"> <div className="rounded-lg px-6 py-4 sm:py-0"> <Title className="!mt-0" text="Create a market" /> diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index c87f801b..d1eed970 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -18,6 +18,7 @@ import { Avatar } from 'web/components/avatar' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { UserLink } from 'web/components/user-page' import { searchInAny } from 'common/util/parse' +import { SEO } from 'web/components/SEO' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -100,6 +101,11 @@ export default function Groups(props: { return ( <Page> + <SEO + title="Groups" + description="Manifold Groups are communities centered around a collection of prediction markets. Discuss and compete on questions with your friends." + url="/groups" + /> <Col className="items-center"> <Col className="w-full max-w-2xl px-4 sm:px-2"> <Row className="items-center justify-between"> diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 7ee13172..6ce5ca01 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -13,6 +13,7 @@ import { useEffect, useState } from 'react' import { Title } from 'web/components/title' import { Tabs } from 'web/components/layout/tabs' import { useTracking } from 'web/hooks/use-tracking' +import { SEO } from 'web/components/SEO' export async function getStaticProps() { const props = await fetchProps() @@ -123,6 +124,11 @@ export default function Leaderboards(_props: { return ( <Page> + <SEO + title="Leaderboards" + description="Manifold's leaderboards show the top traders and market creators." + url="/leaderboards" + /> <Title text={'Leaderboards'} className={'hidden md:block'} /> <Tabs currentPageForAnalytics={'leaderboards'} diff --git a/web/pages/markets.tsx b/web/pages/markets.tsx index a3e851fc..2d3346c1 100644 --- a/web/pages/markets.tsx +++ b/web/pages/markets.tsx @@ -8,7 +8,7 @@ export default function Markets() { <Page> <SEO title="Explore" - description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets." + description="Discover what's new, trending, or soon-to-close. Or search thousands of prediction markets." url="/markets" /> <ContractSearch /> From e13f4d3d4de3260d6d27793a18286154048e40e3 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 11:59:25 -0500 Subject: [PATCH 317/519] charity description --- web/pages/charity/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index d416726b..80003c81 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -134,6 +134,9 @@ export default function Charity(props: { </SiteLink> ! </span> */} + <span className="text-gray-600"> + Convert your M$ earnings into real charitable donations. + </span> <DonatedStats stats={[ { From c3a0326b1ec9d1b960271e63e025872b3c3c96b4 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 12:01:52 -0500 Subject: [PATCH 318/519] homepage seo --- web/pages/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/pages/index.tsx b/web/pages/index.tsx index c7e81d97..473189aa 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -5,6 +5,7 @@ import { Col } from 'web/components/layout/col' import { ManifoldLogo } from 'web/components/nav/manifold-logo' import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth' import { useSaveReferral } from 'web/hooks/use-save-referral' +import { SEO } from 'web/components/SEO' export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { // These hardcoded markets will be shown in the frontpage for signed-out users: @@ -30,6 +31,11 @@ export default function Home(props: { hotContracts: Contract[] }) { return ( <Page> + <SEO + title="Manifold Markets" + description="Create a play-money prediction market on any topic you care about + and bet with your friends on what will happen!" + /> <div className="px-4 pt-2 md:mt-0 lg:hidden"> <ManifoldLogo /> </div> From 163c990e9d57b92ffc349afbdb43ce49e3a93c95 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 22 Jul 2022 12:03:33 -0500 Subject: [PATCH 319/519] "bettors" => "traders" --- web/components/contract/contract-leaderboard.tsx | 2 +- web/pages/group/[...slugs]/index.tsx | 4 ++-- web/pages/leaderboards.tsx | 2 +- web/pages/notifications.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 0623b6d7..deb9b857 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -49,7 +49,7 @@ export function ContractLeaderboard(props: { return users && users.length > 0 ? ( <Leaderboard - title="🏅 Top bettors" + title="🏅 Top traders" users={users || []} columns={[ { diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 06f043e7..c27d998e 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -492,7 +492,7 @@ function GroupLeaderboards(props: { <SortedLeaderboard users={members} scoreFunction={(user) => traderScores[user.id] ?? 0} - title="🏅 Top bettors" + title="🏅 Top traders" header="Profit" maxToShow={maxToShow} /> @@ -508,7 +508,7 @@ function GroupLeaderboards(props: { <> <Leaderboard className="max-w-xl" - title="🏅 Top bettors" + title="🏅 Top traders" users={topTraders} columns={[ { diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 6ce5ca01..45c484c4 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -79,7 +79,7 @@ export default function Leaderboards(_props: { <> <Col className="mx-4 items-center gap-10 lg:flex-row"> <Leaderboard - title="🏅 Top bettors" + title="🏅 Top traders" users={topTraders} columns={[ { diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 3db345ef..72754d32 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -391,7 +391,7 @@ function IncomeNotificationItem(props: { reasonText = !simple ? `Bonus for ${ parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT - } unique bettors` + } unique traders` : 'bonus on' } else if (sourceType === 'tip') { reasonText = !simple ? `tipped you` : `in tips on` From 6fb984900787234ef4062388e7e2b759b854819f Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 11:34:10 -0600 Subject: [PATCH 320/519] Allow to add/remove from groups on market page (#685) * Allow to add/remove from groups on market page * remove lib * Fix Sinclair's relative import from May * Clean --- web/components/button.tsx | 3 +- web/components/contract/contract-details.tsx | 79 ++++++++++--------- .../groups/contract-groups-list.tsx | 66 ++++++++++++++++ web/components/groups/group-selector.tsx | 30 ++++--- web/hooks/use-group.ts | 15 ++-- web/lib/firebase/groups.ts | 29 ++++++- web/pages/charity/[charitySlug].tsx | 8 +- web/pages/create.tsx | 4 +- 8 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 web/components/groups/contract-groups-list.tsx diff --git a/web/components/button.tsx b/web/components/button.tsx index d279d9a0..8877c023 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -6,7 +6,7 @@ export function Button(props: { onClick?: () => void children?: ReactNode size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' - color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' + color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' | 'gray-white' type?: 'button' | 'reset' | 'submit' disabled?: boolean }) { @@ -40,6 +40,7 @@ export function Button(props: { color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500', color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600', color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200', + color === 'gray-white' && 'bg-white text-gray-500 hover:bg-gray-200', className )} disabled={disabled} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 036311fe..0f5a1d42 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -26,8 +26,6 @@ import NewContractBadge from '../new-contract-badge' import { CATEGORY_LIST } from 'common/categories' import { TagsList } from '../tags-list' import { UserFollowButton } from '../follow-button' -import { groupPath } from 'web/lib/firebase/groups' -import { SiteLink } from 'web/components/site-link' import { DAY_MS } from 'common/util/time' import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' @@ -35,6 +33,10 @@ import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' import { exhibitExts } from 'common/util/parse' import { ENV_CONFIG } from 'common/envs/constants' +import { Button } from 'web/components/button' +import { Modal } from 'web/components/layout/modal' +import { Col } from 'web/components/layout/col' +import { ContractGroupsList } from 'web/components/groups/contract-groups-list' export type ShowTime = 'resolve-date' | 'close-date' @@ -135,31 +137,11 @@ export function ContractDetails(props: { const { closeTime, creatorName, creatorUsername, creatorId } = contract const { volumeLabel, resolvedDate } = contractMetrics(contract) - const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => { - return g2.createdTime - g1.createdTime - }) + const groups = useGroupsWithContract(contract) + const groupToDisplay = groups[0] ?? null const user = useUser() + const [open, setOpen] = useState(false) - const groupsUserIsMemberOf = groups - ? groups.filter((g) => g.memberIds.includes(contract.creatorId)) - : [] - const groupsUserIsCreatorOf = groups - ? groups.filter((g) => g.creatorId === contract.creatorId) - : [] - - // Priorities for which group the contract belongs to: - // In order of created most recently - // Group that the contract owner created - // Group the contract owner is a member of - // Any group the contract is in - const groupToDisplay = - groupsUserIsCreatorOf.length > 0 - ? groupsUserIsCreatorOf[0] - : groupsUserIsMemberOf.length > 0 - ? groupsUserIsMemberOf[0] - : groups - ? groups[0] - : undefined return ( <Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500"> <Row className="items-center gap-2"> @@ -180,16 +162,34 @@ export function ContractDetails(props: { )} {!disabled && <UserFollowButton userId={creatorId} small />} </Row> - {groupToDisplay ? ( - <Row className={'line-clamp-1 mt-1 max-w-[200px]'}> - <SiteLink href={`${groupPath(groupToDisplay.slug)}`}> - <UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" /> - <span>{groupToDisplay.name}</span> - </SiteLink> - </Row> - ) : ( - <div /> - )} + <Row> + <Button + size={'xs'} + className={'max-w-[200px]'} + color={'gray-white'} + onClick={() => setOpen(!open)} + > + <Row> + <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> + <span className={'line-clamp-1'}> + {contract.groupSlugs && !groupToDisplay + ? '' + : groupToDisplay + ? groupToDisplay.name + : 'No group'} + </span> + </Row> + </Button> + </Row> + <Modal open={open} setOpen={setOpen} size={'md'}> + <Col + className={ + 'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6' + } + > + <ContractGroupsList groups={groups} contract={contract} user={user} /> + </Col> + </Modal> {(!!closeTime || !!resolvedDate) && ( <Row className="items-center gap-1"> @@ -326,12 +326,13 @@ function EditableCloseDate(props: { Done </button> ) : ( - <button - className="btn btn-xs btn-ghost" + <Button + size={'xs'} + color={'gray-white'} onClick={() => setIsEditingCloseTime(true)} > - <PencilIcon className="mr-2 inline h-4 w-4" /> Edit - </button> + <PencilIcon className="mr-0.5 inline h-4 w-4" /> Edit + </Button> ))} </> ) diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx new file mode 100644 index 00000000..b52179b1 --- /dev/null +++ b/web/components/groups/contract-groups-list.tsx @@ -0,0 +1,66 @@ +import { Group } from 'common/group' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' +import clsx from 'clsx' +import { GroupLink } from 'web/pages/groups' +import { XIcon } from '@heroicons/react/outline' +import { Button } from 'web/components/button' +import { GroupSelector } from 'web/components/groups/group-selector' +import { + addContractToGroup, + removeContractFromGroup, +} from 'web/lib/firebase/groups' +import { User } from 'common/user' +import { Contract } from 'common/contract' + +export function ContractGroupsList(props: { + groups: Group[] + contract: Contract + user: User | null | undefined +}) { + const { groups, user, contract } = props + + return ( + <Col className={'gap-2'}> + {user && ( + <Row className={'ml-2 items-center justify-between'}> + <span>Add to group: </span> + <GroupSelector + options={{ + showSelector: true, + showLabel: false, + ignoreGroupIds: groups.map((g) => g.id), + }} + setSelectedGroup={(group) => addContractToGroup(group, contract)} + selectedGroup={undefined} + creator={user} + /> + </Row> + )} + {groups.length === 0 && ( + <Col className="ml-2 h-full justify-center text-gray-500"> + No groups yet... + </Col> + )} + {groups.map((group) => ( + <Row + key={group.id} + className={clsx('items-center justify-between gap-2 p-2')} + > + <Row className="line-clamp-1 items-center gap-2"> + <GroupLink group={group} /> + </Row> + {user && group.memberIds.includes(user.id) && ( + <Button + color={'gray-white'} + size={'xs'} + onClick={() => removeContractFromGroup(group, contract)} + > + <XIcon className="h-4 w-4 text-gray-500" /> + </Button> + )} + </Row> + ))} + </Col> + ) +} diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index c7b4cb39..e6270a4d 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -14,16 +14,22 @@ import { User } from 'common/user' import { searchInAny } from 'common/util/parse' export function GroupSelector(props: { - selectedGroup?: Group + selectedGroup: Group | undefined setSelectedGroup: (group: Group) => void creator: User | null | undefined - showSelector?: boolean + options: { + showSelector: boolean + showLabel: boolean + ignoreGroupIds?: string[] + } }) { - const { selectedGroup, setSelectedGroup, creator, showSelector } = props + const { selectedGroup, setSelectedGroup, creator, options } = props const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) - + const { showSelector, showLabel, ignoreGroupIds } = options const [query, setQuery] = useState('') - const memberGroups = useMemberGroups(creator?.id) ?? [] + const memberGroups = (useMemberGroups(creator?.id) ?? []).filter( + (group) => !ignoreGroupIds?.includes(group.id) + ) const filteredGroups = memberGroups.filter((group) => searchInAny(query, group.name) ) @@ -55,16 +61,18 @@ export function GroupSelector(props: { > {() => ( <> - <Combobox.Label className="label justify-start gap-2 text-base"> - Add to Group - <InfoTooltip text="Question will be displayed alongside the other questions in the group." /> - </Combobox.Label> + {showLabel && ( + <Combobox.Label className="label justify-start gap-2 text-base"> + Add to Group + <InfoTooltip text="Question will be displayed alongside the other questions in the group." /> + </Combobox.Label> + )} <div className="relative mt-2"> <Combobox.Input - className="w-full rounded-md border border-gray-300 bg-white p-3 pl-4 pr-20 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 " + className="w-60 rounded-md border border-gray-300 bg-white p-3 pl-4 pr-20 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 " onChange={(event) => setQuery(event.target.value)} displayValue={(group: Group) => group && group.name} - placeholder={'None'} + placeholder={'E.g. Science, Politics'} /> <Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"> <SelectorIcon diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 892efda0..84913962 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -2,13 +2,15 @@ import { useEffect, useState } from 'react' import { Group } from 'common/group' import { User } from 'common/user' import { - getGroupsWithContractId, listenForGroup, listenForGroups, listenForMemberGroups, + listGroups, } from 'web/lib/firebase/groups' import { getUser, getUsers } from 'web/lib/firebase/users' import { filterDefined } from 'common/util/array' +import { Contract } from 'common/contract' +import { uniq } from 'lodash' export const useGroup = (groupId: string | undefined) => { const [group, setGroup] = useState<Group | null | undefined>() @@ -103,12 +105,15 @@ export async function listMembers(group: Group, max?: number) { return await Promise.all(group.memberIds.slice(0, numToRetrieve).map(getUser)) } -export const useGroupsWithContract = (contractId: string | undefined) => { - const [groups, setGroups] = useState<Group[] | null | undefined>() +export const useGroupsWithContract = (contract: Contract) => { + const [groups, setGroups] = useState<Group[]>([]) useEffect(() => { - if (contractId) getGroupsWithContractId(contractId, setGroups) - }, [contractId]) + if (contract.groupSlugs) + listGroups(uniq(contract.groupSlugs)).then((groups) => + setGroups(filterDefined(groups)) + ) + }, [contract.groupSlugs]) return groups } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 5a031ca7..dc096f4e 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -44,6 +44,10 @@ export async function listAllGroups() { return getValues<Group>(groups) } +export async function listGroups(groupSlugs: string[]) { + return Promise.all(groupSlugs.map(getGroupBySlug)) +} + export function listenForGroups(setGroups: (groups: Group[]) => void) { return listenForValues(groups, setGroups) } @@ -86,12 +90,12 @@ export function listenForMemberGroups( }) } -export async function getGroupsWithContractId( +export async function listenForGroupsWithContractId( contractId: string, setGroups: (groups: Group[]) => void ) { const q = query(groups, where('contractIds', 'array-contains', contractId)) - setGroups(await getValues<Group>(q)) + return listenForValues<Group>(q, setGroups) } export async function addUserToGroupViaId(groupId: string, userId: string) { @@ -134,6 +138,27 @@ export async function addContractToGroup(group: Group, contract: Contract) { }) } +export async function removeContractFromGroup( + group: Group, + contract: Contract +) { + const newGroupSlugs = contract.groupSlugs?.filter( + (slug) => slug !== group.slug + ) + await updateContract(contract.id, { + groupSlugs: uniq(newGroupSlugs ?? []), + }) + const newContractIds = group.contractIds.filter((id) => id !== contract.id) + return await updateGroup(group, { + contractIds: uniq(newContractIds), + }) + .then(() => group) + .catch((err) => { + console.error('error removing contract from group', err) + return err + }) +} + export async function setContractGroupSlugs(group: Group, contractId: string) { await updateContract(contractId, { groupSlugs: [group.slug] }) return await updateGroup(group, { diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index da3141d2..89d2d3a3 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -20,7 +20,7 @@ import Custom404 from '../404' import { useCharityTxns } from 'web/hooks/use-charity-txns' import { useWindowSize } from 'web/hooks/use-window-size' import { Donation } from 'web/components/charity/feed-items' -import { manaToUSD } from '../../../common/util/format' +import { manaToUSD } from 'common/util/format' import { track } from 'web/lib/service/analytics' import { SEO } from 'web/components/SEO' @@ -65,11 +65,7 @@ function CharityPage(props: { charity: Charity }) { /> } > - <SEO - title={name} - description={description} - url="/groups" - /> + <SEO title={name} description={description} url="/groups" /> {showConfetti && ( <Confetti width={width ? width : 500} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 00e49f80..9fa340f5 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { setContractGroupSlugs, getGroup } from 'web/lib/firebase/groups' +import { getGroup, setContractGroupSlugs } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -353,7 +353,7 @@ export function NewContract(props: { selectedGroup={selectedGroup} setSelectedGroup={setSelectedGroup} creator={creator} - showSelector={showGroupSelector} + options={{ showSelector: showGroupSelector, showLabel: true }} /> </div> From d3d472f5d2d47931eb43a1de463ac9a2d2d1ae28 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 14:50:29 -0500 Subject: [PATCH 321/519] Hide "Your bets" when signed out. "For you" becomes "Featured" when signed out. --- web/components/contract-search.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index fca1b272..383aa99c 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -238,16 +238,18 @@ export function ContractSearch(props: { selected={pillFilter === 'personal'} onSelect={selectFilter('personal')} > - For you + {user ? 'For you' : 'Featured' } </PillButton> - <PillButton - key={'your-bets'} - selected={pillFilter === 'your-bets'} - onSelect={selectFilter('your-bets')} - > - Your bets - </PillButton> + {user && ( + <PillButton + key={'your-bets'} + selected={pillFilter === 'your-bets'} + onSelect={selectFilter('your-bets')} + > + Your bets + </PillButton> + )} {pillGroups.map(({ name, slug }) => { return ( From 63d8e6739b7884e7ec4616e3bb7d108d401b4bf0 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 13:53:19 -0600 Subject: [PATCH 322/519] Add title, mobile flex --- web/components/groups/contract-groups-list.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index b52179b1..7fab42d8 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -12,6 +12,7 @@ import { } from 'web/lib/firebase/groups' import { User } from 'common/user' import { Contract } from 'common/contract' +import { SiteLink } from 'web/components/site-link' export function ContractGroupsList(props: { groups: Group[] @@ -22,20 +23,25 @@ export function ContractGroupsList(props: { return ( <Col className={'gap-2'}> + <span className={'text-xl text-indigo-700'}> + <SiteLink href={'/groups/'}>Groups</SiteLink> + </span> {user && ( - <Row className={'ml-2 items-center justify-between'}> - <span>Add to group: </span> + <Col className={'ml-2 items-center justify-between sm:flex-row'}> + <span>Add to: </span> <GroupSelector options={{ showSelector: true, showLabel: false, ignoreGroupIds: groups.map((g) => g.id), }} - setSelectedGroup={(group) => addContractToGroup(group, contract)} + setSelectedGroup={(group) => + group && addContractToGroup(group, contract) + } selectedGroup={undefined} creator={user} /> - </Row> + </Col> )} {groups.length === 0 && ( <Col className="ml-2 h-full justify-center text-gray-500"> From d319b654cea9d0568e50789469c7358c916d1557 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 14:15:42 -0600 Subject: [PATCH 323/519] Add creator id to unique bettor ids --- functions/src/on-create-bet.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index 4e10875e..d33e71dd 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -64,10 +64,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( if (!previousUniqueBettorIds) { const contractBets = ( - await firestore - .collection(`contracts/${contractId}/bets`) - .where('userId', '!=', contract.creatorId) - .get() + await firestore.collection(`contracts/${contractId}/bets`).get() ).docs.map((doc) => doc.data() as Bet) if (contractBets.length === 0) { @@ -82,9 +79,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( ) } - const isNewUniqueBettor = - !previousUniqueBettorIds.includes(bettorId) && - bettorId !== contract.creatorId + const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId) const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId]) // Update contract unique bettors @@ -96,7 +91,9 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( uniqueBettorCount: newUniqueBettorIds.length, }) } - if (!isNewUniqueBettor) return + + // No need to give a bonus for the creator's bet + if (!isNewUniqueBettor || bettorId == contract.creatorId) return // Create combined txn for all new unique bettors const bonusTxnDetails = { From f800570845806da5e90cd5339e89f0aedbac3f4a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 16:03:52 -0500 Subject: [PATCH 324/519] Improve range limit order UI --- web/components/bet-panel.tsx | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 7d2b1e5a..064be0be 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -20,7 +20,13 @@ import { APIError, placeBet } from 'web/lib/firebase/api' import { sellShares } from 'web/lib/firebase/api' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' -import { BinaryOutcomeLabel, HigherLabel, LowerLabel } from './outcome-label' +import { + BinaryOutcomeLabel, + HigherLabel, + LowerLabel, + NoLabel, + YesLabel, +} from './outcome-label' import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' @@ -366,12 +372,12 @@ function LimitOrderPanel(props: { (highLimitProb !== undefined && (highLimitProb <= 0 || highLimitProb >= 100)) - const initialLow = initialProb * 0.9 - const initialHigh = initialProb + (1 - initialProb) * 0.1 - const lowPlaceholder = Math.round( + const initialLow = initialProb * 0.85 + const initialHigh = initialProb + (1 - initialProb) * 0.15 + const lowPlaceholder = Math.floor( isPseudoNumeric ? getMappedValue(contract)(initialLow) : initialLow * 100 ).toString() - const highPlaceholder = Math.round( + const highPlaceholder = Math.ceil( isPseudoNumeric ? getMappedValue(contract)(initialHigh) : initialHigh * 100 ).toString() @@ -514,14 +520,11 @@ function LimitOrderPanel(props: { return ( <Col className={hidden ? 'hidden' : ''}> - <div className="my-3 text-sm text-gray-500"> - Bet when the {isPseudoNumeric ? 'value' : 'probability'} reaches Low - and/or High limit. - </div> - - <Row className="items-center gap-4"> + <Row className="mt-1 items-center gap-4"> <Col className="gap-2"> - <div className="ml-1 text-sm text-gray-500">Low</div> + <div className="relative ml-1 text-sm text-gray-500"> + Bet <YesLabel /> at + </div> <ProbabilityOrNumericInput contract={contract} prob={lowLimitProb} @@ -531,7 +534,9 @@ function LimitOrderPanel(props: { /> </Col> <Col className="gap-2"> - <div className="ml-1 text-sm text-gray-500">High</div> + <div className="ml-1 text-sm text-gray-500"> + Bet <NoLabel /> at + </div> <ProbabilityOrNumericInput contract={contract} prob={highLimitProb} @@ -544,7 +549,7 @@ function LimitOrderPanel(props: { {rangeError && ( <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> - Low limit must be less than High limit + YES limit must be less than NO limit </div> )} {outOfRangeError && ( @@ -574,7 +579,7 @@ function LimitOrderPanel(props: { ) : ( <BinaryOutcomeLabel outcome={'YES'} /> )}{' '} - current fill + filled now </div> <div className="mr-2 whitespace-nowrap"> {formatMoney(yesBet.amount)} of{' '} @@ -590,7 +595,7 @@ function LimitOrderPanel(props: { ) : ( <BinaryOutcomeLabel outcome={'NO'} /> )}{' '} - current fill + filled now </div> <div className="mr-2 whitespace-nowrap"> {formatMoney(noBet.amount)} of{' '} From a1d51e377877c306d6aec2bee324d60701090350 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 16:07:59 -0500 Subject: [PATCH 325/519] Update labels for numeric market outcomes --- web/components/bet-panel.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 064be0be..db021382 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -523,7 +523,7 @@ function LimitOrderPanel(props: { <Row className="mt-1 items-center gap-4"> <Col className="gap-2"> <div className="relative ml-1 text-sm text-gray-500"> - Bet <YesLabel /> at + Bet {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} at </div> <ProbabilityOrNumericInput contract={contract} @@ -535,7 +535,7 @@ function LimitOrderPanel(props: { </Col> <Col className="gap-2"> <div className="ml-1 text-sm text-gray-500"> - Bet <NoLabel /> at + Bet {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} at </div> <ProbabilityOrNumericInput contract={contract} @@ -549,7 +549,8 @@ function LimitOrderPanel(props: { {rangeError && ( <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> - YES limit must be less than NO limit + {isPseudoNumeric ? 'HIGHER' : 'YES'} limit must be less than{' '} + {isPseudoNumeric ? 'LOWER' : 'NO'} limit </div> )} {outOfRangeError && ( @@ -558,7 +559,7 @@ function LimitOrderPanel(props: { </div> )} - <div className="my-3 text-left text-sm text-gray-500"> + <div className="mt-1 mb-3 text-left text-sm text-gray-500"> Max amount<span className="ml-1 text-red-500">*</span> </div> <BuyAmountInput From 135160dd924741cc4a680a777ec7727f64cf24e2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 16:18:36 -0500 Subject: [PATCH 326/519] Remove custom placeholders. Just show '0' for limit inputs --- web/components/bet-panel.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index db021382..987a394e 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -372,15 +372,6 @@ function LimitOrderPanel(props: { (highLimitProb !== undefined && (highLimitProb <= 0 || highLimitProb >= 100)) - const initialLow = initialProb * 0.85 - const initialHigh = initialProb + (1 - initialProb) * 0.15 - const lowPlaceholder = Math.floor( - isPseudoNumeric ? getMappedValue(contract)(initialLow) : initialLow * 100 - ).toString() - const highPlaceholder = Math.ceil( - isPseudoNumeric ? getMappedValue(contract)(initialHigh) : initialHigh * 100 - ).toString() - const hasYesLimitBet = lowLimitProb !== undefined && !!betAmount const hasNoLimitBet = highLimitProb !== undefined && !!betAmount const hasTwoBets = hasYesLimitBet && hasNoLimitBet @@ -499,7 +490,7 @@ function LimitOrderPanel(props: { 'YES', yesAmount, contract, - Math.min(yesLimitProb ?? initialLow, 0.999), + Math.min(yesLimitProb ?? initialProb, 0.999), unfilledBets as LimitBet[] ) const yesReturnPercent = formatPercent(yesReturn) @@ -513,7 +504,7 @@ function LimitOrderPanel(props: { 'NO', noAmount, contract, - Math.max(noLimitProb ?? initialHigh, 0.01), + Math.max(noLimitProb ?? initialProb, 0.01), unfilledBets as LimitBet[] ) const noReturnPercent = formatPercent(noReturn) @@ -530,7 +521,6 @@ function LimitOrderPanel(props: { prob={lowLimitProb} setProb={setLowLimitProb} isSubmitting={isSubmitting} - placeholder={lowPlaceholder} /> </Col> <Col className="gap-2"> @@ -542,7 +532,6 @@ function LimitOrderPanel(props: { prob={highLimitProb} setProb={setHighLimitProb} isSubmitting={isSubmitting} - placeholder={highPlaceholder} /> </Col> </Row> From 5899c1f3c07d99a9b217ecc1b1280553462a2da4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 16:30:07 -0500 Subject: [PATCH 327/519] Fix lints --- web/components/bet-panel.tsx | 2 +- web/components/contract-search.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 987a394e..902b0040 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -31,7 +31,7 @@ import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' -import { getFormattedMappedValue, getMappedValue } from 'common/pseudo-numeric' +import { getFormattedMappedValue } from 'common/pseudo-numeric' import { SellRow } from './sell-row' import { useSaveBinaryShares } from './use-save-binary-shares' import { SignUpPrompt } from './sign-up-prompt' diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 383aa99c..cf59b573 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -238,7 +238,7 @@ export function ContractSearch(props: { selected={pillFilter === 'personal'} onSelect={selectFilter('personal')} > - {user ? 'For you' : 'Featured' } + {user ? 'For you' : 'Featured'} </PillButton> {user && ( From 5f074206de42cf6241daada18cd1da9183c52537 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 16:28:53 -0600 Subject: [PATCH 328/519] Backfill and forward fill contracts with group info (#686) * Backfill and forward fill contracts with group info * No nested queries :( * Fix filter * Pass empty arrays instead of undefined --- common/contract.ts | 2 + common/group.ts | 8 +++ firestore.rules | 2 +- functions/src/on-delete-group.ts | 11 ++-- functions/src/scripts/convert-categories.ts | 39 ++++++-------- .../src/scripts/link-contracts-to-groups.ts | 52 +++++++++++++++++++ web/components/contract/contract-details.tsx | 50 +++++++++++------- .../groups/contract-groups-list.tsx | 17 +++--- web/components/groups/groups-button.tsx | 4 +- web/lib/firebase/contracts.ts | 17 +++--- web/lib/firebase/groups.ts | 52 ++++++++++++++++--- web/pages/create.tsx | 4 +- web/pages/group/[...slugs]/index.tsx | 12 ++--- web/pages/groups.tsx | 4 +- 14 files changed, 191 insertions(+), 83 deletions(-) create mode 100644 functions/src/scripts/link-contracts-to-groups.ts diff --git a/common/contract.ts b/common/contract.ts index b1242ab9..177af862 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -1,6 +1,7 @@ import { Answer } from './answer' import { Fees } from './fees' import { JSONContent } from '@tiptap/core' +import { GroupLink } from 'common/group' export type AnyMechanism = DPM | CPMM export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric @@ -46,6 +47,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { collectedFees: Fees groupSlugs?: string[] + groupLinks?: GroupLink[] uniqueBettorIds?: string[] uniqueBettorCount?: number popularityScore?: number diff --git a/common/group.ts b/common/group.ts index e367ded7..7d3215ae 100644 --- a/common/group.ts +++ b/common/group.ts @@ -19,3 +19,11 @@ export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] export const GROUP_CHAT_SLUG = 'chat' + +export type GroupLink = { + slug: string + name: string + groupId: string + createdTime: number + userId?: string +} diff --git a/firestore.rules b/firestore.rules index 96378d8b..0f28ca80 100644 --- a/firestore.rules +++ b/firestore.rules @@ -74,7 +74,7 @@ service cloud.firestore { match /contracts/{contractId} { allow read; allow update: if request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); + .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']); allow update: if request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['description', 'closeTime', 'question']) && resource.data.creatorId == request.auth.uid; diff --git a/functions/src/on-delete-group.ts b/functions/src/on-delete-group.ts index ca833254..e5531d7b 100644 --- a/functions/src/on-delete-group.ts +++ b/functions/src/on-delete-group.ts @@ -3,6 +3,7 @@ import * as admin from 'firebase-admin' import { Group } from 'common/group' import { Contract } from 'common/contract' + const firestore = admin.firestore() export const onDeleteGroup = functions.firestore @@ -15,17 +16,21 @@ export const onDeleteGroup = functions.firestore .collection('contracts') .where('groupSlugs', 'array-contains', group.slug) .get() + console.log("contracts with group's slug:", contracts) for (const doc of contracts.docs) { const contract = doc.data() as Contract + const newGroupLinks = contract.groupLinks?.filter( + (link) => link.slug !== group.slug + ) + // remove the group from the contract await firestore .collection('contracts') .doc(contract.id) .update({ - groupSlugs: (contract.groupSlugs ?? []).filter( - (groupSlug) => groupSlug !== group.slug - ), + groupSlugs: contract.groupSlugs?.filter((s) => s !== group.slug), + groupLinks: newGroupLinks ?? [], }) } }) diff --git a/functions/src/scripts/convert-categories.ts b/functions/src/scripts/convert-categories.ts index d559bf92..3436bcbc 100644 --- a/functions/src/scripts/convert-categories.ts +++ b/functions/src/scripts/convert-categories.ts @@ -3,7 +3,7 @@ import * as admin from 'firebase-admin' import { initAdmin } from './script-init' import { getValues, isProd } from '../utils' import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' -import { Group } from 'common/group' +import { Group, GroupLink } from 'common/group' import { uniq } from 'lodash' import { Contract } from 'common/contract' import { User } from 'common/user' @@ -17,27 +17,6 @@ initAdmin() const adminFirestore = admin.firestore() -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const addGroupIdToContracts = async () => { - const groups = await getValues<Group>(adminFirestore.collection('groups')) - const contracts = await getValues<Contract>( - adminFirestore.collection('contracts') - ) - for (const group of groups) { - const groupContracts = contracts.filter((contract) => - group.contractIds.includes(contract.id) - ) - for (const contract of groupContracts) { - await adminFirestore - .collection('contracts') - .doc(contract.id) - .update({ - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - }) - } - } -} - const convertCategoriesToGroupsInternal = async (categories: string[]) => { for (const category of categories) { const markets = await getValues<Contract>( @@ -93,18 +72,30 @@ const convertCategoriesToGroupsInternal = async (categories: string[]) => { }) for (const market of markets) { + if (market.groupLinks?.map((l) => l.groupId).includes(newGroup.id)) + continue // already in that group + + const newGroupLinks = [ + ...(market.groupLinks ?? []), + { + groupId: newGroup.id, + createdTime: Date.now(), + slug: newGroup.slug, + name: newGroup.name, + } as GroupLink, + ] await adminFirestore .collection('contracts') .doc(market.id) .update({ - groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]), + groupSlugs: uniq([...(market.groupSlugs ?? []), newGroup.slug]), + groupLinks: newGroupLinks, }) } } } async function convertCategoriesToGroups() { - // await addGroupIdToContracts() // const defaultCategories = Object.values(DEFAULT_CATEGORIES) const moreCategories = ['world', 'culture'] await convertCategoriesToGroupsInternal(moreCategories) diff --git a/functions/src/scripts/link-contracts-to-groups.ts b/functions/src/scripts/link-contracts-to-groups.ts new file mode 100644 index 00000000..feda249e --- /dev/null +++ b/functions/src/scripts/link-contracts-to-groups.ts @@ -0,0 +1,52 @@ +import { getValues } from 'functions/src/utils' +import { Group } from 'common/group' +import { Contract } from 'common/contract' +import { initAdmin } from 'functions/src/scripts/script-init' +import * as admin from 'firebase-admin' +import { filterDefined } from 'common/util/array' +import { uniq } from 'lodash' + +initAdmin() + +const adminFirestore = admin.firestore() + +const addGroupIdToContracts = async () => { + const groups = await getValues<Group>(adminFirestore.collection('groups')) + const contracts = await getValues<Contract>( + adminFirestore.collection('contracts') + ) + for (const group of groups) { + const groupContracts = contracts.filter((contract) => + group.contractIds.includes(contract.id) + ) + for (const contract of groupContracts) { + const oldGroupLinks = contract.groupLinks?.filter( + (l) => l.slug != group.slug + ) + const newGroupLinks = filterDefined([ + ...(oldGroupLinks ?? []), + group.id + ? { + slug: group.slug, + name: group.name, + groupId: group.id, + createdTime: Date.now(), + } + : undefined, + ]) + await adminFirestore + .collection('contracts') + .doc(contract.id) + .update({ + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, + }) + } + } +} + +if (require.main === module) { + addGroupIdToContracts() + .then(() => process.exit()) + .catch(console.log) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 0f5a1d42..544e9c27 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -23,11 +23,8 @@ import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' -import { CATEGORY_LIST } from 'common/categories' -import { TagsList } from '../tags-list' import { UserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' -import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' @@ -37,6 +34,8 @@ import { Button } from 'web/components/button' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' import { ContractGroupsList } from 'web/components/groups/contract-groups-list' +import { SiteLink } from 'web/components/site-link' +import { groupPath } from 'web/lib/firebase/groups' export type ShowTime = 'resolve-date' | 'close-date' @@ -50,15 +49,16 @@ export function MiscDetails(props: { volume, volume24Hours, closeTime, - tags, isResolved, createdTime, resolutionTime, + groupLinks, } = contract + // Show at most one category that this contract is tagged by - const categories = CATEGORY_LIST.filter((category) => - tags.map((t) => t.toLowerCase()).includes(category) - ).slice(0, 1) + // const categories = CATEGORY_LIST.filter((category) => + // tags.map((t) => t.toLowerCase()).includes(category) + // ).slice(0, 1) const isNew = createdTime > Date.now() - DAY_MS && !isResolved return ( @@ -80,13 +80,24 @@ export function MiscDetails(props: { {fromNow(resolutionTime || 0)} </Row> ) : volume > 0 || !isNew ? ( - <Row>{contractPool(contract)} pool</Row> + <Row className={'shrink-0'}>{contractPool(contract)} pool</Row> ) : ( <NewContractBadge /> )} - {categories.length > 0 && ( - <TagsList className="text-gray-400" tags={categories} noLabel /> + {/*{categories.length > 0 && (*/} + {/* <TagsList className="text-gray-400" tags={categories} noLabel />*/} + {/*)}*/} + {groupLinks && groupLinks.length > 0 && ( + <SiteLink + href={groupPath(groupLinks[0].slug)} + className="text-sm text-gray-400" + > + <Row className={'line-clamp-1 flex-wrap items-center '}> + <UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" /> + {groupLinks[0].name} + </Row> + </SiteLink> )} </Row> ) @@ -134,11 +145,12 @@ export function ContractDetails(props: { disabled?: boolean }) { const { contract, bets, isCreator, disabled } = props - const { closeTime, creatorName, creatorUsername, creatorId } = contract + const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } = + contract const { volumeLabel, resolvedDate } = contractMetrics(contract) - const groups = useGroupsWithContract(contract) - const groupToDisplay = groups[0] ?? null + const groupToDisplay = + groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null const user = useUser() const [open, setOpen] = useState(false) @@ -172,11 +184,7 @@ export function ContractDetails(props: { <Row> <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> <span className={'line-clamp-1'}> - {contract.groupSlugs && !groupToDisplay - ? '' - : groupToDisplay - ? groupToDisplay.name - : 'No group'} + {groupToDisplay ? groupToDisplay.name : 'No group'} </span> </Row> </Button> @@ -187,7 +195,11 @@ export function ContractDetails(props: { 'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6' } > - <ContractGroupsList groups={groups} contract={contract} user={user} /> + <ContractGroupsList + groupLinks={groupLinks ?? []} + contract={contract} + user={user} + /> </Col> </Modal> diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index 7fab42d8..423cbb97 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -1,8 +1,7 @@ -import { Group } from 'common/group' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import clsx from 'clsx' -import { GroupLink } from 'web/pages/groups' +import { GroupLinkItem } from 'web/pages/groups' import { XIcon } from '@heroicons/react/outline' import { Button } from 'web/components/button' import { GroupSelector } from 'web/components/groups/group-selector' @@ -13,14 +12,16 @@ import { import { User } from 'common/user' import { Contract } from 'common/contract' import { SiteLink } from 'web/components/site-link' +import { GroupLink } from 'common/group' +import { useGroupsWithContract } from 'web/hooks/use-group' export function ContractGroupsList(props: { - groups: Group[] + groupLinks: GroupLink[] contract: Contract user: User | null | undefined }) { - const { groups, user, contract } = props - + const { groupLinks, user, contract } = props + const groups = useGroupsWithContract(contract) return ( <Col className={'gap-2'}> <span className={'text-xl text-indigo-700'}> @@ -33,10 +34,10 @@ export function ContractGroupsList(props: { options={{ showSelector: true, showLabel: false, - ignoreGroupIds: groups.map((g) => g.id), + ignoreGroupIds: groupLinks.map((g) => g.groupId), }} setSelectedGroup={(group) => - group && addContractToGroup(group, contract) + group && addContractToGroup(group, contract, user.id) } selectedGroup={undefined} creator={user} @@ -54,7 +55,7 @@ export function ContractGroupsList(props: { className={clsx('items-center justify-between gap-2 p-2')} > <Row className="line-clamp-1 items-center gap-2"> - <GroupLink group={group} /> + <GroupLinkItem group={group} /> </Row> {user && group.memberIds.includes(user.id) && ( <Button diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index 39c75d40..bb94c9ed 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -11,7 +11,7 @@ import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { firebaseLogin } from 'web/lib/firebase/users' -import { GroupLink } from 'web/pages/groups' +import { GroupLinkItem } from 'web/pages/groups' import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { @@ -77,7 +77,7 @@ function GroupItem(props: { group: Group; className?: string }) { return ( <Row className={clsx('items-center justify-between gap-2 p-2', className)}> <Row className="line-clamp-1 items-center gap-2"> - <GroupLink group={group} /> + <GroupLinkItem group={group} /> </Row> <JoinOrLeaveGroupButton group={group} /> </Row> diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 63efa53b..14594803 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -1,17 +1,17 @@ import dayjs from 'dayjs' import { - doc, - setDoc, - deleteDoc, - where, collection, - query, - getDocs, - orderBy, + deleteDoc, + doc, getDoc, - updateDoc, + getDocs, limit, + orderBy, + query, + setDoc, startAfter, + updateDoc, + where, } from 'firebase/firestore' import { sortBy, sum } from 'lodash' @@ -129,6 +129,7 @@ export async function listContractsByGroupSlug( ): Promise<Contract[]> { const q = query(contracts, where('groupSlugs', 'array-contains', slug)) const snapshot = await getDocs(q) + console.log(snapshot.docs.map((doc) => doc.data())) return snapshot.docs.map((doc) => doc.data()) } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index dc096f4e..151e7fa1 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' -import { Group, GROUP_CHAT_SLUG } from 'common/group' +import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' import { updateContract } from './contracts' import { coll, @@ -124,9 +124,27 @@ export async function leaveGroup(group: Group, userId: string): Promise<void> { return await updateGroup(group, { memberIds: uniq(newMemberIds) }) } -export async function addContractToGroup(group: Group, contract: Contract) { +export async function addContractToGroup( + group: Group, + contract: Contract, + userId: string +) { + if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // already in that group + + const newGroupLinks = [ + ...(contract.groupLinks ?? []), + { + groupId: group.id, + createdTime: Date.now(), + slug: group.slug, + userId, + name: group.name, + } as GroupLink, + ] + await updateContract(contract.id, { - groupSlugs: [...(contract.groupSlugs ?? []), group.slug], + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, }) return await updateGroup(group, { contractIds: uniq([...group.contractIds, contract.id]), @@ -142,11 +160,15 @@ export async function removeContractFromGroup( group: Group, contract: Contract ) { - const newGroupSlugs = contract.groupSlugs?.filter( - (slug) => slug !== group.slug + if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // not in that group + + const newGroupLinks = contract.groupLinks?.filter( + (link) => link.slug !== group.slug ) await updateContract(contract.id, { - groupSlugs: uniq(newGroupSlugs ?? []), + groupSlugs: + contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [], + groupLinks: newGroupLinks ?? [], }) const newContractIds = group.contractIds.filter((id) => id !== contract.id) return await updateGroup(group, { @@ -159,8 +181,22 @@ export async function removeContractFromGroup( }) } -export async function setContractGroupSlugs(group: Group, contractId: string) { - await updateContract(contractId, { groupSlugs: [group.slug] }) +export async function setContractGroupLinks( + group: Group, + contractId: string, + userId: string +) { + await updateContract(contractId, { + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + ], + }) return await updateGroup(group, { contractIds: uniq([...group.contractIds, contractId]), }) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 9fa340f5..ec86a277 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { getGroup, setContractGroupSlugs } from 'web/lib/firebase/groups' +import { getGroup, setContractGroupLinks } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -226,7 +226,7 @@ export function NewContract(props: { isFree: false, }) if (result && selectedGroup) { - await setContractGroupSlugs(selectedGroup, result.id) + await setContractGroupLinks(selectedGroup, result.id, creator.id) } await router.push(contractPath(result as Contract)) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c27d998e..eebf0619 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,4 +1,4 @@ -import { take, sortBy, debounce } from 'lodash' +import { debounce, sortBy, take } from 'lodash' import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon' import { Group, GROUP_CHAT_SLUG } from 'common/group' @@ -6,11 +6,11 @@ import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' import { - groupPath, - getGroupBySlug, - updateGroup, - joinGroup, addContractToGroup, + getGroupBySlug, + groupPath, + joinGroup, + updateGroup, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' @@ -543,7 +543,7 @@ function AddContractButton(props: { group: Group; user: User }) { const [open, setOpen] = useState(false) async function addContractToCurrentGroup(contract: Contract) { - await addContractToGroup(group, contract) + await addContractToGroup(group, contract, user.id) setOpen(false) } diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index d1eed970..521742b2 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -1,4 +1,4 @@ -import { sortBy, debounce } from 'lodash' +import { debounce, sortBy } from 'lodash' import Link from 'next/link' import React, { useEffect, useState } from 'react' import { Group } from 'common/group' @@ -238,7 +238,7 @@ function GroupMembersList(props: { group: Group }) { ) } -export function GroupLink(props: { group: Group; className?: string }) { +export function GroupLinkItem(props: { group: Group; className?: string }) { const { group, className } = props return ( From abde013ab6339dd3517e27fc4cadb42ad39da8c9 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 16:40:37 -0600 Subject: [PATCH 329/519] Re-get contracts to get updated links --- functions/src/scripts/link-contracts-to-groups.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/functions/src/scripts/link-contracts-to-groups.ts b/functions/src/scripts/link-contracts-to-groups.ts index feda249e..a61c6c6c 100644 --- a/functions/src/scripts/link-contracts-to-groups.ts +++ b/functions/src/scripts/link-contracts-to-groups.ts @@ -12,13 +12,21 @@ const adminFirestore = admin.firestore() const addGroupIdToContracts = async () => { const groups = await getValues<Group>(adminFirestore.collection('groups')) - const contracts = await getValues<Contract>( - adminFirestore.collection('contracts') - ) + for (const group of groups) { + const contracts = await getValues<Contract>( + adminFirestore + .collection('contracts') + .where('groupSlugs', 'array-contains', group.slug) + ) const groupContracts = contracts.filter((contract) => group.contractIds.includes(contract.id) ) + if (groupContracts.length !== contracts.length) + console.log( + `Found ${groupContracts.length} contracts for group ${group.slug}` + ) + for (const contract of groupContracts) { const oldGroupLinks = contract.groupLinks?.filter( (l) => l.slug != group.slug From 56a579ff910d81830a9821133df85c6cbc30074b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 22 Jul 2022 16:44:03 -0600 Subject: [PATCH 330/519] Don't filter for group contract ids --- functions/src/scripts/link-contracts-to-groups.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/functions/src/scripts/link-contracts-to-groups.ts b/functions/src/scripts/link-contracts-to-groups.ts index a61c6c6c..e3296160 100644 --- a/functions/src/scripts/link-contracts-to-groups.ts +++ b/functions/src/scripts/link-contracts-to-groups.ts @@ -14,18 +14,11 @@ const addGroupIdToContracts = async () => { const groups = await getValues<Group>(adminFirestore.collection('groups')) for (const group of groups) { - const contracts = await getValues<Contract>( + const groupContracts = await getValues<Contract>( adminFirestore .collection('contracts') .where('groupSlugs', 'array-contains', group.slug) ) - const groupContracts = contracts.filter((contract) => - group.contractIds.includes(contract.id) - ) - if (groupContracts.length !== contracts.length) - console.log( - `Found ${groupContracts.length} contracts for group ${group.slug}` - ) for (const contract of groupContracts) { const oldGroupLinks = contract.groupLinks?.filter( From 2116b86aecfa546039979c13cd77ce5b1b1b4540 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 21:03:07 -0500 Subject: [PATCH 331/519] Fix infinite loop in numeric limit bet --- common/new-bet.ts | 7 +++++++ common/pseudo-numeric.ts | 3 +++ web/components/bet-panel.tsx | 36 +++++++++++++++++++----------------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/common/new-bet.ts b/common/new-bet.ts index ea0b011d..1f5c0340 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -142,6 +142,13 @@ export const computeFills = ( limitProb: number | undefined, unfilledBets: LimitBet[] ) => { + if (isNaN(betAmount)) { + throw new Error('Invalid bet amount: ${betAmount}') + } + if (isNaN(limitProb ?? 0)) { + throw new Error('Invalid limitProb: ${limitProb}') + } + const sortedBets = sortBy( unfilledBets.filter((bet) => bet.outcome !== outcome), (bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb), diff --git a/common/pseudo-numeric.ts b/common/pseudo-numeric.ts index 73f9fd01..ca62a80e 100644 --- a/common/pseudo-numeric.ts +++ b/common/pseudo-numeric.ts @@ -37,6 +37,9 @@ export const getPseudoProbability = ( max: number, isLogScale = false ) => { + if (value < min) return 0 + if (value > max) return 1 + if (isLogScale) { return Math.log10(value - min + 1) / Math.log10(max - min + 1) } diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 902b0040..1d9f128c 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' -import { partition, sum, sumBy } from 'lodash' +import { clamp, partition, sum, sumBy } from 'lodash' import { useUser } from 'web/hooks/use-user' import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract' @@ -385,20 +385,22 @@ function LimitOrderPanel(props: { (!hasYesLimitBet && !hasNoLimitBet) const yesLimitProb = - lowLimitProb === undefined ? undefined : lowLimitProb / 100 + lowLimitProb === undefined ? undefined : clamp(lowLimitProb, 0.001, 0.999) const noLimitProb = - highLimitProb === undefined ? undefined : highLimitProb / 100 + highLimitProb === undefined ? undefined : clamp(highLimitProb, 0.001, 0.999) + const amount = betAmount ?? 0 const shares = yesLimitProb !== undefined && noLimitProb !== undefined - ? Math.min( - (betAmount ?? 0) / yesLimitProb, - (betAmount ?? 0) / (1 - noLimitProb) - ) - : (betAmount ?? 0) / (yesLimitProb ?? 1 - (noLimitProb ?? 1)) + ? Math.min(amount / yesLimitProb, amount / (1 - noLimitProb)) + : yesLimitProb !== undefined + ? amount / yesLimitProb + : noLimitProb !== undefined + ? amount / (1 - noLimitProb) + : 0 const yesAmount = shares * (yesLimitProb ?? 1) - const noAmount = shares * (1 - (noLimitProb ?? 1)) + const noAmount = shares * (1 - (noLimitProb ?? 0)) const profitIfBothFilled = shares - (yesAmount + noAmount) @@ -490,7 +492,7 @@ function LimitOrderPanel(props: { 'YES', yesAmount, contract, - Math.min(yesLimitProb ?? initialProb, 0.999), + yesLimitProb ?? initialProb, unfilledBets as LimitBet[] ) const yesReturnPercent = formatPercent(yesReturn) @@ -504,7 +506,7 @@ function LimitOrderPanel(props: { 'NO', noAmount, contract, - Math.max(noLimitProb ?? initialProb, 0.01), + noLimitProb ?? initialProb, unfilledBets as LimitBet[] ) const noReturnPercent = formatPercent(noReturn) @@ -536,17 +538,17 @@ function LimitOrderPanel(props: { </Col> </Row> - {rangeError && ( - <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> - {isPseudoNumeric ? 'HIGHER' : 'YES'} limit must be less than{' '} - {isPseudoNumeric ? 'LOWER' : 'NO'} limit - </div> - )} {outOfRangeError && ( <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> Limit is out of range </div> )} + {rangeError && !outOfRangeError && ( + <div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> + {isPseudoNumeric ? 'HIGHER' : 'YES'} limit must be less than{' '} + {isPseudoNumeric ? 'LOWER' : 'NO'} limit + </div> + )} <div className="mt-1 mb-3 text-left text-sm text-gray-500"> Max amount<span className="ml-1 text-red-500">*</span> From 408027dd6a15abcd83ced44f6c86328242eb58c7 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 22 Jul 2022 22:44:21 -0500 Subject: [PATCH 332/519] Fix bug --- web/components/bet-panel.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 1d9f128c..c638fcde 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -385,9 +385,13 @@ function LimitOrderPanel(props: { (!hasYesLimitBet && !hasNoLimitBet) const yesLimitProb = - lowLimitProb === undefined ? undefined : clamp(lowLimitProb, 0.001, 0.999) + lowLimitProb === undefined + ? undefined + : clamp(lowLimitProb / 100, 0.001, 0.999) const noLimitProb = - highLimitProb === undefined ? undefined : clamp(highLimitProb, 0.001, 0.999) + highLimitProb === undefined + ? undefined + : clamp(highLimitProb / 100, 0.001, 0.999) const amount = betAmount ?? 0 const shares = From 71880dfc989d406060f113b7991e43569f83de2c Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sat, 23 Jul 2022 09:19:49 -0700 Subject: [PATCH 333/519] Add a toolbar for images and iframes (#688) * Add a toolbar for images and iframes * Insert embed code via modal --- web/components/editor.tsx | 122 +++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index d64dcc78..4dfddac9 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -12,7 +12,7 @@ import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import clsx from 'clsx' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' @@ -20,6 +20,12 @@ import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import Iframe from 'common/util/tiptap-iframe' +import { CodeIcon, PhotographIcon } from '@heroicons/react/solid' +import { Modal } from './layout/modal' +import { Col } from './layout/col' +import { Button } from './button' +import { Row } from './layout/row' +import { Spacer } from './layout/spacer' const proseClass = clsx( 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', @@ -36,7 +42,7 @@ export function useTextEditor(props: { const editorClass = clsx( proseClass, - 'box-content min-h-[6em] textarea textarea-bordered text-base' + 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' ) const editor = useEditor({ @@ -78,8 +84,7 @@ export function useTextEditor(props: { // If the pasted content is iframe code, directly inject it const text = event.clipboardData?.getData('text/plain').trim() ?? '' - const isValidIframe = /^<iframe.*<\/iframe>$/.test(text) - if (isValidIframe) { + if (isValidIframe(text)) { editor.chain().insertContent(text).run() return true // Prevent the code from getting pasted as text } @@ -96,16 +101,21 @@ export function useTextEditor(props: { return { editor, upload } } +function isValidIframe(text: string) { + return /^<iframe.*<\/iframe>$/.test(text) +} + export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> }) { const { editor, upload } = props + const [iframeOpen, setIframeOpen] = useState(false) return ( <> {/* hide placeholder when focused */} - <div className="w-full [&:focus-within_p.is-empty]:before:content-none"> + <div className="relative w-full [&:focus-within_p.is-empty]:before:content-none"> {editor && ( <FloatingMenu editor={editor} @@ -121,7 +131,46 @@ export function TextEditor(props: { images! </FloatingMenu> )} - <EditorContent editor={editor} /> + <div className="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <EditorContent editor={editor} /> + {/* Spacer element to match the height of the toolbar */} + <div className="py-2" aria-hidden="true"> + {/* Matches height of button in toolbar (1px border + 36px content height) */} + <div className="py-px"> + <div className="h-9" /> + </div> + </div> + </div> + + {/* Toolbar, with buttons for image and embeds */} + <div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> + <div className="flex items-center space-x-5"> + <div className="flex items-center"> + <FileUploadButton + onFiles={upload.mutate} + className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" + > + <PhotographIcon className="h-5 w-5" aria-hidden="true" /> + <span className="sr-only">Upload an image</span> + </FileUploadButton> + </div> + <div className="flex items-center"> + <button + type="button" + onClick={() => setIframeOpen(true)} + className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" + > + <IframeModal + editor={editor} + open={iframeOpen} + setOpen={setIframeOpen} + /> + <CodeIcon className="h-5 w-5" aria-hidden="true" /> + <span className="sr-only">Embed an iframe</span> + </button> + </div> + </div> + </div> </div> {upload.isLoading && <span className="text-xs">Uploading image...</span>} {upload.isError && ( @@ -131,6 +180,65 @@ export function TextEditor(props: { ) } +function IframeModal(props: { + editor: Editor | null + open: boolean + setOpen: (open: boolean) => void +}) { + const { editor, open, setOpen } = props + const [embedCode, setEmbedCode] = useState('') + const valid = isValidIframe(embedCode) + + return ( + <Modal open={open} setOpen={setOpen}> + <Col className="gap-2 rounded bg-white p-6"> + <label + htmlFor="embed" + className="block text-sm font-medium text-gray-700" + > + Embed a market, Youtube video, etc. + </label> + <input + type="text" + name="embed" + id="embed" + className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + placeholder='e.g. <iframe src="..."></iframe>' + value={embedCode} + onChange={(e) => setEmbedCode(e.target.value)} + /> + + {/* Preview the embed if it's valid */} + {valid ? <RichContent content={embedCode} /> : <Spacer h={2} />} + + <Row className="gap-2"> + <Button + disabled={!valid} + onClick={() => { + if (editor && valid) { + editor.chain().insertContent(embedCode).run() + setEmbedCode('') + setOpen(false) + } + }} + > + Embed + </Button> + <Button + color="gray" + onClick={() => { + setEmbedCode('') + setOpen(false) + }} + > + Cancel + </Button> + </Row> + </Col> + </Modal> + ) +} + const useUploadMutation = (editor: Editor | null) => useMutation( (files: File[]) => @@ -149,7 +257,7 @@ const useUploadMutation = (editor: Editor | null) => } ) -function RichContent(props: { content: JSONContent }) { +function RichContent(props: { content: JSONContent | string }) { const { content } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, From 7f42796724e26eba160c08c38e447f44fb2343a4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 23 Jul 2022 15:02:06 -0500 Subject: [PATCH 334/519] Update algolia filters to use groupLinks.slug isntead of deprecated groupSlugs field. --- web/components/contract-search.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index cf59b573..45145c54 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -130,15 +130,15 @@ export function ContractSearch(props: { : '', additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', additionalFilter?.groupSlug - ? `groupSlugs:${additionalFilter.groupSlug}` + ? `groupLinks.slug:${additionalFilter.groupSlug}` : '', pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' - ? `groupSlugs:${pillFilter}` + ? `groupLinks.slug:${pillFilter}` : '', pillFilter === 'personal' ? // Show contracts in groups that the user is a member of memberGroupSlugs - .map((slug) => `groupSlugs:${slug}`) + .map((slug) => `groupLinks.slug:${slug}`) // Show contracts created by users the user follows .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) // Show contracts bet on by users the user follows From 71b20eb61af177dfc222ce43557ac7dfa094acde Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 23 Jul 2022 15:10:54 -0500 Subject: [PATCH 335/519] Tweak visually hidden style --- web/components/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/page.tsx b/web/components/page.tsx index 40cbf7f7..1913eb7a 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -62,4 +62,6 @@ const visuallyHiddenStyle = { position: 'absolute', width: 1, whiteSpace: 'nowrap', + userSelect: 'none', + visibility: 'hidden', } as const From f43df424492f8fd6ca51190b683b8e62b2afcfc4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 23 Jul 2022 15:23:47 -0500 Subject: [PATCH 336/519] Change card to show volume instead of pool --- web/components/contract/contract-details.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 544e9c27..f581519b 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -55,10 +55,6 @@ export function MiscDetails(props: { groupLinks, } = contract - // Show at most one category that this contract is tagged by - // const categories = CATEGORY_LIST.filter((category) => - // tags.map((t) => t.toLowerCase()).includes(category) - // ).slice(0, 1) const isNew = createdTime > Date.now() - DAY_MS && !isResolved return ( @@ -80,14 +76,11 @@ export function MiscDetails(props: { {fromNow(resolutionTime || 0)} </Row> ) : volume > 0 || !isNew ? ( - <Row className={'shrink-0'}>{contractPool(contract)} pool</Row> + <Row className={'shrink-0'}>{formatMoney(contract.volume)} bet</Row> ) : ( <NewContractBadge /> )} - {/*{categories.length > 0 && (*/} - {/* <TagsList className="text-gray-400" tags={categories} noLabel />*/} - {/*)}*/} {groupLinks && groupLinks.length > 0 && ( <SiteLink href={groupPath(groupLinks[0].slug)} From 64f2dbbe7172944f592e222fa8e594a5f6704675 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 23 Jul 2022 15:26:08 -0500 Subject: [PATCH 337/519] Fix unused var --- web/components/contract/contract-details.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index f581519b..83c291c7 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -12,7 +12,6 @@ import { Contract, contractMetrics, contractPath, - contractPool, updateContract, } from 'web/lib/firebase/contracts' import dayjs from 'dayjs' From 6c8c0683279820503f3bbe1b6b8fd2f5f69b959f Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 23 Jul 2022 13:48:28 -0700 Subject: [PATCH 338/519] Write script to fix old comments without IDs and user IDs (#680) --- functions/src/scripts/backfill-comment-ids.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 functions/src/scripts/backfill-comment-ids.ts diff --git a/functions/src/scripts/backfill-comment-ids.ts b/functions/src/scripts/backfill-comment-ids.ts new file mode 100644 index 00000000..e6bb6902 --- /dev/null +++ b/functions/src/scripts/backfill-comment-ids.ts @@ -0,0 +1,55 @@ +// We have some old comments without IDs and user IDs. Let's fill them in. +// Luckily, this was back when all comments had associated bets, so it's possible +// to retrieve the user IDs through the bets. + +import * as admin from 'firebase-admin' +import { QueryDocumentSnapshot } from 'firebase-admin/firestore' +import { initAdmin } from './script-init' +import { log, writeAsync } from '../utils' +import { Bet } from '../../../common/bet' + +initAdmin() +const firestore = admin.firestore() + +const getUserIdsByCommentId = async (comments: QueryDocumentSnapshot[]) => { + const bets = await firestore.collectionGroup('bets').get() + log(`Loaded ${bets.size} bets.`) + const betsById = Object.fromEntries( + bets.docs.map((b) => [b.id, b.data() as Bet]) + ) + return Object.fromEntries( + comments.map((c) => [c.id, betsById[c.data().betId].userId]) + ) +} + +if (require.main === module) { + const commentsQuery = firestore.collectionGroup('comments') + commentsQuery.get().then(async (commentSnaps) => { + log(`Loaded ${commentSnaps.size} comments.`) + const needsFilling = commentSnaps.docs.filter((ct) => { + return !('id' in ct.data()) || !('userId' in ct.data()) + }) + log(`${needsFilling.length} comments need IDs.`) + const userIdNeedsFilling = needsFilling.filter((ct) => { + return !('userId' in ct.data()) + }) + log(`${userIdNeedsFilling.length} comments need user IDs.`) + const userIdsByCommentId = + userIdNeedsFilling.length > 0 + ? await getUserIdsByCommentId(userIdNeedsFilling) + : {} + const updates = needsFilling.map((ct) => { + const fields: { [k: string]: unknown } = {} + if (!ct.data().id) { + fields.id = ct.id + } + if (!ct.data().userId && userIdsByCommentId[ct.id]) { + fields.userId = userIdsByCommentId[ct.id] + } + return { doc: ct.ref, fields } + }) + log(`Updating ${updates.length} comments.`) + await writeAsync(firestore, updates) + log(`Updated all comments.`) + }) +} From f4e45829137c681867a44bd6846f4eff715fc76b Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Sat, 23 Jul 2022 15:04:11 -0600 Subject: [PATCH 339/519] Add group slug during create --- web/lib/firebase/groups.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 151e7fa1..f782f6a8 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -187,6 +187,7 @@ export async function setContractGroupLinks( userId: string ) { await updateContract(contractId, { + groupSlugs: [group.slug], groupLinks: [ { groupId: group.id, From 6c89e5f18fd6105e07a3f3efb393e371f0fec3f6 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Sat, 23 Jul 2022 20:37:34 -0700 Subject: [PATCH 340/519] Add @ mentions to editor (#670) * Add @ mentions to editor * Fix mention list not loading * Sort mention list by prefix, follow count * Render at mention with Linkify component - mentions are now Next <Link> rather than <a> - fix bug where editor.getText() returns [object Object] for mentions - fix mention rendering for posted markets --- common/package.json | 1 + common/util/parse.ts | 3 +- functions/package.json | 1 + web/components/editor.tsx | 62 +++++++++++------- web/components/editor/mention-list.tsx | 62 ++++++++++++++++++ web/components/editor/mention-suggestion.ts | 72 +++++++++++++++++++++ web/components/editor/mention.tsx | 29 +++++++++ web/package.json | 4 +- yarn.lock | 20 +++++- 9 files changed, 228 insertions(+), 26 deletions(-) create mode 100644 web/components/editor/mention-list.tsx create mode 100644 web/components/editor/mention-suggestion.ts create mode 100644 web/components/editor/mention.tsx diff --git a/common/package.json b/common/package.json index 6f0f5b29..c324379f 100644 --- a/common/package.json +++ b/common/package.json @@ -10,6 +10,7 @@ "dependencies": { "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", + "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", "lodash": "4.17.21" }, diff --git a/common/util/parse.ts b/common/util/parse.ts index cdaa6a6c..cacd0862 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -20,6 +20,7 @@ import { Text } from '@tiptap/extension-text' // other tiptap extensions import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' +import { Mention } from '@tiptap/extension-mention' import Iframe from './tiptap-iframe' export function parseTags(text: string) { @@ -81,9 +82,9 @@ export const exhibitExts = [ Image, Link, + Mention, Iframe, ] -// export const exhibitExts = [StarterKit as unknown as Extension, Image] export function richTextToString(text?: JSONContent) { return !text ? '' : generateText(text, exhibitExts) diff --git a/functions/package.json b/functions/package.json index f8657516..fe63dc4e 100644 --- a/functions/package.json +++ b/functions/package.json @@ -27,6 +27,7 @@ "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", + "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 4dfddac9..963cea7e 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -11,6 +11,7 @@ import { import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' +import { Mention } from '@tiptap/extension-mention' import clsx from 'clsx' import { useEffect, useState } from 'react' import { Linkify } from './linkify' @@ -19,6 +20,9 @@ import { useMutation } from 'react-query' import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' +import { useUsers } from 'web/hooks/use-users' +import { mentionSuggestion } from './editor/mention-suggestion' +import { DisplayMention } from './editor/mention' import Iframe from 'common/util/tiptap-iframe' import { CodeIcon, PhotographIcon } from '@heroicons/react/solid' import { Modal } from './layout/modal' @@ -40,33 +44,41 @@ export function useTextEditor(props: { }) { const { placeholder, max, defaultValue = '', disabled } = props + const users = useUsers() + const editorClass = clsx( proseClass, 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' ) - const editor = useEditor({ - editorProps: { attributes: { class: editorClass } }, - extensions: [ - StarterKit.configure({ - heading: { levels: [1, 2, 3] }, - }), - Placeholder.configure({ - placeholder, - emptyEditorClass: - 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', - }), - CharacterCount.configure({ limit: max }), - Image, - Link.configure({ - HTMLAttributes: { - class: clsx('no-underline !text-indigo-700', linkClass), - }, - }), - Iframe, - ], - content: defaultValue, - }) + const editor = useEditor( + { + editorProps: { attributes: { class: editorClass } }, + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + }), + Placeholder.configure({ + placeholder, + emptyEditorClass: + 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', + }), + CharacterCount.configure({ limit: max }), + Image, + Link.configure({ + HTMLAttributes: { + class: clsx('no-underline !text-indigo-700', linkClass), + }, + }), + DisplayMention.configure({ + suggestion: mentionSuggestion(users), + }), + Iframe, + ], + content: defaultValue, + }, + [!users.length] // passed as useEffect dependency. (re-render editor when users load, to update mention menu) + ) const upload = useUploadMutation(editor) @@ -261,7 +273,11 @@ function RichContent(props: { content: JSONContent | string }) { const { content } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, - extensions: exhibitExts, + extensions: [ + // replace tiptap's Mention with ours, to add style and link + ...exhibitExts.filter((ex) => ex.name !== Mention.name), + DisplayMention, + ], content, editable: false, }) diff --git a/web/components/editor/mention-list.tsx b/web/components/editor/mention-list.tsx new file mode 100644 index 00000000..f9e67daf --- /dev/null +++ b/web/components/editor/mention-list.tsx @@ -0,0 +1,62 @@ +import { SuggestionProps } from '@tiptap/suggestion' +import clsx from 'clsx' +import { User } from 'common/user' +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import { Avatar } from '../avatar' + +// copied from https://tiptap.dev/api/nodes/mention#usage +export const MentionList = forwardRef((props: SuggestionProps<User>, ref) => { + const { items: users, command } = props + + const [selectedIndex, setSelectedIndex] = useState(0) + useEffect(() => setSelectedIndex(0), [users]) + + const submitUser = (index: number) => { + const user = users[index] + if (user) command({ id: user.id, label: user.username } as any) + } + + const onUp = () => + setSelectedIndex((i) => (i + users.length - 1) % users.length) + const onDown = () => setSelectedIndex((i) => (i + 1) % users.length) + const onEnter = () => submitUser(selectedIndex) + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }: any) => { + if (event.key === 'ArrowUp') { + onUp() + return true + } + if (event.key === 'ArrowDown') { + onDown() + return true + } + if (event.key === 'Enter') { + onEnter() + return true + } + return false + }, + })) + + return ( + <div className="w-42 absolute z-10 overflow-x-hidden rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + {!users.length ? ( + <span className="m-1 whitespace-nowrap">No results...</span> + ) : ( + users.map((user, i) => ( + <button + className={clsx( + 'flex h-8 w-full cursor-pointer select-none items-center gap-2 truncate px-4', + selectedIndex === i ? 'bg-indigo-500 text-white' : 'text-gray-900' + )} + onClick={() => submitUser(i)} + > + <Avatar avatarUrl={user.avatarUrl} size="xs" /> + {user.username} + </button> + )) + )} + </div> + ) +}) diff --git a/web/components/editor/mention-suggestion.ts b/web/components/editor/mention-suggestion.ts new file mode 100644 index 00000000..e21789c9 --- /dev/null +++ b/web/components/editor/mention-suggestion.ts @@ -0,0 +1,72 @@ +import type { MentionOptions } from '@tiptap/extension-mention' +import { ReactRenderer } from '@tiptap/react' +import { User } from 'common/user' +import { searchInAny } from 'common/util/parse' +import { orderBy } from 'lodash' +import tippy from 'tippy.js' +import { MentionList } from './mention-list' + +type Suggestion = MentionOptions['suggestion'] + +const beginsWith = (text: string, query: string) => + text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()) + +// copied from https://tiptap.dev/api/nodes/mention#usage +export const mentionSuggestion = (users: User[]): Suggestion => ({ + items: ({ query }) => + orderBy( + users.filter((u) => searchInAny(query, u.username, u.name)), + [ + (u) => [u.name, u.username].some((s) => beginsWith(s, query)), + 'followerCountCached', + ], + ['desc', 'desc'] + ).slice(0, 5), + render: () => { + let component: ReactRenderer + let popup: ReturnType<typeof tippy> + return { + onStart: (props) => { + component = new ReactRenderer(MentionList, { + props, + editor: props.editor, + }) + if (!props.clientRect) { + return + } + + popup = tippy('body', { + getReferenceClientRect: props.clientRect as any, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }) + }, + onUpdate(props) { + component.updateProps(props) + + if (!props.clientRect) { + return + } + + popup[0].setProps({ + getReferenceClientRect: props.clientRect as any, + }) + }, + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup[0].hide() + return true + } + return (component.ref as any)?.onKeyDown(props) + }, + onExit() { + popup[0].destroy() + component.destroy() + }, + } + }, +}) diff --git a/web/components/editor/mention.tsx b/web/components/editor/mention.tsx new file mode 100644 index 00000000..3ad5de39 --- /dev/null +++ b/web/components/editor/mention.tsx @@ -0,0 +1,29 @@ +import Mention from '@tiptap/extension-mention' +import { + mergeAttributes, + NodeViewWrapper, + ReactNodeViewRenderer, +} from '@tiptap/react' +import clsx from 'clsx' +import { Linkify } from '../linkify' + +const name = 'mention-component' + +const MentionComponent = (props: any) => { + return ( + <NodeViewWrapper className={clsx(name, 'not-prose inline text-indigo-700')}> + <Linkify text={'@' + props.node.attrs.label} /> + </NodeViewWrapper> + ) +} + +/** + * Mention extension that renders React. See: + * https://tiptap.dev/guide/custom-extensions#extend-existing-extensions + * https://tiptap.dev/guide/node-views/react#render-a-react-component + */ +export const DisplayMention = Mention.extend({ + parseHTML: () => [{ tag: name }], + renderHTML: ({ HTMLAttributes }) => [name, mergeAttributes(HTMLAttributes)], + addNodeView: () => ReactNodeViewRenderer(MentionComponent), +}) diff --git a/web/package.json b/web/package.json index d09ccaf0..9f27643e 100644 --- a/web/package.json +++ b/web/package.json @@ -27,6 +27,7 @@ "@tiptap/extension-character-count": "2.0.0-beta.31", "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", + "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-placeholder": "2.0.0-beta.53", "@tiptap/react": "2.0.0-beta.114", "@tiptap/starter-kit": "2.0.0-beta.190", @@ -49,7 +50,8 @@ "react-hot-toast": "2.2.0", "react-instantsearch-hooks-web": "6.24.1", "react-query": "3.39.0", - "string-similarity": "^4.0.4" + "string-similarity": "^4.0.4", + "tippy.js": "6.3.7" }, "devDependencies": { "@tailwindcss/forms": "0.4.0", diff --git a/yarn.lock b/yarn.lock index ffa8e6f0..019a3dd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3010,6 +3010,15 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843" integrity sha512-AkzvdELz3ZnrlZM0r9+ritBDOnAjXHR/8zCZhW0ZlWx4zyKPMsNG5ygivY+xr4QT65NEGRT8P8b2zOhXrMjjMQ== +"@tiptap/extension-mention@2.0.0-beta.102": + version "2.0.0-beta.102" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.102.tgz#a80036b0a4481efc4f69b788af3f5c76428624cc" + integrity sha512-QTBBpWnRnoV7/ZW31HwhPvZL3HiwnlehlHSLeMioVxAQPF5WrRtlOpxK/SRu7+KuwdCb7ZA1eWW/yjbXI3oktg== + dependencies: + "@tiptap/suggestion" "^2.0.0-beta.97" + prosemirror-model "1.18.1" + prosemirror-state "1.4.1" + "@tiptap/extension-ordered-list@^2.0.0-beta.30": version "2.0.0-beta.30" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.30.tgz#1f656b664302d90272c244b2e478d7056203f2a8" @@ -3073,6 +3082,15 @@ "@tiptap/extension-strike" "^2.0.0-beta.29" "@tiptap/extension-text" "^2.0.0-beta.17" +"@tiptap/suggestion@^2.0.0-beta.97": + version "2.0.0-beta.97" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.97.tgz#2e3dc20deebc2c37c5d39c848e61e9c837e7188a" + integrity sha512-3NWG+HE7v2w97Ek6z1tUosoZKpCDH+oAtIG9XoNkK1PmlaVV/H4d6HT9uPX+Y6SeN7fSAqlcXFUGLXcDi9d+Zw== + dependencies: + prosemirror-model "1.18.1" + prosemirror-state "1.4.1" + prosemirror-view "1.26.2" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -11058,7 +11076,7 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tippy.js@^6.3.7: +tippy.js@6.3.7, tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== From 1f655acddb0f36406a001ba67e7ddfcb358b2c7c Mon Sep 17 00:00:00 2001 From: Olivia Appleton <gabe@gabeappleton.me> Date: Sun, 24 Jul 2022 02:33:19 -0400 Subject: [PATCH 341/519] Add my market manager tool (#690) --- docs/docs/awesome-manifold.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/awesome-manifold.md b/docs/docs/awesome-manifold.md index ade5caee..44167bcb 100644 --- a/docs/docs/awesome-manifold.md +++ b/docs/docs/awesome-manifold.md @@ -15,6 +15,7 @@ A list of community-created projects built on, or related to, Manifold Markets. - [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API - [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics) +- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets - [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets ## Bots From 6ad43b02c79aca0e188fcbd127630cdd54f90bb8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 24 Jul 2022 00:11:33 -0700 Subject: [PATCH 342/519] Show the number of comments and bets --- web/components/contract/contract-tabs.tsx | 15 ++++++++++++--- web/components/feed/contract-activity.tsx | 6 +----- web/components/layout/tabs.tsx | 5 +++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index c7759fb8..fbf056e3 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -9,6 +9,7 @@ import { Tabs } from '../layout/tabs' import { Col } from '../layout/col' import { CommentTipMap } from 'web/hooks/use-tip-txns' import { LiquidityProvision } from 'common/liquidity-provision' +import { useComments } from 'web/hooks/use-comments' export function ContractTabs(props: { contract: Contract @@ -18,11 +19,15 @@ export function ContractTabs(props: { comments: Comment[] tips: CommentTipMap }) { - const { contract, user, bets, comments, tips, liquidityProvisions } = props + const { contract, user, bets, tips, liquidityProvisions } = props const { outcomeType } = contract const userBets = user && bets.filter((bet) => bet.userId === user.id) + // Load comments here, so the badge count will be correct + const updatedComments = useComments(contract.id) + const comments = updatedComments ?? props.comments + const betActivity = ( <ContractActivity contract={contract} @@ -89,8 +94,12 @@ export function ContractTabs(props: { <Tabs currentPageForAnalytics={'contract'} tabs={[ - { title: 'Comments', content: commentActivity }, - { title: 'Bets', content: betActivity }, + { + title: 'Comments', + content: commentActivity, + badge: `${comments.length}`, + }, + { title: 'Bets', content: betActivity, badge: `${bets.length}` }, ...(!user || !userBets?.length ? [] : [{ title: 'Your bets', content: yourTrades }]), diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index c60afa70..e50eb76b 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -2,7 +2,6 @@ import { Contract } from 'web/lib/firebase/contracts' import { Comment } from 'web/lib/firebase/comments' import { Bet } from 'common/bet' import { useBets } from 'web/hooks/use-bets' -import { useComments } from 'web/hooks/use-comments' import { getSpecificContractActivityItems } from './activity-items' import { FeedItems } from './feed-items' import { User } from 'common/user' @@ -26,10 +25,7 @@ export function ContractActivity(props: { props const contract = useContractWithPreload(props.contract) ?? props.contract - - const updatedComments = useComments(contract.id) - const comments = updatedComments ?? props.comments - + const comments = props.comments const updatedBets = useBets(contract.id) const bets = (updatedBets ?? props.bets).filter( (bet) => !bet.isRedemption && bet.amount !== 0 diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 8aec39b1..da3593a1 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -10,6 +10,8 @@ type Tab = { content: ReactNode // If set, change the url to this href when the tab is selected href?: string + // If set, show a badge with this content + badge?: string } export function Tabs(props: { @@ -63,6 +65,9 @@ export function Tabs(props: { > <Row className={'items-center justify-center gap-1'}> {tab.tabIcon && <span> {tab.tabIcon}</span>} + {tab.badge ? ( + <div className="px-0.5 font-bold">{tab.badge}</div> + ) : null} {tab.title} </Row> </a> From a1d5d161ddd779d5057ba727d9ce65a4681b2756 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 24 Jul 2022 00:26:38 -0700 Subject: [PATCH 343/519] Revamp backend code to support good local function development (#657) * Move concurrently dep upwards * Add express as explicit dependency * Accept just one HTTP method per endpoint * Fix endpoint option coalescing * Expressification of cloud functions * Nicer logging of API requests * Refactor web package.json * Add ts-node and nodemon to dev dependencies, bring back cors * Add scaffolding to point dev server at local functions * Enable emulator in dev server scaffolding * Fix up a little stuff I broke --- common/api.ts | 4 +- dev.sh | 45 +++++++++ functions/package.json | 4 + functions/src/api.ts | 63 ++++++------ functions/src/health.ts | 2 +- functions/src/index.ts | 79 +++++++++++---- functions/src/scripts/script-init.ts | 20 ++-- functions/src/serve.ts | 68 +++++++++++++ functions/src/stripe.ts | 27 ++--- functions/src/unsubscribe.ts | 110 +++++++++++---------- package.json | 5 +- web/package.json | 11 ++- yarn.lock | 143 +++++++++++++++++++++++++-- 13 files changed, 451 insertions(+), 130 deletions(-) create mode 100755 dev.sh create mode 100644 functions/src/serve.ts diff --git a/common/api.ts b/common/api.ts index b9376be5..1ae9a5fd 100644 --- a/common/api.ts +++ b/common/api.ts @@ -12,7 +12,9 @@ export class APIError extends Error { } export function getFunctionUrl(name: string) { - if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { + if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) { + return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}` + } else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { const { projectId, region } = ENV_CONFIG.firebaseConfig return `http://localhost:5001/${projectId}/${region}/${name}` } else { diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..178e20e8 --- /dev/null +++ b/dev.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +ENV=${1:-dev} +case $ENV in + dev) + FIREBASE_PROJECT=dev + NEXT_ENV=DEV + EMULATOR=false ;; + prod) + FIREBASE_PROJECT=prod + NEXT_ENV=PROD + EMULATOR=false ;; + localdb) + FIREBASE_PROJECT=dev + NEXT_ENV=DEV + EMULATOR=true ;; + *) + echo "Invalid environment; must be dev, prod, or localdb." + exit 1 +esac + +firebase use $FIREBASE_PROJECT + +if [ ! -z $EMULATOR ] +then + npx concurrently \ + -n FIRESTORE,FUNCTIONS,NEXT,TS \ + -c green,white,magenta,cyan \ + "yarn --cwd=functions firestore" \ + "cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \ + "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \ + NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \ + NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \ + yarn --cwd=web serve" \ + "cross-env yarn --cwd=web ts-watch" +else + npx concurrently \ + -n FUNCTIONS,NEXT,TS \ + -c white,magenta,cyan \ + "yarn --cwd=functions dev" \ + "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \ + NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \ + yarn --cwd=web serve" \ + "cross-env yarn --cwd=web ts-watch" +fi diff --git a/functions/package.json b/functions/package.json index fe63dc4e..b20a8fd0 100644 --- a/functions/package.json +++ b/functions/package.json @@ -12,6 +12,8 @@ "start": "yarn shell", "deploy": "firebase deploy --only functions", "logs": "firebase functions:log", + "dev": "nodemon src/serve.ts", + "firestore": "firebase emulators:start --only firestore --import=./firestore_export", "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export", @@ -29,6 +31,8 @@ "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", + "cors": "2.8.5", + "express": "4.18.1", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/functions/src/api.ts b/functions/src/api.ts index 8c01ea05..fdda0ad5 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -1,6 +1,7 @@ import * as admin from 'firebase-admin' -import { logger } from 'firebase-functions/v2' -import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https' +import { Request, RequestHandler, Response } from 'express' +import { error } from 'firebase-functions/logger' +import { HttpsOptions } from 'firebase-functions/v2/https' import { log } from './utils' import { z } from 'zod' import { APIError } from '../../common/api' @@ -45,7 +46,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => { return { kind: 'jwt', data: await auth.verifyIdToken(payload) } } catch (err) { // This is somewhat suspicious, so get it into the firebase console - logger.error('Error verifying Firebase JWT: ', err) + error('Error verifying Firebase JWT: ', err) throw new APIError(403, 'Error validating token.') } case 'Key': @@ -83,6 +84,11 @@ export const zTimestamp = () => { }, z.date()) } +export type EndpointDefinition = { + opts: EndpointOptions & { method: string } + handler: RequestHandler +} + export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { const result = schema.safeParse(val) if (!result.success) { @@ -99,12 +105,12 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { } } -interface EndpointOptions extends HttpsOptions { - methods?: string[] +export interface EndpointOptions extends HttpsOptions { + method?: string } const DEFAULT_OPTS = { - methods: ['POST'], + method: 'POST', minInstances: 1, concurrency: 100, memory: '2GiB', @@ -113,28 +119,29 @@ const DEFAULT_OPTS = { } export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { - const opts = Object.assign(endpointOpts, DEFAULT_OPTS) - return onRequest(opts, async (req, res) => { - log('Request processing started.') - try { - if (!opts.methods.includes(req.method)) { - const allowed = opts.methods.join(', ') - throw new APIError(405, `This endpoint supports only ${allowed}.`) - } - const authedUser = await lookupUser(await parseCredentials(req)) - log('User credentials processed.') - res.status(200).json(await fn(req, authedUser)) - } catch (e) { - if (e instanceof APIError) { - const output: { [k: string]: unknown } = { message: e.message } - if (e.details != null) { - output.details = e.details + const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts) + return { + opts, + handler: async (req: Request, res: Response) => { + log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`) + try { + if (opts.method !== req.method) { + throw new APIError(405, `This endpoint supports only ${opts.method}.`) + } + const authedUser = await lookupUser(await parseCredentials(req)) + res.status(200).json(await fn(req, authedUser)) + } catch (e) { + if (e instanceof APIError) { + const output: { [k: string]: unknown } = { message: e.message } + if (e.details != null) { + output.details = e.details + } + res.status(e.code).json(output) + } else { + error(e) + res.status(500).json({ message: 'An unknown error occurred.' }) } - res.status(e.code).json(output) - } else { - logger.error(e) - res.status(500).json({ message: 'An unknown error occurred.' }) } - } - }) + }, + } as EndpointDefinition } diff --git a/functions/src/health.ts b/functions/src/health.ts index 938261db..4ce04e05 100644 --- a/functions/src/health.ts +++ b/functions/src/health.ts @@ -1,6 +1,6 @@ import { newEndpoint } from './api' -export const health = newEndpoint({ methods: ['GET'] }, async (_req, auth) => { +export const health = newEndpoint({ method: 'GET' }, async (_req, auth) => { return { message: 'Server is working.', uid: auth.uid, diff --git a/functions/src/index.ts b/functions/src/index.ts index df311886..239806de 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,4 +1,6 @@ import * as admin from 'firebase-admin' +import { onRequest } from 'firebase-functions/v2/https' +import { EndpointDefinition } from './api' admin.initializeApp() @@ -25,20 +27,63 @@ export * from './on-delete-group' export * from './score-contracts' // v2 -export * from './health' -export * from './transact' -export * from './change-user-info' -export * from './create-user' -export * from './create-answer' -export * from './place-bet' -export * from './cancel-bet' -export * from './sell-bet' -export * from './sell-shares' -export * from './claim-manalink' -export * from './create-contract' -export * from './add-liquidity' -export * from './withdraw-liquidity' -export * from './create-group' -export * from './resolve-market' -export * from './unsubscribe' -export * from './stripe' +import { health } from './health' +import { transact } from './transact' +import { changeuserinfo } from './change-user-info' +import { createuser } from './create-user' +import { createanswer } from './create-answer' +import { placebet } from './place-bet' +import { cancelbet } from './cancel-bet' +import { sellbet } from './sell-bet' +import { sellshares } from './sell-shares' +import { claimmanalink } from './claim-manalink' +import { createmarket } from './create-contract' +import { addliquidity } from './add-liquidity' +import { withdrawliquidity } from './withdraw-liquidity' +import { creategroup } from './create-group' +import { resolvemarket } from './resolve-market' +import { unsubscribe } from './unsubscribe' +import { stripewebhook, createcheckoutsession } from './stripe' + +const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { + return onRequest(opts, handler as any) +} +const healthFunction = toCloudFunction(health) +const transactFunction = toCloudFunction(transact) +const changeUserInfoFunction = toCloudFunction(changeuserinfo) +const createUserFunction = toCloudFunction(createuser) +const createAnswerFunction = toCloudFunction(createanswer) +const placeBetFunction = toCloudFunction(placebet) +const cancelBetFunction = toCloudFunction(cancelbet) +const sellBetFunction = toCloudFunction(sellbet) +const sellSharesFunction = toCloudFunction(sellshares) +const claimManalinkFunction = toCloudFunction(claimmanalink) +const createMarketFunction = toCloudFunction(createmarket) +const addLiquidityFunction = toCloudFunction(addliquidity) +const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity) +const createGroupFunction = toCloudFunction(creategroup) +const resolveMarketFunction = toCloudFunction(resolvemarket) +const unsubscribeFunction = toCloudFunction(unsubscribe) +const stripeWebhookFunction = toCloudFunction(stripewebhook) +const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) + +export { + healthFunction as health, + transactFunction as transact, + changeUserInfoFunction as changeuserinfo, + createUserFunction as createuser, + createAnswerFunction as createanswer, + placeBetFunction as placebet, + cancelBetFunction as cancelbet, + sellBetFunction as sellbet, + sellSharesFunction as sellshares, + claimManalinkFunction as claimmanalink, + createMarketFunction as createmarket, + addLiquidityFunction as addliquidity, + withdrawLiquidityFunction as withdrawliquidity, + createGroupFunction as creategroup, + resolveMarketFunction as resolvemarket, + unsubscribeFunction as unsubscribe, + stripeWebhookFunction as stripewebhook, + createCheckoutSessionFunction as createcheckoutsession, +} diff --git a/functions/src/scripts/script-init.ts b/functions/src/scripts/script-init.ts index cc17a620..5f7dc410 100644 --- a/functions/src/scripts/script-init.ts +++ b/functions/src/scripts/script-init.ts @@ -66,10 +66,18 @@ export const getServiceAccountCredentials = (env?: string) => { } export const initAdmin = (env?: string) => { - const serviceAccount = getServiceAccountCredentials(env) - console.log(`Initializing connection to ${serviceAccount.project_id}...`) - return admin.initializeApp({ - projectId: serviceAccount.project_id, - credential: admin.credential.cert(serviceAccount), - }) + try { + const serviceAccount = getServiceAccountCredentials(env) + console.log( + `Initializing connection to ${serviceAccount.project_id} Firebase...` + ) + return admin.initializeApp({ + projectId: serviceAccount.project_id, + credential: admin.credential.cert(serviceAccount), + }) + } catch (err) { + console.error(err) + console.log(`Initializing connection to default Firebase...`) + return admin.initializeApp() + } } diff --git a/functions/src/serve.ts b/functions/src/serve.ts new file mode 100644 index 00000000..77282951 --- /dev/null +++ b/functions/src/serve.ts @@ -0,0 +1,68 @@ +import * as cors from 'cors' +import * as express from 'express' +import { Express, Request, Response, NextFunction } from 'express' +import { EndpointDefinition } from './api' + +const PORT = 8088 + +import { initAdmin } from './scripts/script-init' +initAdmin() + +import { health } from './health' +import { transact } from './transact' +import { changeuserinfo } from './change-user-info' +import { createuser } from './create-user' +import { createanswer } from './create-answer' +import { placebet } from './place-bet' +import { cancelbet } from './cancel-bet' +import { sellbet } from './sell-bet' +import { sellshares } from './sell-shares' +import { claimmanalink } from './claim-manalink' +import { createmarket } from './create-contract' +import { addliquidity } from './add-liquidity' +import { withdrawliquidity } from './withdraw-liquidity' +import { creategroup } from './create-group' +import { resolvemarket } from './resolve-market' +import { unsubscribe } from './unsubscribe' +import { stripewebhook, createcheckoutsession } from './stripe' + +type Middleware = (req: Request, res: Response, next: NextFunction) => void +const app = express() + +const addEndpointRoute = ( + path: string, + endpoint: EndpointDefinition, + ...middlewares: Middleware[] +) => { + const method = endpoint.opts.method.toLowerCase() as keyof Express + const corsMiddleware = cors({ origin: endpoint.opts.cors }) + const allMiddleware = [...middlewares, corsMiddleware] + app.options(path, corsMiddleware) // preflight requests + app[method](path, ...allMiddleware, endpoint.handler) +} + +const addJsonEndpointRoute = (name: string, endpoint: EndpointDefinition) => { + addEndpointRoute(name, endpoint, express.json()) +} + +addEndpointRoute('/health', health) +addJsonEndpointRoute('/transact', transact) +addJsonEndpointRoute('/changeuserinfo', changeuserinfo) +addJsonEndpointRoute('/createuser', createuser) +addJsonEndpointRoute('/createanswer', createanswer) +addJsonEndpointRoute('/placebet', placebet) +addJsonEndpointRoute('/cancelbet', cancelbet) +addJsonEndpointRoute('/sellbet', sellbet) +addJsonEndpointRoute('/sellshares', sellshares) +addJsonEndpointRoute('/claimmanalink', claimmanalink) +addJsonEndpointRoute('/createmarket', createmarket) +addJsonEndpointRoute('/addliquidity', addliquidity) +addJsonEndpointRoute('/withdrawliquidity', withdrawliquidity) +addJsonEndpointRoute('/creategroup', creategroup) +addJsonEndpointRoute('/resolvemarket', resolvemarket) +addJsonEndpointRoute('/unsubscribe', unsubscribe) +addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession) +addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) + +app.listen(PORT) +console.log(`Serving functions on port ${PORT}.`) diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index 450bbe35..79f0ad53 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -1,7 +1,7 @@ -import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' import Stripe from 'stripe' +import { EndpointDefinition } from './api' import { getPrivateUser, getUser, isProd, payUser } from './utils' import { sendThankYouEmail } from './emails' import { track } from './analytics' @@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd() 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', } -export const createcheckoutsession = onRequest( - { minInstances: 1, secrets: ['STRIPE_APIKEY'] }, - async (req, res) => { +export const createcheckoutsession: EndpointDefinition = { + opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] }, + handler: async (req, res) => { const userId = req.query.userId?.toString() const manticDollarQuantity = req.query.manticDollarQuantity?.toString() @@ -86,21 +86,24 @@ export const createcheckoutsession = onRequest( }) res.redirect(303, session.url || '') - } -) + }, +} -export const stripewebhook = onRequest( - { +export const stripewebhook: EndpointDefinition = { + opts: { + method: 'POST', minInstances: 1, secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'], }, - async (req, res) => { + handler: async (req, res) => { const stripe = initStripe() let event try { + // Cloud Functions jam the raw body into a special `rawBody` property + const rawBody = (req as any).rawBody ?? req.body event = stripe.webhooks.constructEvent( - req.rawBody, + rawBody, req.headers['stripe-signature'] as string, process.env.STRIPE_WEBHOOKSECRET as string ) @@ -116,8 +119,8 @@ export const stripewebhook = onRequest( } res.status(200).send('success') - } -) + }, +} const issueMoneys = async (session: StripeSession) => { const { id: sessionId } = session diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index 48dd29c0..fda20e16 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -1,66 +1,72 @@ -import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' +import { EndpointDefinition } from './api' import { getUser } from './utils' import { PrivateUser } from '../../common/user' -export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => { - const id = req.query.id as string - let type = req.query.type as string - if (!id || !type) { - res.status(400).send('Empty id or type parameter.') - return - } +export const unsubscribe: EndpointDefinition = { + opts: { method: 'GET', minInstances: 1 }, + handler: async (req, res) => { + const id = req.query.id as string + let type = req.query.type as string + if (!id || !type) { + res.status(400).send('Empty id or type parameter.') + return + } - if (type === 'market-resolved') type = 'market-resolve' + if (type === 'market-resolved') type = 'market-resolve' - if ( - !['market-resolve', 'market-comment', 'market-answer', 'generic'].includes( - type - ) - ) { - res.status(400).send('Invalid type parameter.') - return - } + if ( + ![ + 'market-resolve', + 'market-comment', + 'market-answer', + 'generic', + ].includes(type) + ) { + res.status(400).send('Invalid type parameter.') + return + } - const user = await getUser(id) + const user = await getUser(id) - if (!user) { - res.send('This user is not currently subscribed or does not exist.') - return - } + if (!user) { + res.send('This user is not currently subscribed or does not exist.') + return + } - const { name } = user + const { name } = user - const update: Partial<PrivateUser> = { - ...(type === 'market-resolve' && { - unsubscribedFromResolutionEmails: true, - }), - ...(type === 'market-comment' && { - unsubscribedFromCommentEmails: true, - }), - ...(type === 'market-answer' && { - unsubscribedFromAnswerEmails: true, - }), - ...(type === 'generic' && { - unsubscribedFromGenericEmails: true, - }), - } + const update: Partial<PrivateUser> = { + ...(type === 'market-resolve' && { + unsubscribedFromResolutionEmails: true, + }), + ...(type === 'market-comment' && { + unsubscribedFromCommentEmails: true, + }), + ...(type === 'market-answer' && { + unsubscribedFromAnswerEmails: true, + }), + ...(type === 'generic' && { + unsubscribedFromGenericEmails: true, + }), + } - await firestore.collection('private-users').doc(id).update(update) + await firestore.collection('private-users').doc(id).update(update) - if (type === 'market-resolve') - res.send( - `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` - ) - else if (type === 'market-comment') - res.send( - `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` - ) - else if (type === 'market-answer') - res.send( - `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` - ) - else res.send(`${name}, you have been unsubscribed.`) -}) + if (type === 'market-resolve') + res.send( + `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` + ) + else if (type === 'market-comment') + res.send( + `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` + ) + else if (type === 'market-answer') + res.send( + `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` + ) + else res.send(`${name}, you have been unsubscribed.`) + }, +} const firestore = admin.firestore() diff --git a/package.json b/package.json index e4aee3fd..77420607 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,13 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "5.25.0", "@typescript-eslint/parser": "5.25.0", + "concurrently": "6.5.1", "eslint": "8.15.0", "eslint-plugin-lodash": "^7.4.0", "prettier": "2.5.0", - "typescript": "4.6.4" + "typescript": "4.6.4", + "ts-node": "10.9.1", + "nodemon": "2.0.19" }, "resolutions": { "@types/react": "17.0.43" diff --git a/web/package.json b/web/package.json index 9f27643e..a31dbffa 100644 --- a/web/package.json +++ b/web/package.json @@ -3,12 +3,14 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"", - "devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=DEV next dev -p 3000\" \"cross-env FIREBASE_ENV=DEV yarn ts --watch\"", + "serve": "next dev -p 3000", + "ts-watch": "tsc --watch --noEmit --incremental --preserveWatchOutput --pretty", + "dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"", + "devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev", "dev:dev": "yarn devdev", - "dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"", + "dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE yarn dev", + "dev:local": "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8080 yarn devdev", "dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev", - "ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty", "build": "next build", "start": "next start", "lint": "next lint", @@ -62,7 +64,6 @@ "@types/react": "17.0.43", "@types/string-similarity": "^4.0.0", "autoprefixer": "10.2.6", - "concurrently": "6.5.1", "critters": "0.0.16", "cross-env": "^7.0.3", "eslint-config-next": "12.1.6", diff --git a/yarn.lock b/yarn.lock index 019a3dd4..dea9e29b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,6 +1353,13 @@ resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-2.6.148.tgz#8fa825d53ffd1cbcafce1b6a830eefd3dcc09dd5" integrity sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@docsearch/css@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.1.0.tgz#6781cad43fc2e034d012ee44beddf8f93ba21f19" @@ -2337,6 +2344,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" @@ -3106,6 +3121,26 @@ resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.5.tgz#5298c5b0333c6263f06c3149b38ebccc9f169a4e" integrity sha512-KM/TuJa9fugo67dTGx+ktIqf3fVc077J6jwHu845Hex4EQf7LABlNonP/mohDKT0cmncdtlYVHHF74xR/YpThg== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -3670,7 +3705,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -3854,6 +3889,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + arg@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" @@ -4410,7 +4450,7 @@ cheerio@^1.0.0-rc.10: parse5-htmlparser2-tree-adapter "^7.0.0" tslib "^2.4.0" -chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -4772,6 +4812,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + critters@0.0.16: version "0.0.16" resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" @@ -5274,6 +5319,11 @@ didyoumean@^1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -5927,7 +5977,7 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.16.4, express@^4.17.1, express@^4.17.3: +express@4.18.1, express@^4.16.4, express@^4.17.1, express@^4.17.3: version "4.18.1" resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== @@ -7059,6 +7109,11 @@ idb@3.0.2: resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384" integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.1.9, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -8083,6 +8138,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" @@ -8434,7 +8494,23 @@ node-releases@^2.0.3: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== -nopt@1.0.10: +nodemon@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.19.tgz#cac175f74b9cb8b57e770d47841995eebe4488bd" + integrity sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.8" + semver "^5.7.1" + simple-update-notifier "^1.0.7" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@1.0.10, nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= @@ -9552,6 +9628,11 @@ pseudomap@^1.0.1: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -10422,12 +10503,12 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.0.0: +semver@7.0.0, semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== @@ -10574,6 +10655,13 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-update-notifier@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" + integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== + dependencies: + semver "~7.0.0" + sirv@^1.0.7: version "1.0.19" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" @@ -10937,7 +11025,7 @@ stylehacks@^5.1.0: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -11117,6 +11205,13 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -11142,6 +11237,25 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-node@10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsc-files@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tsc-files/-/tsc-files-1.1.3.tgz#ef4cfcb7affc9b90577d707a879dc53bb105be83" @@ -11253,6 +11367,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + unherit@^1.0.4: version "1.1.3" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" @@ -11511,6 +11630,11 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -11930,6 +12054,11 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 312b244e2a254c7bf5bb9f89aab3df1f2e2a917f Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 24 Jul 2022 00:45:45 -0700 Subject: [PATCH 344/519] Small backend cleanups (#643) * Reuse DAY_MS in update-metrics job * More concise transaction in cancelbet * Remove some meaningless awaits * Do less work in onCreateLiquidityProvision * Do less work in onCreateAnswer --- functions/src/cancel-bet.ts | 4 +--- functions/src/create-contract.ts | 2 +- functions/src/on-create-answer.ts | 8 ++++---- functions/src/on-create-liquidity-provision.ts | 8 ++++---- functions/src/on-update-user.ts | 4 ++-- functions/src/update-metrics.ts | 6 ++---- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/functions/src/cancel-bet.ts b/functions/src/cancel-bet.ts index d29a6cee..0b7a42aa 100644 --- a/functions/src/cancel-bet.ts +++ b/functions/src/cancel-bet.ts @@ -10,7 +10,7 @@ const bodySchema = z.object({ export const cancelbet = newEndpoint({}, async (req, auth) => { const { betId } = validate(bodySchema, req.body) - const result = await firestore.runTransaction(async (trans) => { + return await firestore.runTransaction(async (trans) => { const snap = await trans.get( firestore.collectionGroup('bets').where('id', '==', betId) ) @@ -28,8 +28,6 @@ export const cancelbet = newEndpoint({}, async (req, auth) => { return { ...bet, isCancelled: true } }) - - return result }) const firestore = admin.firestore() diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index c8cfc7c4..d58141a5 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -120,7 +120,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { let group = null if (groupId) { - const groupDocRef = await firestore.collection('groups').doc(groupId) + const groupDocRef = firestore.collection('groups').doc(groupId) const groupDoc = await groupDocRef.get() if (!groupDoc.exists) { throw new APIError(400, 'No group exists with the given group ID.') diff --git a/functions/src/on-create-answer.ts b/functions/src/on-create-answer.ts index af4690b0..6af5e699 100644 --- a/functions/src/on-create-answer.ts +++ b/functions/src/on-create-answer.ts @@ -10,14 +10,14 @@ export const onCreateAnswer = functions.firestore contractId: string } const { eventId } = context - const contract = await getContract(contractId) - if (!contract) - throw new Error('Could not find contract corresponding with answer') - const answer = change.data() as Answer // Ignore ante answer. if (answer.number === 0) return + const contract = await getContract(contractId) + if (!contract) + throw new Error('Could not find contract corresponding with answer') + const answerCreator = await getUser(answer.userId) if (!answerCreator) throw new Error('Could not find answer creator') diff --git a/functions/src/on-create-liquidity-provision.ts b/functions/src/on-create-liquidity-provision.ts index ba17f3e7..6ec092a5 100644 --- a/functions/src/on-create-liquidity-provision.ts +++ b/functions/src/on-create-liquidity-provision.ts @@ -8,14 +8,14 @@ export const onCreateLiquidityProvision = functions.firestore .onCreate(async (change, context) => { const liquidity = change.data() as LiquidityProvision const { eventId } = context - const contract = await getContract(liquidity.contractId) - - if (!contract) - throw new Error('Could not find contract corresponding with liquidity') // Ignore Manifold Markets liquidity for now - users see a notification for free market liquidity provision if (liquidity.userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2') return + const contract = await getContract(liquidity.contractId) + if (!contract) + throw new Error('Could not find contract corresponding with liquidity') + const liquidityProvider = await getUser(liquidity.userId) if (!liquidityProvider) throw new Error('Could not find liquidity provider') diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts index f5558730..a76132b5 100644 --- a/functions/src/on-update-user.ts +++ b/functions/src/on-update-user.ts @@ -103,8 +103,8 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { description: `Referred new user id: ${user.id} for ${REFERRAL_AMOUNT}`, } - const txnDoc = await firestore.collection(`txns/`).doc(txn.id) - await transaction.set(txnDoc, txn) + const txnDoc = firestore.collection(`txns/`).doc(txn.id) + transaction.set(txnDoc, txn) console.log('created referral with txn id:', txn.id) // We're currently not subtracting M$ from the house, not sure if we want to for accounting purposes. transaction.update(referredByUserDoc, { diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts index 76570f54..cc9f8ebe 100644 --- a/functions/src/update-metrics.ts +++ b/functions/src/update-metrics.ts @@ -11,8 +11,6 @@ import { last } from 'lodash' const firestore = admin.firestore() -const oneDay = 1000 * 60 * 60 * 24 - const computeInvestmentValue = ( bets: Bet[], contractsDict: { [k: string]: Contract } @@ -59,8 +57,8 @@ export const updateMetricsCore = async () => { return { doc: firestore.collection('contracts').doc(contract.id), fields: { - volume24Hours: computeVolume(contractBets, now - oneDay), - volume7Days: computeVolume(contractBets, now - oneDay * 7), + volume24Hours: computeVolume(contractBets, now - DAY_MS), + volume7Days: computeVolume(contractBets, now - DAY_MS * 7), }, } }) From 9840742927f861ad2f92156eda34338003b6fa05 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 24 Jul 2022 02:30:28 -0700 Subject: [PATCH 345/519] Fix overaggressive emulator running in dev.sh --- dev.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dev.sh b/dev.sh index 178e20e8..ca3246ac 100755 --- a/dev.sh +++ b/dev.sh @@ -4,12 +4,10 @@ ENV=${1:-dev} case $ENV in dev) FIREBASE_PROJECT=dev - NEXT_ENV=DEV - EMULATOR=false ;; + NEXT_ENV=DEV ;; prod) FIREBASE_PROJECT=prod - NEXT_ENV=PROD - EMULATOR=false ;; + NEXT_ENV=PROD ;; localdb) FIREBASE_PROJECT=dev NEXT_ENV=DEV From e389f4cc3be1e293d51d62a023c23fd529194941 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 24 Jul 2022 22:50:33 -0700 Subject: [PATCH 346/519] referrals text --- web/pages/referrals.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx index f50c2e2b..b2666309 100644 --- a/web/pages/referrals.tsx +++ b/web/pages/referrals.tsx @@ -53,7 +53,7 @@ export default function ReferralsPage() { <InfoBox title="FYI" className="mt-4 max-w-md" - text="You can also earn the referral bonus from sharing the link to any market or group you've created!" + text="You can also earn the referral bonus using the share link to any market or group!" /> </Col> </Col> From df91310d0fc23f2d7d2c2a03231da6618a091a9e Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 24 Jul 2022 23:28:05 -0700 Subject: [PATCH 347/519] PlayMoneyDisclaimer; hide limit orders for signed out users; infobox styling --- web/components/bet-panel.tsx | 62 ++++++++++++++++++++++++------------ web/components/info-box.tsx | 2 +- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index c638fcde..7a918948 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -42,6 +42,7 @@ import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBets } from './limit-bets' import { PillButton } from './buttons/pill-button' import { YesNoSelector } from './yes-no-selector' +import { InfoBox } from './info-box' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -72,6 +73,7 @@ export function BetPanel(props: { <QuickOrLimitBet isLimitOrder={isLimitOrder} setIsLimitOrder={setIsLimitOrder} + hideToggle={!user} /> <BuyPanel hidden={isLimitOrder} @@ -85,7 +87,10 @@ export function BetPanel(props: { user={user} unfilledBets={unfilledBets} /> + <SignUpPrompt /> + + {!user && <PlayMoneyDisclaimer />} </Col> {unfilledBets.length > 0 && ( <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> @@ -94,6 +99,14 @@ export function BetPanel(props: { ) } +const PlayMoneyDisclaimer = () => ( + <InfoBox + title="M$ are play-money" + className="mt-4 max-w-md" + text="Manifold Dollars are the play currency used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" + /> +) + export function SimpleBetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract className?: string @@ -124,6 +137,7 @@ export function SimpleBetPanel(props: { <QuickOrLimitBet isLimitOrder={isLimitOrder} setIsLimitOrder={setIsLimitOrder} + hideToggle={!user} /> <BuyPanel hidden={isLimitOrder} @@ -140,7 +154,10 @@ export function SimpleBetPanel(props: { unfilledBets={unfilledBets} onBuySuccess={onBetSuccess} /> + <SignUpPrompt /> + + {!user && <PlayMoneyDisclaimer />} </Col> {unfilledBets.length > 0 && ( @@ -688,32 +705,35 @@ function LimitOrderPanel(props: { function QuickOrLimitBet(props: { isLimitOrder: boolean setIsLimitOrder: (isLimitOrder: boolean) => void + hideToggle?: boolean }) { - const { isLimitOrder, setIsLimitOrder } = props + const { isLimitOrder, setIsLimitOrder, hideToggle } = props return ( <Row className="align-center mb-4 justify-between"> <div className="text-4xl">Bet</div> - <Row className="mt-1 items-center gap-2"> - <PillButton - selected={!isLimitOrder} - onSelect={() => { - setIsLimitOrder(false) - track('select quick order') - }} - > - Quick - </PillButton> - <PillButton - selected={isLimitOrder} - onSelect={() => { - setIsLimitOrder(true) - track('select limit order') - }} - > - Limit - </PillButton> - </Row> + {!hideToggle && ( + <Row className="mt-1 items-center gap-2"> + <PillButton + selected={!isLimitOrder} + onSelect={() => { + setIsLimitOrder(false) + track('select quick order') + }} + > + Quick + </PillButton> + <PillButton + selected={isLimitOrder} + onSelect={() => { + setIsLimitOrder(true) + track('select limit order') + }} + > + Limit + </PillButton> + </Row> + )} </Row> ) } diff --git a/web/components/info-box.tsx b/web/components/info-box.tsx index 34f65089..21bcde47 100644 --- a/web/components/info-box.tsx +++ b/web/components/info-box.tsx @@ -20,7 +20,7 @@ export function InfoBox(props: { </div> <div className="ml-3"> <h3 className="text-sm font-medium text-black">{title}</h3> - <div className="mt-2 text-sm text-black"> + <div className="mt-2 text-sm text-gray-600"> <Linkify text={text} /> </div> </div> From d982d0332cd2d43d9980cb4035a86bef2efc574c Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 24 Jul 2022 23:38:57 -0700 Subject: [PATCH 348/519] play money wording --- web/components/bet-panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 7a918948..eadcae45 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -101,9 +101,9 @@ export function BetPanel(props: { const PlayMoneyDisclaimer = () => ( <InfoBox - title="M$ are play-money" + title="It's play-money" className="mt-4 max-w-md" - text="Manifold Dollars are the play currency used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" + text="Manifold Dollars (M$) are the play currency used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" /> ) From d82c7d7f3e91bc2d11d86d4ad78c198961db556d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 25 Jul 2022 12:22:38 -0700 Subject: [PATCH 349/519] =?UTF-8?q?=E2=80=9Cadded=20liquidity=E2=80=9D=20?= =?UTF-8?q?=E2=87=92=20=E2=80=9Cadded=20a=20subsidy=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/components/feed/feed-liquidity.tsx | 3 +-- web/pages/notifications.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/components/feed/feed-liquidity.tsx b/web/components/feed/feed-liquidity.tsx index cfce3861..0ed06046 100644 --- a/web/components/feed/feed-liquidity.tsx +++ b/web/components/feed/feed-liquidity.tsx @@ -77,8 +77,7 @@ export function LiquidityStatusText(props: { ) : ( <span>{isSelf ? 'You' : 'A trader'}</span> )}{' '} - {bought} {money} - {' of liquidity'} + {bought} a subsidy of {money} <RelativeTimestamp time={createdTime} /> </div> ) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 72754d32..8076f71f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -957,7 +957,7 @@ function getReasonForShowingNotification( reasonText = 'followed you' break case 'liquidity': - reasonText = 'added liquidity to your question' + reasonText = 'added a subsidy to your question' break case 'group': reasonText = 'added you to the group' From d8f96876a0f42ac7561bb56933b433fff4ffd50a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 25 Jul 2022 12:29:29 -0700 Subject: [PATCH 350/519] PlayMoneyDisclaimer copy; hide order book for signed out users --- web/components/bet-panel.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index eadcae45..0e3bb6fd 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -92,7 +92,8 @@ export function BetPanel(props: { {!user && <PlayMoneyDisclaimer />} </Col> - {unfilledBets.length > 0 && ( + + {user && unfilledBets.length > 0 && ( <LimitBets className="mt-4" contract={contract} bets={unfilledBets} /> )} </Col> @@ -101,9 +102,9 @@ export function BetPanel(props: { const PlayMoneyDisclaimer = () => ( <InfoBox - title="It's play-money" + title="Play-money betting" className="mt-4 max-w-md" - text="Manifold Dollars (M$) are the play currency used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" + text="Mana (M$) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" /> ) From e4f8c14fab5ceb776708445981832eb9ff33b536 Mon Sep 17 00:00:00 2001 From: TrueMilli <61841994+TrueMilli@users.noreply.github.com> Date: Mon, 25 Jul 2022 21:51:51 +0200 Subject: [PATCH 351/519] Image compression (#689) * added image compression * removed TODO --- web/lib/firebase/storage.ts | 14 +++++++++++++- web/package.json | 1 + yarn.lock | 12 ++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index 4918a99c..fcf4422d 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -1,8 +1,8 @@ import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage' +import imageCompression from 'browser-image-compression' import { nanoid } from 'nanoid' import { storage } from './init' -// TODO: compress large images export const uploadImage = async ( username: string, file: File, @@ -12,6 +12,18 @@ export const uploadImage = async ( const [, ext] = file.name.split('.') const filename = `${nanoid(10)}.${ext}` const storageRef = ref(storage, `user-images/${username}/${filename}`) + + if (file.size > 20 * 1024 ** 2) { + return Promise.reject('File is over 20 MB.') + } + + if (file.size > 1024 ** 2) { + file = await imageCompression(file, { + maxSizeMB: 1, + maxWidthOrHeight: 1920, + }) + } + const uploadTask = uploadBytesResumable(storageRef, file) let resolvePromise: (url: string) => void diff --git a/web/package.json b/web/package.json index a31dbffa..4fba3359 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "@tiptap/react": "2.0.0-beta.114", "@tiptap/starter-kit": "2.0.0-beta.190", "algoliasearch": "4.13.0", + "browser-image-compression": "2.0.0", "clsx": "1.1.1", "cors": "2.8.5", "daisyui": "1.16.4", diff --git a/yarn.lock b/yarn.lock index dea9e29b..9334b737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4270,6 +4270,13 @@ broadcast-channel@^3.4.1: rimraf "3.0.2" unload "2.2.0" +browser-image-compression@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-2.0.0.tgz#f421381a76d474d4da7dcd82810daf595b09bef6" + integrity sha512-kBlkZo13yOOfcmrPW0M0K/UdZPogIQj2gRvXIM3FktAnfW6VRq9aY2RI+F6O0x6DMj1Xm+WLGgWcFK8Fu/ddnw== + dependencies: + uzip "0.20201231.0" + browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3: version "4.20.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" @@ -11630,6 +11637,11 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uzip@0.20201231.0: + version "0.20201231.0" + resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14" + integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" From 64462d6ab4fb29fd9962ab123de5de82acf84db9 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 25 Jul 2022 13:27:09 -0700 Subject: [PATCH 352/519] Make tabs components better (#691) * Make better tabs components, apply to user page * Remove fishy unused href property from tabs * Remove tab ID property * Clean up crufty markup in tabs component * Fix naming to be right (thanks James!) --- web/components/layout/tabs.tsx | 141 +++++++++++++++++++++------------ web/components/user-page.tsx | 34 +++----- web/pages/[username]/index.tsx | 9 +-- 3 files changed, 102 insertions(+), 82 deletions(-) diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index da3593a1..a87c6607 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -1,82 +1,121 @@ import clsx from 'clsx' -import Link from 'next/link' +import { useRouter, NextRouter } from 'next/router' import { ReactNode, useState } from 'react' -import { Row } from './row' import { track } from '@amplitude/analytics-browser' type Tab = { title: string tabIcon?: ReactNode content: ReactNode - // If set, change the url to this href when the tab is selected - href?: string // If set, show a badge with this content badge?: string } -export function Tabs(props: { +type TabProps = { tabs: Tab[] - defaultIndex?: number labelClassName?: string onClick?: (tabTitle: string, index: number) => void className?: string currentPageForAnalytics?: string -}) { +} + +export function ControlledTabs(props: TabProps & { activeIndex: number }) { const { tabs, - defaultIndex, + activeIndex, labelClassName, onClick, className, currentPageForAnalytics, } = props - const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case - return ( <> - <div className={clsx('mb-4 border-b border-gray-200', className)}> - <nav className="-mb-px flex space-x-8" aria-label="Tabs"> - {tabs.map((tab, i) => ( - <Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> - <a - id={`tab-${i}`} - key={tab.title} - onClick={(e) => { - track('Clicked Tab', { - title: tab.title, - href: tab.href, - currentPage: currentPageForAnalytics, - }) - if (!tab.href) { - e.preventDefault() - } - setActiveIndex(i) - onClick?.(tab.title, i) - }} - className={clsx( - activeIndex === i - ? 'border-indigo-500 text-indigo-600' - : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', - 'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium', - labelClassName - )} - aria-current={activeIndex === i ? 'page' : undefined} - > - <Row className={'items-center justify-center gap-1'}> - {tab.tabIcon && <span> {tab.tabIcon}</span>} - {tab.badge ? ( - <div className="px-0.5 font-bold">{tab.badge}</div> - ) : null} - {tab.title} - </Row> - </a> - </Link> - ))} - </nav> - </div> - + <nav + className={clsx('mb-4 space-x-8 border-b border-gray-200', className)} + aria-label="Tabs" + > + {tabs.map((tab, i) => ( + <a + href="#" + key={tab.title} + onClick={(e) => { + e.preventDefault() + track('Clicked Tab', { + title: tab.title, + currentPage: currentPageForAnalytics, + }) + onClick?.(tab.title, i) + }} + className={clsx( + activeIndex === i + ? 'border-indigo-500 text-indigo-600' + : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', + 'inline-flex cursor-pointer flex-row gap-1 whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium', + labelClassName + )} + aria-current={activeIndex === i ? 'page' : undefined} + > + {tab.tabIcon && <span>{tab.tabIcon}</span>} + {tab.badge ? ( + <span className="px-0.5 font-bold">{tab.badge}</span> + ) : null} + {tab.title} + </a> + ))} + </nav> {activeTab?.content} </> ) } + +export function UncontrolledTabs(props: TabProps & { defaultIndex?: number }) { + const { defaultIndex, onClick, ...rest } = props + const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) + return ( + <ControlledTabs + {...rest} + activeIndex={activeIndex} + onClick={(title, i) => { + setActiveIndex(i) + onClick?.(title, i) + }} + /> + ) +} + +const isTabSelected = (router: NextRouter, queryParam: string, tab: Tab) => { + const selected = router.query[queryParam] + if (typeof selected === 'string') { + return tab.title.toLowerCase() === selected + } else { + return false + } +} + +export function QueryUncontrolledTabs( + props: TabProps & { defaultIndex?: number } +) { + const { tabs, defaultIndex, onClick, ...rest } = props + const router = useRouter() + const selectedIdx = tabs.findIndex((t) => isTabSelected(router, 'tab', t)) + const activeIndex = selectedIdx !== -1 ? selectedIdx : defaultIndex ?? 0 + return ( + <ControlledTabs + {...rest} + tabs={tabs} + activeIndex={activeIndex} + onClick={(title, i) => { + router.replace( + { query: { ...router.query, tab: title.toLowerCase() } }, + undefined, + { shallow: true } + ) + onClick?.(title, i) + }} + /> + ) +} + +// legacy code that didn't know about any other kind of tabs imports this +export const Tabs = UncontrolledTabs diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 09c28920..fc89a285 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -22,7 +22,7 @@ import { Linkify } from './linkify' import { Spacer } from './layout/spacer' import { Row } from './layout/row' import { genHash } from 'common/util/random' -import { Tabs } from './layout/tabs' +import { QueryUncontrolledTabs } from './layout/tabs' import { UserCommentsList } from './comments-list' import { useWindowSize } from 'web/hooks/use-window-size' import { Comment, getUsersComments } from 'web/lib/firebase/comments' @@ -64,12 +64,8 @@ export function UserLink(props: { export const TAB_IDS = ['markets', 'comments', 'bets', 'groups'] const JUNE_1_2022 = new Date('2022-06-01T00:00:00.000Z').valueOf() -export function UserPage(props: { - user: User - currentUser?: User - defaultTabTitle?: string | undefined -}) { - const { user, currentUser, defaultTabTitle } = props +export function UserPage(props: { user: User; currentUser?: User }) { + const { user, currentUser } = props const router = useRouter() const isCurrentUser = user.id === currentUser?.id const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) @@ -276,29 +272,17 @@ export function UserPage(props: { <Spacer h={10} /> {usersContracts !== 'loading' && contractsById && usersComments ? ( - <Tabs + <QueryUncontrolledTabs currentPageForAnalytics={'profile'} labelClassName={'pb-2 pt-1 '} - defaultIndex={ - defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0 - } - onClick={(tabName) => { - const tabId = tabName.toLowerCase() - const subpath = tabId === 'markets' ? '' : '?tab=' + tabId - // BUG: if you start on `/Bob/bets`, then click on Markets, use-query-and-sort-params - // rewrites the url incorrectly to `/Bob/bets` instead of `/Bob` - router.push(`/${user.username}${subpath}`, undefined, { - shallow: true, - }) - }} tabs={[ { title: 'Markets', content: <CreatorContractsList creator={user} />, tabIcon: ( - <div className="px-0.5 font-bold"> + <span className="px-0.5 font-bold"> {usersContracts.length} - </div> + </span> ), }, { @@ -311,7 +295,9 @@ export function UserPage(props: { /> ), tabIcon: ( - <div className="px-0.5 font-bold">{usersComments.length}</div> + <span className="px-0.5 font-bold"> + {usersComments.length} + </span> ), }, { @@ -329,7 +315,7 @@ export function UserPage(props: { /> </div> ), - tabIcon: <div className="px-0.5 font-bold">{betCount}</div>, + tabIcon: <span className="px-0.5 font-bold">{betCount}</span>, }, ]} /> diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index 3c44a5cc..22083c90 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -31,9 +31,8 @@ export default function UserProfile(props: { user: User | null }) { const { user } = props const router = useRouter() - const { username, tab } = router.query as { + const { username } = router.query as { username: string - tab?: string | undefined } const currentUser = useUser() @@ -42,11 +41,7 @@ export default function UserProfile(props: { user: User | null }) { if (user === undefined) return <div /> return user ? ( - <UserPage - user={user} - currentUser={currentUser || undefined} - defaultTabTitle={tab} - /> + <UserPage user={user} currentUser={currentUser || undefined} /> ) : ( <Custom404 /> ) From 06948bb98b3e66aa8f715f3806023599c27d47ab Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 25 Jul 2022 16:37:23 -0700 Subject: [PATCH 353/519] Make `setNotificationsAsSeen` return a promise (#692) --- web/pages/notifications.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 8076f71f..9f076c41 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -726,18 +726,14 @@ function NotificationItem(props: { ) } -export const setNotificationsAsSeen = (notifications: Notification[]) => { - notifications.forEach((notification) => { - if (!notification.isSeen) - updateDoc( - doc(db, `users/${notification.userId}/notifications/`, notification.id), - { - isSeen: true, - viewTime: new Date(), - } - ) - }) - return notifications +export const setNotificationsAsSeen = async (notifications: Notification[]) => { + const unseenNotifications = notifications.filter((n) => !n.isSeen) + return await Promise.all( + unseenNotifications.map((n) => { + const notificationDoc = doc(db, `users/${n.userId}/notifications/`, n.id) + return updateDoc(notificationDoc, { isSeen: true, viewTime: new Date() }) + }) + ) } function QuestionOrGroupLink(props: { From 24124ac86a1528db5e97a16fa42c8aec9cf0c509 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 25 Jul 2022 17:45:33 -0700 Subject: [PATCH 354/519] show sign up button on mobile on market page --- web/components/bet-panel.tsx | 10 +-------- web/components/feed/contract-activity.tsx | 1 + web/components/feed/feed-items.tsx | 25 +++++++++++++++++------ web/components/play-money-disclaimer.tsx | 9 ++++++++ 4 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 web/components/play-money-disclaimer.tsx diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 0e3bb6fd..7a9b77e4 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -42,7 +42,7 @@ import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBets } from './limit-bets' import { PillButton } from './buttons/pill-button' import { YesNoSelector } from './yes-no-selector' -import { InfoBox } from './info-box' +import { PlayMoneyDisclaimer } from './play-money-disclaimer' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -100,14 +100,6 @@ export function BetPanel(props: { ) } -const PlayMoneyDisclaimer = () => ( - <InfoBox - title="Play-money betting" - className="mt-4 max-w-md" - text="Mana (M$) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" - /> -) - export function SimpleBetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract className?: string diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index e50eb76b..b1c8f6ee 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -46,6 +46,7 @@ export function ContractActivity(props: { items={items} className={className} betRowClassName={betRowClassName} + user={user} /> ) } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index ea8302b8..b1cd765c 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -36,14 +36,18 @@ import { import { FeedBet } from 'web/components/feed/feed-bets' import { CPMMBinaryContract, NumericContract } from 'common/contract' import { FeedLiquidity } from './feed-liquidity' +import { SignUpPrompt } from '../sign-up-prompt' +import { User } from 'common/user' +import { PlayMoneyDisclaimer } from '../play-money-disclaimer' export function FeedItems(props: { contract: Contract items: ActivityItem[] className?: string betRowClassName?: string + user: User | null | undefined }) { - const { contract, items, className, betRowClassName } = props + const { contract, items, className, betRowClassName, user } = props const { outcomeType } = contract const [elem, setElem] = useState<HTMLElement | null>(null) @@ -67,11 +71,20 @@ export function FeedItems(props: { </div> ))} </div> - {outcomeType === 'BINARY' && tradingAllowed(contract) && ( - <BetRow - contract={contract as CPMMBinaryContract} - className={clsx('mb-2', betRowClassName)} - /> + + {!user ? ( + <Col className="mt-4 max-w-sm items-center xl:hidden"> + <SignUpPrompt /> + <PlayMoneyDisclaimer /> + </Col> + ) : ( + outcomeType === 'BINARY' && + tradingAllowed(contract) && ( + <BetRow + contract={contract as CPMMBinaryContract} + className={clsx('mb-2', betRowClassName)} + /> + ) )} </div> ) diff --git a/web/components/play-money-disclaimer.tsx b/web/components/play-money-disclaimer.tsx new file mode 100644 index 00000000..6ee16c1e --- /dev/null +++ b/web/components/play-money-disclaimer.tsx @@ -0,0 +1,9 @@ +import { InfoBox } from './info-box' + +export const PlayMoneyDisclaimer = () => ( + <InfoBox + title="Play-money betting" + className="mt-4 max-w-md" + text="Mana (M$) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!" + /> +) From 3107c8fe3042f619294741064b0391112ae6a634 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 25 Jul 2022 18:11:29 -0700 Subject: [PATCH 355/519] large bet warning --- web/components/alert-box.tsx | 26 ++++++++++++++------------ web/components/bet-panel.tsx | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/web/components/alert-box.tsx b/web/components/alert-box.tsx index a8306583..b908b180 100644 --- a/web/components/alert-box.tsx +++ b/web/components/alert-box.tsx @@ -1,24 +1,26 @@ import { ExclamationIcon } from '@heroicons/react/solid' +import { Col } from './layout/col' +import { Row } from './layout/row' import { Linkify } from './linkify' export function AlertBox(props: { title: string; text: string }) { const { title, text } = props return ( - <div className="rounded-md bg-yellow-50 p-4"> - <div className="flex"> - <div className="flex-shrink-0"> - <ExclamationIcon - className="h-5 w-5 text-yellow-400" - aria-hidden="true" - /> - </div> + <Col className="rounded-md bg-yellow-50 p-4"> + <Row className="mb-2 flex-shrink-0"> + <ExclamationIcon + className="h-5 w-5 text-yellow-400" + aria-hidden="true" + /> + <div className="ml-3"> <h3 className="text-sm font-medium text-yellow-800">{title}</h3> - <div className="mt-2 text-sm text-yellow-700"> - <Linkify text={text} /> - </div> </div> + </Row> + + <div className="mt-2 whitespace-pre-line text-sm text-yellow-700"> + <Linkify text={text} /> </div> - </div> + </Col> ) } diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 7a9b77e4..4d27918b 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -43,6 +43,7 @@ import { LimitBets } from './limit-bets' import { PillButton } from './buttons/pill-button' import { YesNoSelector } from './yes-no-selector' import { PlayMoneyDisclaimer } from './play-money-disclaimer' +import { AlertBox } from './alert-box' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -264,6 +265,8 @@ function BuyPanel(props: { const format = getFormattedMappedValue(contract) + const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) + return ( <Col className={hidden ? 'hidden' : ''}> <div className="my-3 text-left text-sm text-gray-500"> @@ -287,6 +290,22 @@ function BuyPanel(props: { disabled={isSubmitting} inputRef={inputRef} /> + + {(betAmount ?? 0) > 10 && + bankrollFraction >= 0.5 && + bankrollFraction <= 1 ? ( + <AlertBox + title="Whoa, there!" + text={`You might not want to spend ${formatPercent( + bankrollFraction + )} of your balance on a single bet. \n\nCurrent balance: ${formatMoney( + user?.balance ?? 0 + )}`} + /> + ) : ( + '' + )} + <Col className="mt-3 w-full gap-3"> <Row className="items-center justify-between text-sm"> <div className="text-gray-500"> From ec0e25e5ed674e0124189269f313418b94adce45 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 25 Jul 2022 18:25:23 -0700 Subject: [PATCH 356/519] create user: remove ip check --- functions/src/create-user.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 1f413b6d..ab7c8e9a 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -63,10 +63,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => { const deviceUsedBefore = !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) - const ipCount = req.ip ? await numberUsersWithIp(req.ip) : 0 - - const balance = - deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE + const balance = deviceUsedBefore ? SUS_STARTING_BALANCE : STARTING_BALANCE const user: User = { id: auth.uid, @@ -113,7 +110,7 @@ const isPrivateUserWithDeviceToken = async (deviceToken: string) => { return !snap.empty } -const numberUsersWithIp = async (ipAddress: string) => { +export const numberUsersWithIp = async (ipAddress: string) => { const snap = await firestore .collection('private-users') .where('initialIpAddress', '==', ipAddress) From af25a6c7951f8046ae2bf9e5fc9c3d1fc2cd99b0 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 25 Jul 2022 18:27:43 -0700 Subject: [PATCH 357/519] Allow adding multiple contracts to group in modal --- web/components/contract-search.tsx | 28 +++++-- web/components/contract/contract-card.tsx | 15 +++- web/components/contract/contract-details.tsx | 5 +- web/components/contract/contracts-list.tsx | 26 +++++- web/components/layout/modal.tsx | 2 +- web/lib/firebase/contracts.ts | 1 - web/lib/firebase/groups.ts | 87 +++++++++++--------- web/pages/group/[...slugs]/index.tsx | 84 ++++++++++++++----- 8 files changed, 171 insertions(+), 77 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 45145c54..c7660138 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -15,7 +15,10 @@ import { useInitialQueryAndSort, useUpdateQueryAndSort, } from '../hooks/use-sort-and-query-params' -import { ContractsGrid } from './contract/contracts-list' +import { + ContractHighlightOptions, + ContractsGrid, +} from './contract/contracts-list' import { Row } from './layout/row' import { useEffect, useMemo, useRef, useState } from 'react' import { Spacer } from './layout/spacer' @@ -64,11 +67,15 @@ export function ContractSearch(props: { excludeContractIds?: string[] groupSlug?: string } + highlightOptions?: ContractHighlightOptions onContractClick?: (contract: Contract) => void showPlaceHolder?: boolean hideOrderSelector?: boolean overrideGridClassName?: string - hideQuickBet?: boolean + cardHideOptions?: { + hideGroupLink?: boolean + hideQuickBet?: boolean + } }) { const { querySortOptions, @@ -77,7 +84,8 @@ export function ContractSearch(props: { overrideGridClassName, hideOrderSelector, showPlaceHolder, - hideQuickBet, + cardHideOptions, + highlightOptions, } = props const user = useUser() @@ -276,8 +284,9 @@ export function ContractSearch(props: { querySortOptions={querySortOptions} onContractClick={onContractClick} overrideGridClassName={overrideGridClassName} - hideQuickBet={hideQuickBet} excludeContractIds={additionalFilter?.excludeContractIds} + highlightOptions={highlightOptions} + cardHideOptions={cardHideOptions} /> )} </InstantSearch> @@ -293,13 +302,19 @@ export function ContractSearchInner(props: { overrideGridClassName?: string hideQuickBet?: boolean excludeContractIds?: string[] + highlightOptions?: ContractHighlightOptions + cardHideOptions?: { + hideQuickBet?: boolean + hideGroupLink?: boolean + } }) { const { querySortOptions, onContractClick, overrideGridClassName, - hideQuickBet, + cardHideOptions, excludeContractIds, + highlightOptions, } = props const { initialQuery } = useInitialQueryAndSort(querySortOptions) @@ -360,7 +375,8 @@ export function ContractSearchInner(props: { showTime={showTime} onContractClick={onContractClick} overrideGridClassName={overrideGridClassName} - hideQuickBet={hideQuickBet} + highlightOptions={highlightOptions} + cardHideOptions={cardHideOptions} /> ) } diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 30c54363..f3f9807c 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -5,8 +5,8 @@ import { formatLargeNumber, formatPercent } from 'common/util/format' import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { - Contract, BinaryContract, + Contract, FreeResponseContract, NumericContract, PseudoNumericContract, @@ -24,7 +24,7 @@ import { } from 'common/calculate' import { AvatarDetails, MiscDetails, ShowTime } from './contract-details' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' -import { QuickBet, ProbBar, getColor } from './quick-bet' +import { getColor, ProbBar, QuickBet } from './quick-bet' import { useContractWithPreload } from 'web/hooks/use-contract' import { useUser } from 'web/hooks/use-user' import { track } from '@amplitude/analytics-browser' @@ -38,8 +38,16 @@ export function ContractCard(props: { className?: string onClick?: () => void hideQuickBet?: boolean + hideGroupLink?: boolean }) { - const { showHotVolume, showTime, className, onClick, hideQuickBet } = props + const { + showHotVolume, + showTime, + className, + onClick, + hideQuickBet, + hideGroupLink, + } = props const contract = useContractWithPreload(props.contract) ?? props.contract const { question, outcomeType } = contract const { resolution } = contract @@ -121,6 +129,7 @@ export function ContractCard(props: { contract={contract} showHotVolume={showHotVolume} showTime={showTime} + hideGroupLink={hideGroupLink} /> </Col> {showQuickBet ? ( diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 83c291c7..7a7242a0 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -42,8 +42,9 @@ export function MiscDetails(props: { contract: Contract showHotVolume?: boolean showTime?: ShowTime + hideGroupLink?: boolean }) { - const { contract, showHotVolume, showTime } = props + const { contract, showHotVolume, showTime, hideGroupLink } = props const { volume, volume24Hours, @@ -80,7 +81,7 @@ export function MiscDetails(props: { <NewContractBadge /> )} - {groupLinks && groupLinks.length > 0 && ( + {!hideGroupLink && groupLinks && groupLinks.length > 0 && ( <SiteLink href={groupPath(groupLinks[0].slug)} className="text-sm text-gray-400" diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-list.tsx index 20a85ef4..c733bd76 100644 --- a/web/components/contract/contracts-list.tsx +++ b/web/components/contract/contracts-list.tsx @@ -1,5 +1,5 @@ -import { Contract } from '../../lib/firebase/contracts' -import { User } from '../../lib/firebase/users' +import { Contract } from 'web/lib/firebase/contracts' +import { User } from 'web/lib/firebase/users' import { Col } from '../layout/col' import { SiteLink } from '../site-link' import { ContractCard } from './contract-card' @@ -9,6 +9,11 @@ import { useIsVisible } from 'web/hooks/use-is-visible' import { useEffect, useState } from 'react' import clsx from 'clsx' +export type ContractHighlightOptions = { + contractIds?: string[] + highlightClassName?: string +} + export function ContractsGrid(props: { contracts: Contract[] loadMore: () => void @@ -16,7 +21,11 @@ export function ContractsGrid(props: { showTime?: ShowTime onContractClick?: (contract: Contract) => void overrideGridClassName?: string - hideQuickBet?: boolean + cardHideOptions?: { + hideQuickBet?: boolean + hideGroupLink?: boolean + } + highlightOptions?: ContractHighlightOptions }) { const { contracts, @@ -25,9 +34,12 @@ export function ContractsGrid(props: { loadMore, onContractClick, overrideGridClassName, - hideQuickBet, + cardHideOptions, + highlightOptions, } = props + const { hideQuickBet, hideGroupLink } = cardHideOptions || {} + const { contractIds, highlightClassName } = highlightOptions || {} const [elem, setElem] = useState<HTMLElement | null>(null) const isBottomVisible = useIsVisible(elem) @@ -66,6 +78,12 @@ export function ContractsGrid(props: { onContractClick ? () => onContractClick(contract) : undefined } hideQuickBet={hideQuickBet} + hideGroupLink={hideGroupLink} + className={ + contractIds?.includes(contract.id) + ? highlightClassName + : undefined + } /> ))} </ul> diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index af2b66de..aac9e4c8 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -26,7 +26,7 @@ export function Modal(props: { className="fixed inset-0 z-50 overflow-y-auto" onClose={setOpen} > - <div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0"> + <div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:p-0"> <Transition.Child as={Fragment} enter="ease-out duration-300" diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 14594803..9e5de871 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -129,7 +129,6 @@ export async function listContractsByGroupSlug( ): Promise<Contract[]> { const q = query(contracts, where('groupSlugs', 'array-contains', slug)) const snapshot = await getDocs(q) - console.log(snapshot.docs.map((doc) => doc.data())) return snapshot.docs.map((doc) => doc.data()) } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index f782f6a8..debc9a97 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -129,56 +129,61 @@ export async function addContractToGroup( contract: Contract, userId: string ) { - if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // already in that group + if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { + const newGroupLinks = [ + ...(contract.groupLinks ?? []), + { + groupId: group.id, + createdTime: Date.now(), + slug: group.slug, + userId, + name: group.name, + } as GroupLink, + ] - const newGroupLinks = [ - ...(contract.groupLinks ?? []), - { - groupId: group.id, - createdTime: Date.now(), - slug: group.slug, - userId, - name: group.name, - } as GroupLink, - ] - - await updateContract(contract.id, { - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - groupLinks: newGroupLinks, - }) - return await updateGroup(group, { - contractIds: uniq([...group.contractIds, contract.id]), - }) - .then(() => group) - .catch((err) => { - console.error('error adding contract to group', err) - return err + await updateContract(contract.id, { + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, }) + } + if (!group.contractIds.includes(contract.id)) { + return await updateGroup(group, { + contractIds: uniq([...group.contractIds, contract.id]), + }) + .then(() => group) + .catch((err) => { + console.error('error adding contract to group', err) + return err + }) + } } export async function removeContractFromGroup( group: Group, contract: Contract ) { - if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // not in that group - - const newGroupLinks = contract.groupLinks?.filter( - (link) => link.slug !== group.slug - ) - await updateContract(contract.id, { - groupSlugs: - contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [], - groupLinks: newGroupLinks ?? [], - }) - const newContractIds = group.contractIds.filter((id) => id !== contract.id) - return await updateGroup(group, { - contractIds: uniq(newContractIds), - }) - .then(() => group) - .catch((err) => { - console.error('error removing contract from group', err) - return err + if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { + const newGroupLinks = contract.groupLinks?.filter( + (link) => link.slug !== group.slug + ) + await updateContract(contract.id, { + groupSlugs: + contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [], + groupLinks: newGroupLinks ?? [], }) + } + + if (group.contractIds.includes(contract.id)) { + const newContractIds = group.contractIds.filter((id) => id !== contract.id) + return await updateGroup(group, { + contractIds: uniq(newContractIds), + }) + .then(() => group) + .catch((err) => { + console.error('error removing contract from group', err) + return err + }) + } } export async function setContractGroupLinks( diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index eebf0619..dd712a36 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -49,6 +49,7 @@ import { useWindowSize } from 'web/hooks/use-window-size' import { CopyLinkButton } from 'web/components/copy-link-button' import { ENV_CONFIG } from 'common/envs/constants' import { useSaveReferral } from 'web/hooks/use-save-referral' +import { Button } from 'web/components/button' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -541,10 +542,26 @@ function GroupLeaderboards(props: { function AddContractButton(props: { group: Group; user: User }) { const { group, user } = props const [open, setOpen] = useState(false) + const [contracts, setContracts] = useState<Contract[]>([]) + const [loading, setLoading] = useState(false) async function addContractToCurrentGroup(contract: Contract) { - await addContractToGroup(group, contract, user.id) - setOpen(false) + if (contracts.map((c) => c.id).includes(contract.id)) { + setContracts(contracts.filter((c) => c.id !== contract.id)) + } else setContracts([...contracts, contract]) + } + + async function doneAddingContracts() { + Promise.all( + contracts.map(async (contract) => { + setLoading(true) + await addContractToGroup(group, contract, user.id) + }) + ).then(() => { + setLoading(false) + setOpen(false) + setContracts([]) + }) } return ( @@ -558,37 +575,66 @@ function AddContractButton(props: { group: Group; user: User }) { </button> </div> - <Modal open={open} setOpen={setOpen} className={'sm:p-0'}> - <Col - className={ - 'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white' - } - > + <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> + <Col className={' w-full gap-4 rounded-md bg-white'}> <Col className="p-8 pb-0"> <div className={'text-xl text-indigo-700'}> Add a question to your group </div> - <Col className="items-center"> - <CreateQuestionButton - user={user} - overrideText={'New question'} - className={'w-48 flex-shrink-0 '} - query={`?groupId=${group.id}`} - /> + {contracts.length === 0 ? ( + <Col className="items-center justify-center"> + <CreateQuestionButton + user={user} + overrideText={'New question'} + className={'w-48 flex-shrink-0 '} + query={`?groupId=${group.id}`} + /> - <div className={'mt-2 text-lg text-indigo-700'}>or</div> - </Col> + <div className={'mt-1 text-lg text-gray-600'}> + (or select old questions) + </div> + </Col> + ) : ( + <Col className={'w-full '}> + {!loading ? ( + <Row className={'justify-end gap-4'}> + <Button onClick={doneAddingContracts} color={'indigo'}> + Add {contracts.length} question + {contracts.length > 1 && 's'} + </Button> + <Button + onClick={() => { + setContracts([]) + }} + color={'gray'} + > + Cancel + </Button> + </Row> + ) : ( + <Row className={'justify-center'}> + <LoadingIndicator /> + </Row> + )} + </Col> + )} </Col> <div className={'overflow-y-scroll sm:px-8'}> <ContractSearch hideOrderSelector={true} onContractClick={addContractToCurrentGroup} - overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'} + overrideGridClassName={ + 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' + } showPlaceHolder={true} - hideQuickBet={true} + cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} additionalFilter={{ excludeContractIds: group.contractIds }} + highlightOptions={{ + contractIds: contracts.map((c) => c.id), + highlightClassName: '!bg-indigo-100 border-indigo-100 border-2', + }} /> </div> </Col> From 0c2bcceae2ecb69333885491a62dc405f7fb5244 Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Tue, 26 Jul 2022 11:10:22 +0900 Subject: [PATCH 358/519] Update charity.ts (#695) Added Founder's Pledge Global Health and Development Fund. Made new logos for all the Founder's Pledge charities to help distinguish between them. --- common/charity.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/common/charity.ts b/common/charity.ts index f1223b04..c18c6ba1 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -169,7 +169,7 @@ export const charities: Charity[] = [ { name: "Founder's Pledge Climate Change Fund", website: 'https://founderspledge.com/funds/climate-change-fund', - photo: 'https://i.imgur.com/ZAhzHu4.png', + photo: 'https://i.imgur.com/9turaJW.png', preview: 'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.', description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally. @@ -183,7 +183,7 @@ export const charities: Charity[] = [ { name: "Founder's Pledge Patient Philanthropy Fund", website: 'https://founderspledge.com/funds/patient-philanthropy-fund', - photo: 'https://i.imgur.com/ZAhzHu4.png', + photo: 'https://i.imgur.com/LLR6CI6.png', preview: 'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity', description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future. @@ -551,6 +551,20 @@ With an emphasis on approval voting, we bring better elections to people across The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that don’t represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`, }, + { + name: 'Founders Pledge Global Health and Development Fund', + website: 'https://founderspledge.com/funds/global-health-and-development', + photo: 'https://i.imgur.com/EXbxH7T.png', + preview: + 'Tackling the vast global inequalities in health, wealth and opportunity', + description: `Nearly half the world lives on less than $2.50 a day, yet giving by the world’s richest often overlooks the world’s poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally. + +This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to: + +Improve the lives of the world's most vulnerable people. +Reduce the number of easily preventable deaths worldwide. +Work towards sustainable, systemic change.`, + }, ].map((charity) => { const slug = charity.name.toLowerCase().replace(/\s/g, '-') return { From 7e4f4b9a87e7601b4158300e7808a1547b41b4df Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 26 Jul 2022 00:10:11 -0700 Subject: [PATCH 359/519] Clean up a bunch of crufty stuff on user comments list (#693) --- web/components/comments-list.tsx | 75 +++++++++++++++----------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index ab9ed523..f8e1d7e1 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -17,58 +17,55 @@ export function UserCommentsList(props: { contractsById: { [id: string]: Contract } }) { const { comments, contractsById } = props - const commentsByContract = groupBy(comments, 'contractId') - const contractCommentPairs = Object.entries(commentsByContract) - .map( - ([contractId, comments]) => [contractsById[contractId], comments] as const - ) - .filter(([contract]) => contract) + // we don't show comments in groups here atm, just comments on contracts + const contractComments = comments.filter((c) => c.contractId) + const commentsByContract = groupBy(contractComments, 'contractId') return ( <Col className={'bg-white'}> - {contractCommentPairs.map(([contract, comments]) => ( - <div key={contract.id} className={'border-width-1 border-b p-5'}> - <div className={'mb-2 text-sm text-indigo-700'}> - <SiteLink href={contractPath(contract)}> + {Object.entries(commentsByContract).map(([contractId, comments]) => { + const contract = contractsById[contractId] + return ( + <div key={contractId} className={'border-width-1 border-b p-5'}> + <SiteLink + className={'mb-2 block text-sm text-indigo-700'} + href={contractPath(contract)} + > {contract.question} </SiteLink> + {comments.map((comment) => ( + <ProfileComment + key={comment.id} + comment={comment} + className="relative flex items-start space-x-3 pb-6" + /> + ))} </div> - {comments.map((comment) => ( - <div key={comment.id} className={'relative pb-6'}> - <div className="relative flex items-start space-x-3"> - <ProfileComment comment={comment} /> - </div> - </div> - ))} - </div> - ))} + ) + })} </Col> ) } -function ProfileComment(props: { comment: Comment }) { - const { comment } = props +function ProfileComment(props: { comment: Comment; className?: string }) { + const { comment, className } = props const { text, userUsername, userName, userAvatarUrl, createdTime } = comment // TODO: find and attach relevant bets by comment betId at some point return ( - <div> - <Row className={'gap-4'}> - <Avatar username={userUsername} avatarUrl={userAvatarUrl} /> - <div className="min-w-0 flex-1"> - <div> - <p className="mt-0.5 text-sm text-gray-500"> - <UserLink - className="text-gray-500" - username={userUsername} - name={userName} - />{' '} - <RelativeTimestamp time={createdTime} /> - </p> - </div> - <Linkify text={text} /> - </div> - </Row> - </div> + <Row className={className}> + <Avatar username={userUsername} avatarUrl={userAvatarUrl} /> + <div className="min-w-0 flex-1"> + <p className="mt-0.5 text-sm text-gray-500"> + <UserLink + className="text-gray-500" + username={userUsername} + name={userName} + />{' '} + <RelativeTimestamp time={createdTime} /> + </p> + <Linkify text={text} /> + </div> + </Row> ) } From ad46a60c4fed59d7c336dd9d07972be7296ce07e Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 26 Jul 2022 00:10:22 -0700 Subject: [PATCH 360/519] Clean up rendering of user bets list (#694) * Clean up crufty markup in bets list * Don't render bet tables in bets list until expanded * Don't look up unfilled bets for every sell button --- web/components/bets-list.tsx | 285 ++++++++++++++++------------------- 1 file changed, 131 insertions(+), 154 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index a306a020..25cd00ad 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -3,6 +3,7 @@ import { groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' import dayjs from 'dayjs' import { useEffect, useMemo, useState } from 'react' import clsx from 'clsx' +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid' import { Bet } from 'web/lib/firebase/bets' import { User } from 'web/lib/firebase/users' @@ -277,13 +278,7 @@ function ContractBets(props: { bets ) return ( - <div - tabIndex={0} - className={clsx( - 'collapse collapse-arrow relative bg-white p-4 pr-6', - collapsed ? 'collapse-close' : 'collapse-open pb-2' - )} - > + <div tabIndex={0} className="relative bg-white p-4 pr-6"> <Row className="cursor-pointer flex-wrap gap-2" onClick={() => setCollapsed((collapsed) => !collapsed)} @@ -300,10 +295,11 @@ function ContractBets(props: { </Link> {/* Show carrot for collapsing. Hack the positioning. */} - <div - className="collapse-title absolute h-0 min-h-0 w-0 p-0" - style={{ top: -10, right: 0 }} - /> + {collapsed ? ( + <ChevronDownIcon className="absolute top-5 right-4 h-6 w-6" /> + ) : ( + <ChevronUpIcon className="absolute top-5 right-4 h-6 w-6" /> + )} </Row> <Row className="flex-1 items-center gap-2 text-sm text-gray-500"> @@ -335,55 +331,42 @@ function ContractBets(props: { </Row> </Col> - <Row className="mr-5 justify-end sm:mr-8"> - <Col> - <div className="whitespace-nowrap text-right text-lg"> - {formatMoney(metric === 'profit' ? profit : payout)} - </div> - <div className="text-right"> - <ProfitBadge profitPercent={profitPercent} /> - </div> - </Col> - </Row> + <Col className="mr-5 sm:mr-8"> + <div className="whitespace-nowrap text-right text-lg"> + {formatMoney(metric === 'profit' ? profit : payout)} + </div> + <ProfitBadge className="text-right" profitPercent={profitPercent} /> + </Col> </Row> - <div - className="collapse-content !px-0" - style={{ backgroundColor: 'white' }} - > - <Spacer h={8} /> + {!collapsed && ( + <div className="bg-white"> + <BetsSummary + className="mt-8 mr-5 flex-1 sm:mr-8" + contract={contract} + bets={bets} + isYourBets={isYourBets} + /> - <BetsSummary - className="mr-5 flex-1 sm:mr-8" - contract={contract} - bets={bets} - isYourBets={isYourBets} - /> - - <Spacer h={4} /> - - {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( - <> + {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( <div className="max-w-md"> - <div className="bg-gray-50 px-4 py-2">Limit orders</div> + <div className="mt-4 bg-gray-50 px-4 py-2">Limit orders</div> <LimitOrderTable contract={contract} limitBets={limitBets} isYou={true} /> </div> - </> - )} + )} - <Spacer h={4} /> - - <div className="bg-gray-50 px-4 py-2">Bets</div> - <ContractBetsTable - contract={contract} - bets={bets} - isYourBets={isYourBets} - /> - </div> + <div className="mt-4 bg-gray-50 px-4 py-2">Bets</div> + <ContractBetsTable + contract={contract} + bets={bets} + isYourBets={isYourBets} + /> + </div> + )} </div> ) } @@ -427,107 +410,92 @@ export function BetsSummary(props: { return ( <Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}> - <Row className="flex-wrap gap-4 sm:gap-6"> - {!isCpmm && ( - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Invested - </div> - <div className="whitespace-nowrap">{formatMoney(invested)}</div> - </Col> - )} - {resolution ? ( - <Col> - <div className="text-sm text-gray-500">Payout</div> - <div className="whitespace-nowrap"> - {formatMoney(payout)}{' '} - <ProfitBadge profitPercent={profitPercent} /> - </div> - </Col> - ) : ( - <> - {isBinary ? ( - <> - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Payout if <YesLabel /> - </div> - <div className="whitespace-nowrap"> - {formatMoney(yesWinnings)} - </div> - </Col> - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Payout if <NoLabel /> - </div> - <div className="whitespace-nowrap"> - {formatMoney(noWinnings)} - </div> - </Col> - </> - ) : isPseudoNumeric ? ( - <> - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Payout if {'>='} {formatLargeNumber(contract.max)} - </div> - <div className="whitespace-nowrap"> - {formatMoney(yesWinnings)} - </div> - </Col> - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Payout if {'<='} {formatLargeNumber(contract.min)} - </div> - <div className="whitespace-nowrap"> - {formatMoney(noWinnings)} - </div> - </Col> - </> - ) : ( - <Col> - <div className="whitespace-nowrap text-sm text-gray-500"> - Current value - </div> - <div className="whitespace-nowrap">{formatMoney(payout)}</div> - </Col> - )} - </> - )} + {!isCpmm && ( <Col> - <div className="whitespace-nowrap text-sm text-gray-500">Profit</div> + <div className="whitespace-nowrap text-sm text-gray-500"> + Invested + </div> + <div className="whitespace-nowrap">{formatMoney(invested)}</div> + </Col> + )} + {resolution ? ( + <Col> + <div className="text-sm text-gray-500">Payout</div> <div className="whitespace-nowrap"> - {formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} /> - {isYourBets && - isCpmm && - (isBinary || isPseudoNumeric) && - !isClosed && - !resolution && - hasShares && - sharesOutcome && - user && ( - <> - <button - className="btn btn-sm ml-2" - onClick={() => setShowSellModal(true)} - > - Sell - </button> - {showSellModal && ( - <SellSharesModal - contract={contract} - user={user} - userBets={bets} - shares={totalShares[sharesOutcome]} - sharesOutcome={sharesOutcome} - setOpen={setShowSellModal} - /> - )} - </> - )} + {formatMoney(payout)} <ProfitBadge profitPercent={profitPercent} /> </div> </Col> - </Row> + ) : isBinary ? ( + <> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if <YesLabel /> + </div> + <div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div> + </Col> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if <NoLabel /> + </div> + <div className="whitespace-nowrap">{formatMoney(noWinnings)}</div> + </Col> + </> + ) : isPseudoNumeric ? ( + <> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if {'>='} {formatLargeNumber(contract.max)} + </div> + <div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div> + </Col> + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Payout if {'<='} {formatLargeNumber(contract.min)} + </div> + <div className="whitespace-nowrap">{formatMoney(noWinnings)}</div> + </Col> + </> + ) : ( + <Col> + <div className="whitespace-nowrap text-sm text-gray-500"> + Current value + </div> + <div className="whitespace-nowrap">{formatMoney(payout)}</div> + </Col> + )} + <Col> + <div className="whitespace-nowrap text-sm text-gray-500">Profit</div> + <div className="whitespace-nowrap"> + {formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} /> + {isYourBets && + isCpmm && + (isBinary || isPseudoNumeric) && + !isClosed && + !resolution && + hasShares && + sharesOutcome && + user && ( + <> + <button + className="btn btn-sm ml-2" + onClick={() => setShowSellModal(true)} + > + Sell + </button> + {showSellModal && ( + <SellSharesModal + contract={contract} + user={user} + userBets={bets} + shares={totalShares[sharesOutcome]} + sharesOutcome={sharesOutcome} + setOpen={setShowSellModal} + /> + )} + </> + )} + </div> + </Col> </Row> ) } @@ -689,7 +657,13 @@ function BetRow(props: { !isClosed && !isSold && !isAnte && - !isNumeric && <SellButton contract={contract} bet={bet} />} + !isNumeric && ( + <SellButton + contract={contract} + bet={bet} + unfilledBets={unfilledBets} + /> + )} </td> {isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>} <td> @@ -729,8 +703,12 @@ function BetRow(props: { ) } -function SellButton(props: { contract: Contract; bet: Bet }) { - const { contract, bet } = props +function SellButton(props: { + contract: Contract + bet: Bet + unfilledBets: LimitBet[] +}) { + const { contract, bet, unfilledBets } = props const { outcome, shares, loanAmount } = bet const [isSubmitting, setIsSubmitting] = useState(false) @@ -740,8 +718,6 @@ function SellButton(props: { contract: Contract; bet: Bet }) { outcome === 'NO' ? 'YES' : outcome ) - const unfilledBets = useUnfilledBets(contract.id) ?? [] - const outcomeProb = getProbabilityAfterSale( contract, outcome, @@ -787,8 +763,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) { ) } -function ProfitBadge(props: { profitPercent: number }) { - const { profitPercent } = props +function ProfitBadge(props: { profitPercent: number; className?: string }) { + const { profitPercent, className } = props if (!profitPercent) return null const colors = profitPercent > 0 @@ -799,7 +775,8 @@ function ProfitBadge(props: { profitPercent: number }) { <span className={clsx( 'ml-1 inline-flex items-center rounded-full px-3 py-0.5 text-sm font-medium', - colors + colors, + className )} > {(profitPercent > 0 ? '+' : '') + profitPercent.toFixed(1) + '%'} From b506e9654895532e6eef3f9ff787b08e79fb60e3 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 26 Jul 2022 12:47:19 -0700 Subject: [PATCH 361/519] Implement "sell all shares" functionality in `sellshares` and expose API (#696) * Change `sellshares` to be able to sell all shares * Sell all shares properly on bet panel UI * Add API route for selling shares, document --- docs/docs/api.md | 20 ++++++++++++++++++++ functions/src/sell-shares.ts | 7 ++++--- web/components/bet-panel.tsx | 6 ++++-- web/pages/api/v0/market/[id]/sell.ts | 28 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 web/pages/api/v0/market/[id]/sell.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index 667c68b8..8b7dce30 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -579,6 +579,26 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ ]}' ``` +### `POST /v0/market/[marketId]/sell` + +Sells some quantity of shares in a market on behalf of the authorized user. + +Parameters: + +- `outcome`: Required. One of `YES`, `NO`, or a `number` indicating the numeric + bucket ID, depending on the market type. +- `shares`: Optional. The amount of shares to sell of the outcome given + above. If not provided, all the shares you own will be sold. + +Example request: + +``` +$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": "YES", "shares": 10}' +``` + ### `GET /v0/bets` Gets a list of bets, ordered by creation date descending. diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 40ea0f4a..b6238434 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -16,7 +16,7 @@ import { redeemShares } from './redeem-shares' const bodySchema = z.object({ contractId: z.string(), - shares: z.number(), + shares: z.number().optional(), // leave it out to sell all shares outcome: z.enum(['YES', 'NO']), }) @@ -49,11 +49,12 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) const maxShares = sumBy(outcomeBets, (bet) => bet.shares) + const sharesToSell = shares ?? maxShares - if (!floatingLesserEqual(shares, maxShares)) + if (!floatingLesserEqual(sharesToSell, maxShares)) throw new APIError(400, `You can only sell up to ${maxShares} shares.`) - const soldShares = Math.min(shares, maxShares) + const soldShares = Math.min(sharesToSell, maxShares) const unfilledBetsSnap = await transaction.get( getUnfilledBetsQuery(contractDoc) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 4d27918b..aea38c86 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -771,7 +771,9 @@ export function SellPanel(props: { const betDisabled = isSubmitting || !amount || error // Sell all shares if remaining shares would be < 1 - const sellQuantity = amount === Math.floor(shares) ? shares : amount + const isSellingAllShares = amount === Math.floor(shares) + + const sellQuantity = isSellingAllShares ? shares : amount async function submitSell() { if (!user || !amount) return @@ -780,7 +782,7 @@ export function SellPanel(props: { setIsSubmitting(true) await sellShares({ - shares: sellQuantity, + shares: isSellingAllShares ? undefined : amount, outcome: sharesOutcome, contractId: contract.id, }) diff --git a/web/pages/api/v0/market/[id]/sell.ts b/web/pages/api/v0/market/[id]/sell.ts new file mode 100644 index 00000000..431121f2 --- /dev/null +++ b/web/pages/api/v0/market/[id]/sell.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, 'sellshares') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} From f32e995baa3aa336bea7dc5f84a6a2e870feb747 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 26 Jul 2022 15:24:16 -0700 Subject: [PATCH 362/519] Show referrals banner on user-page --- web/components/referrals-button.tsx | 3 +-- web/components/text-button.tsx | 4 ++-- web/components/user-page.tsx | 28 +++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index 74fc113d..fed8fb6b 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -5,13 +5,13 @@ import { prefetchUsers, useUserById } from 'web/hooks/use-user' import { Col } from './layout/col' import { Modal } from './layout/modal' import { Tabs } from './layout/tabs' -import { TextButton } from './text-button' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { useReferrals } from 'web/hooks/use-referrals' import { FilterSelectUsers } from 'web/components/filter-select-users' import { getUser, updateUser } from 'web/lib/firebase/users' +import { TextButton } from 'web/components/text-button' export function ReferralsButton(props: { user: User; currentUser?: User }) { const { user, currentUser } = props @@ -24,7 +24,6 @@ export function ReferralsButton(props: { user: User; currentUser?: User }) { <span className="font-semibold">{referralIds?.length ?? ''}</span>{' '} Referrals </TextButton> - <ReferralsDialog user={user} referralIds={referralIds ?? []} diff --git a/web/components/text-button.tsx b/web/components/text-button.tsx index 54133541..212f1c66 100644 --- a/web/components/text-button.tsx +++ b/web/components/text-button.tsx @@ -8,7 +8,7 @@ export function TextButton(props: { const { onClick, children, className } = props return ( - <div + <span className={clsx( className, 'cursor-pointer gap-2 hover:underline hover:decoration-indigo-400 hover:decoration-2 focus:underline focus:decoration-indigo-400 focus:decoration-2' @@ -17,6 +17,6 @@ export function TextButton(props: { onClick={onClick} > {children} - </div> + </span> ) } diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index fc89a285..035536b5 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -8,9 +8,9 @@ import Confetti from 'react-confetti' import { follow, + getPortfolioHistory, unfollow, User, - getPortfolioHistory, } from 'web/lib/firebase/users' import { CreatorContractsList } from './contract/contracts-list' import { SEO } from './SEO' @@ -40,6 +40,8 @@ import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' import { ReferralsButton } from 'web/components/referrals-button' import { formatMoney } from 'common/util/format' +import { ShareIconButton } from 'web/components/share-icon-button' +import { ENV_CONFIG } from 'common/envs/constants' export function UserLink(props: { name: string @@ -212,9 +214,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> - {currentUser?.username === 'ian' && ( - <ReferralsButton user={user} currentUser={currentUser} /> - )} <GroupsButton user={user} /> </Row> @@ -269,7 +268,26 @@ export function UserPage(props: { user: User; currentUser?: User }) { )} </Col> - <Spacer h={10} /> + <Spacer h={5} /> + {currentUser?.id === user.id && ( + <Row + className={ + 'w-full items-center justify-center gap-2 rounded-md border-2 border-indigo-100 bg-indigo-50 p-2 text-indigo-600' + } + > + <span> + Refer a friend and earn {formatMoney(500)} when they sign up! You + have <ReferralsButton user={user} currentUser={currentUser} /> + </span> + <ShareIconButton + copyPayload={`https://${ENV_CONFIG.domain}?referrer=${currentUser.username}`} + toastClassName={'sm:-left-40 -left-40 min-w-[250%]'} + buttonClassName={'h-10 w-10'} + iconClassName={'h-8 w-8 text-indigo-700'} + /> + </Row> + )} + <Spacer h={5} /> {usersContracts !== 'loading' && contractsById && usersComments ? ( <QueryUncontrolledTabs From 013ff1d941b634078fdcdedbb412a17e6cbf8664 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 26 Jul 2022 16:44:51 -0700 Subject: [PATCH 363/519] Show api error on create contract --- functions/src/create-contract.ts | 9 +++++++-- web/pages/create.tsx | 10 +++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index d58141a5..a30d508d 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -2,8 +2,8 @@ import * as admin from 'firebase-admin' import { z } from 'zod' import { - CPMMBinaryContract, Contract, + CPMMBinaryContract, FreeResponseContract, MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, @@ -97,7 +97,12 @@ export const createmarket = newEndpoint({}, async (req, auth) => { initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 if (initialProb < 1 || initialProb > 99) - throw new APIError(400, 'Invalid initial value.') + if (outcomeType === 'PSEUDO_NUMERIC') + throw new APIError( + 400, + `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` + ) + else throw new APIError(400, 'Invalid initial probability.') } if (outcomeType === 'BINARY') { ;({ initialProb } = validate(binarySchema, req.body)) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index ec86a277..5e50bbbb 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -180,6 +180,11 @@ export function NewContract(props: { min < initialValue && initialValue < max)) + const [errorText, setErrorText] = useState<string>('') + useEffect(() => { + setErrorText('') + }, [isValid]) + const descriptionPlaceholder = outcomeType === 'BINARY' ? `e.g. This question resolves to "YES" if they receive the majority of votes...` @@ -232,6 +237,9 @@ export function NewContract(props: { await router.push(contractPath(result as Contract)) } catch (e) { console.error('error creating contract', e, (e as any).details) + setErrorText( + (e as any).details || (e as any).message || 'Error creating contract' + ) setIsSubmitting(false) } } @@ -413,7 +421,7 @@ export function NewContract(props: { </div> <Spacer h={6} /> - + <span className={'text-error'}>{errorText}</span> <Row className="items-end justify-between"> <div className="form-control mb-1 items-start"> <label className="label mb-1 gap-2"> From b1c4f018f96b44161e09d68ec1abf2c9bedcd52e Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 27 Jul 2022 17:38:25 -0700 Subject: [PATCH 364/519] Expose cancel bet api --- web/pages/api/v0/bet/cancel/[betId].ts | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 web/pages/api/v0/bet/cancel/[betId].ts diff --git a/web/pages/api/v0/bet/cancel/[betId].ts b/web/pages/api/v0/bet/cancel/[betId].ts new file mode 100644 index 00000000..878b9349 --- /dev/null +++ b/web/pages/api/v0/bet/cancel/[betId].ts @@ -0,0 +1,27 @@ +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 { betId } = req.query as { betId: string } + + if (req.body) req.body.betId = betId + try { + const backendRes = await fetchBackend(req, 'cancelbet') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} From 1aaae93113119830eebea0d202aa3c4fac268a9e Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Wed, 27 Jul 2022 21:40:33 -0500 Subject: [PATCH 365/519] Multiple choice markets (#698) * multipe choice answers * create multiple choice cloud function * multi choice market page * show outcome '0' * stats: multi choice type * update place bet * answer doc id = outcome * update resolve market * prettier * fix * fix resolution --- common/antes.ts | 46 +++++++++++++ common/calculate.ts | 5 +- common/contract.ts | 17 ++++- common/new-bet.ts | 3 +- common/new-contract.ts | 28 +++++++- common/payouts-dpm.ts | 8 ++- common/payouts.ts | 6 +- functions/src/create-contract.ts | 45 ++++++++++++- functions/src/place-bet.ts | 5 +- functions/src/resolve-market.ts | 11 +++- web/components/answers/answer-bet-panel.tsx | 4 +- web/components/answers/answer-item.tsx | 4 +- .../answers/answer-resolve-panel.tsx | 4 +- web/components/answers/answers-graph.tsx | 17 +++-- web/components/answers/answers-panel.tsx | 20 ++++-- .../answers/multiple-choice-answers.tsx | 65 +++++++++++++++++++ web/components/contract/contract-card.tsx | 3 +- .../contract/contract-info-dialog.tsx | 2 + web/components/contract/contract-overview.tsx | 6 +- web/components/outcome-label.tsx | 3 +- web/pages/[username]/[contractSlug].tsx | 3 +- web/pages/create.tsx | 18 ++++- 22 files changed, 284 insertions(+), 39 deletions(-) create mode 100644 web/components/answers/multiple-choice-answers.tsx diff --git a/common/antes.ts b/common/antes.ts index b3dd990b..b9914451 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -5,12 +5,14 @@ import { CPMMBinaryContract, DPMBinaryContract, FreeResponseContract, + MultipleChoiceContract, NumericContract, } from './contract' import { User } from './user' import { LiquidityProvision } from './liquidity-provision' import { noFees } from './fees' import { ENV_CONFIG } from './envs/constants' +import { Answer } from './answer' export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100 @@ -111,6 +113,50 @@ export function getFreeAnswerAnte( return anteBet } +export function getMultipleChoiceAntes( + creator: User, + contract: MultipleChoiceContract, + answers: string[], + betDocIds: string[] +) { + const { totalBets, totalShares } = contract + const amount = totalBets['0'] + const shares = totalShares['0'] + const p = 1 / answers.length + + const { createdTime } = contract + + const bets: Bet[] = answers.map((answer, i) => ({ + id: betDocIds[i], + userId: creator.id, + contractId: contract.id, + amount, + shares, + outcome: i.toString(), + probBefore: p, + probAfter: p, + createdTime, + isAnte: true, + fees: noFees, + })) + + const { username, name, avatarUrl } = creator + + const answerObjects: Answer[] = answers.map((answer, i) => ({ + id: i.toString(), + number: i, + contractId: contract.id, + createdTime, + userId: creator.id, + username, + name, + avatarUrl, + text: answer, + })) + + return { bets, answerObjects } +} + export function getNumericAnte( anteBettorId: string, contract: NumericContract, diff --git a/common/calculate.ts b/common/calculate.ts index e1f3e239..d25fd313 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -23,6 +23,7 @@ import { BinaryContract, FreeResponseContract, PseudoNumericContract, + MultipleChoiceContract, } from './contract' import { floatingEqual } from './util/math' @@ -200,7 +201,9 @@ export function getContractBetNullMetrics() { } } -export function getTopAnswer(contract: FreeResponseContract) { +export function getTopAnswer( + contract: FreeResponseContract | MultipleChoiceContract +) { const { answers } = contract const top = maxBy( answers?.map((answer) => ({ diff --git a/common/contract.ts b/common/contract.ts index 177af862..8bdab6fe 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -4,13 +4,19 @@ import { JSONContent } from '@tiptap/core' import { GroupLink } from 'common/group' export type AnyMechanism = DPM | CPMM -export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric +export type AnyOutcomeType = + | Binary + | MultipleChoice + | PseudoNumeric + | FreeResponse + | Numeric export type AnyContractType = | (CPMM & Binary) | (CPMM & PseudoNumeric) | (DPM & Binary) | (DPM & FreeResponse) | (DPM & Numeric) + | (DPM & MultipleChoice) export type Contract<T extends AnyContractType = AnyContractType> = { id: string @@ -57,6 +63,7 @@ export type BinaryContract = Contract & Binary export type PseudoNumericContract = Contract & PseudoNumeric export type NumericContract = Contract & Numeric export type FreeResponseContract = Contract & FreeResponse +export type MultipleChoiceContract = Contract & MultipleChoice export type DPMContract = Contract & DPM export type CPMMContract = Contract & CPMM export type DPMBinaryContract = BinaryContract & DPM @@ -104,6 +111,13 @@ export type FreeResponse = { resolutions?: { [outcome: string]: number } // Used for MKT resolution. } +export type MultipleChoice = { + outcomeType: 'MULTIPLE_CHOICE' + answers: Answer[] + resolution?: string | 'MKT' | 'CANCEL' + resolutions?: { [outcome: string]: number } // Used for MKT resolution. +} + export type Numeric = { outcomeType: 'NUMERIC' bucketCount: number @@ -118,6 +132,7 @@ export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const export const OUTCOME_TYPES = [ 'BINARY', + 'MULTIPLE_CHOICE', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC', diff --git a/common/new-bet.ts b/common/new-bet.ts index 1f5c0340..576f35f8 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -18,6 +18,7 @@ import { CPMMBinaryContract, DPMBinaryContract, FreeResponseContract, + MultipleChoiceContract, NumericContract, PseudoNumericContract, } from './contract' @@ -322,7 +323,7 @@ export const getNewBinaryDpmBetInfo = ( export const getNewMultiBetInfo = ( outcome: string, amount: number, - contract: FreeResponseContract, + contract: FreeResponseContract | MultipleChoiceContract, loanAmount: number ) => { const { pool, totalShares, totalBets } = contract diff --git a/common/new-contract.ts b/common/new-contract.ts index abfafaf8..ad7dc5a2 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -5,6 +5,7 @@ import { CPMM, DPM, FreeResponse, + MultipleChoice, Numeric, outcomeType, PseudoNumeric, @@ -30,7 +31,10 @@ export function getNewContract( bucketCount: number, min: number, max: number, - isLogScale: boolean + isLogScale: boolean, + + // for multiple choice + answers: string[] ) { const tags = parseTags( [ @@ -48,6 +52,8 @@ export function getNewContract( ? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale) : outcomeType === 'NUMERIC' ? getNumericProps(ante, bucketCount, min, max) + : outcomeType === 'MULTIPLE_CHOICE' + ? getMultipleChoiceProps(ante, answers) : getFreeAnswerProps(ante) const contract: Contract = removeUndefinedProps({ @@ -151,6 +157,26 @@ const getFreeAnswerProps = (ante: number) => { return system } +const getMultipleChoiceProps = (ante: number, answers: string[]) => { + const numAnswers = answers.length + const betAnte = ante / numAnswers + const betShares = Math.sqrt(ante ** 2 / numAnswers) + + const defaultValues = (x: any) => + Object.fromEntries(range(0, numAnswers).map((k) => [k, x])) + + const system: DPM & MultipleChoice = { + mechanism: 'dpm-2', + outcomeType: 'MULTIPLE_CHOICE', + pool: defaultValues(betAnte), + totalShares: defaultValues(betShares), + totalBets: defaultValues(betAnte), + answers: [], + } + + return system +} + const getNumericProps = ( ante: number, bucketCount: number, diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts index 6cecddff..7d4a0185 100644 --- a/common/payouts-dpm.ts +++ b/common/payouts-dpm.ts @@ -2,7 +2,11 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash' import { Bet, NumericBet } from './bet' import { deductDpmFees, getDpmProbability } from './calculate-dpm' -import { DPMContract, FreeResponseContract } from './contract' +import { + DPMContract, + FreeResponseContract, + MultipleChoiceContract, +} from './contract' import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { addObjects } from './util/object' @@ -180,7 +184,7 @@ export const getDpmMktPayouts = ( export const getPayoutsMultiOutcome = ( resolutions: { [outcome: string]: number }, - contract: FreeResponseContract, + contract: FreeResponseContract | MultipleChoiceContract, bets: Bet[] ) => { const poolTotal = sum(Object.values(contract.pool)) diff --git a/common/payouts.ts b/common/payouts.ts index 1469cf4e..cc6c338d 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -117,6 +117,7 @@ export const getDpmPayouts = ( resolutionProbability?: number ): PayoutInfo => { const openBets = bets.filter((b) => !b.isSold && !b.sale) + const { outcomeType } = contract switch (outcome) { case 'YES': @@ -124,7 +125,8 @@ export const getDpmPayouts = ( return getDpmStandardPayouts(outcome, contract, openBets) case 'MKT': - return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ? getPayoutsMultiOutcome(resolutions!, contract, openBets) : getDpmMktPayouts(contract, openBets, resolutionProbability) case 'CANCEL': @@ -132,7 +134,7 @@ export const getDpmPayouts = ( return getDpmCancelPayouts(contract, openBets) default: - if (contract.outcomeType === 'NUMERIC') + if (outcomeType === 'NUMERIC') return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[]) // Outcome is a free response answer id. diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index a30d508d..786ee8ae 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -7,6 +7,7 @@ import { FreeResponseContract, MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, + MultipleChoiceContract, NumericContract, OUTCOME_TYPES, } from '../../common/contract' @@ -20,15 +21,18 @@ import { FIXED_ANTE, getCpmmInitialLiquidity, getFreeAnswerAnte, + getMultipleChoiceAntes, getNumericAnte, } from '../../common/antes' -import { getNoneAnswer } from '../../common/answer' +import { Answer, getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { User } from '../../common/user' import { Group, MAX_ID_LENGTH } from '../../common/group' import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' +import { zip } from 'lodash' +import { Bet } from 'common/bet' const descScehma: z.ZodType<JSONContent> = z.lazy(() => z.intersection( @@ -79,11 +83,15 @@ const numericSchema = z.object({ isLogScale: z.boolean().optional(), }) +const multipleChoiceSchema = z.object({ + answers: z.string().trim().min(1).array().min(2), +}) + export const createmarket = newEndpoint({}, async (req, auth) => { const { question, description, tags, closeTime, outcomeType, groupId } = validate(bodySchema, req.body) - let min, max, initialProb, isLogScale + let min, max, initialProb, isLogScale, answers if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { let initialValue @@ -104,10 +112,15 @@ export const createmarket = newEndpoint({}, async (req, auth) => { ) else throw new APIError(400, 'Invalid initial probability.') } + if (outcomeType === 'BINARY') { ;({ initialProb } = validate(binarySchema, req.body)) } + if (outcomeType === 'MULTIPLE_CHOICE') { + ;({ answers } = validate(multipleChoiceSchema, req.body)) + } + const userDoc = await firestore.collection('users').doc(auth.uid).get() if (!userDoc.exists) { throw new APIError(400, 'No user exists with the authenticated user ID.') @@ -167,7 +180,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => { NUMERIC_BUCKET_COUNT, min ?? 0, max ?? 0, - isLogScale ?? false + isLogScale ?? false, + answers ?? [] ) if (ante) await chargeUser(user.id, ante, true) @@ -189,6 +203,31 @@ export const createmarket = newEndpoint({}, async (req, auth) => { ) await liquidityDoc.set(lp) + } else if (outcomeType === 'MULTIPLE_CHOICE') { + const betCol = firestore.collection(`contracts/${contract.id}/bets`) + const betDocs = (answers ?? []).map(() => betCol.doc()) + + const answerCol = firestore.collection(`contracts/${contract.id}/answers`) + const answerDocs = (answers ?? []).map((_, i) => + answerCol.doc(i.toString()) + ) + + const { bets, answerObjects } = getMultipleChoiceAntes( + user, + contract as MultipleChoiceContract, + answers ?? [], + betDocs.map((bd) => bd.id) + ) + + await Promise.all( + zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet)) + ) + await Promise.all( + zip(answerObjects, answerDocs).map(([answer, doc]) => + doc?.create(answer as Answer) + ) + ) + await contractRef.update({ answers: answerObjects }) } else if (outcomeType === 'FREE_RESPONSE') { const noneAnswerDoc = firestore .collection(`contracts/${contract.id}/answers`) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 97ff9780..7501309a 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -96,7 +96,10 @@ export const placebet = newEndpoint({}, async (req, auth) => { limitProb, unfilledBets ) - } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') { + } else if ( + (outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') && + mechanism == 'dpm-2' + ) { const { outcome } = validate(freeResponseSchema, req.body) const answerDoc = contractDoc.collection('answers').doc(outcome) const answerSnap = await trans.get(answerDoc) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index f8976cb3..08778a41 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -5,6 +5,7 @@ import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash' import { Contract, FreeResponseContract, + MultipleChoiceContract, RESOLUTIONS, } from '../../common/contract' import { User } from '../../common/user' @@ -245,7 +246,10 @@ function getResolutionParams(contract: Contract, body: string) { ...validate(pseudoNumericSchema, body), resolutions: undefined, } - } else if (outcomeType === 'FREE_RESPONSE') { + } else if ( + outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE' + ) { const freeResponseParams = validate(freeResponseSchema, body) const { outcome } = freeResponseParams switch (outcome) { @@ -292,7 +296,10 @@ function getResolutionParams(contract: Contract, body: string) { throw new APIError(500, `Invalid outcome type: ${outcomeType}`) } -function validateAnswer(contract: FreeResponseContract, answer: number) { +function validateAnswer( + contract: FreeResponseContract | MultipleChoiceContract, + answer: number +) { const validIds = contract.answers.map((a) => a.id) if (!validIds.includes(answer.toString())) { throw new APIError(400, `${answer} is not a valid answer ID`) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 8c1d0430..6dcba79b 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react' import { XIcon } from '@heroicons/react/solid' import { Answer } from 'common/answer' -import { FreeResponseContract } from 'common/contract' +import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' import { APIError, placeBet } from 'web/lib/firebase/api' @@ -29,7 +29,7 @@ import { isIOS } from 'web/lib/util/device' export function AnswerBetPanel(props: { answer: Answer - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract closePanel: () => void className?: string isModal?: boolean diff --git a/web/components/answers/answer-item.tsx b/web/components/answers/answer-item.tsx index 87756a07..f1ab2f88 100644 --- a/web/components/answers/answer-item.tsx +++ b/web/components/answers/answer-item.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' import { Answer } from 'common/answer' -import { FreeResponseContract } from 'common/contract' +import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { Col } from '../layout/col' import { Row } from '../layout/row' import { Avatar } from '../avatar' @@ -13,7 +13,7 @@ import { Linkify } from '../linkify' export function AnswerItem(props: { answer: Answer - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract showChoice: 'radio' | 'checkbox' | undefined chosenProb: number | undefined totalChosenProb?: number diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 5b59f050..0a4ac1e1 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import { sum } from 'lodash' import { useState } from 'react' -import { Contract, FreeResponse } from 'common/contract' +import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { Col } from '../layout/col' import { APIError, resolveMarket } from 'web/lib/firebase/api' import { Row } from '../layout/row' @@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button' import { removeUndefinedProps } from 'common/util/object' export function AnswerResolvePanel(props: { - contract: Contract & FreeResponse + contract: FreeResponseContract | MultipleChoiceContract resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined setResolveOption: ( option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index 3e16a4c2..27152db9 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -5,14 +5,14 @@ import { groupBy, sortBy, sumBy } from 'lodash' import { memo } from 'react' import { Bet } from 'common/bet' -import { FreeResponseContract } from 'common/contract' +import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { getOutcomeProbability } from 'common/calculate' import { useWindowSize } from 'web/hooks/use-window-size' const NUM_LINES = 6 export const AnswersGraph = memo(function AnswersGraph(props: { - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract bets: Bet[] height?: number }) { @@ -178,15 +178,22 @@ function formatTime( return d.format(format) } -const computeProbsByOutcome = (bets: Bet[], contract: FreeResponseContract) => { - const { totalBets } = contract +const computeProbsByOutcome = ( + bets: Bet[], + contract: FreeResponseContract | MultipleChoiceContract +) => { + const { totalBets, outcomeType } = contract const betsByOutcome = groupBy(bets, (bet) => bet.outcome) const outcomes = Object.keys(betsByOutcome).filter((outcome) => { const maxProb = Math.max( ...betsByOutcome[outcome].map((bet) => bet.probAfter) ) - return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001 + return ( + (outcome !== '0' || outcomeType === 'MULTIPLE_CHOICE') && + maxProb > 0.02 && + totalBets[outcome] > 0.000000001 + ) }) const trackedOutcomes = sortBy( diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index e7bf4da8..6e0bfef6 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -1,7 +1,7 @@ import { sortBy, partition, sum, uniq } from 'lodash' import { useEffect, useState } from 'react' -import { FreeResponseContract } from 'common/contract' +import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { Col } from '../layout/col' import { useUser } from 'web/hooks/use-user' import { getDpmOutcomeProbability } from 'common/calculate-dpm' @@ -25,14 +25,19 @@ import { UserLink } from 'web/components/user-page' import { Linkify } from 'web/components/linkify' import { BuyButton } from 'web/components/yes-no-selector' -export function AnswersPanel(props: { contract: FreeResponseContract }) { +export function AnswersPanel(props: { + contract: FreeResponseContract | MultipleChoiceContract +}) { const { contract } = props - const { creatorId, resolution, resolutions, totalBets } = contract + const { creatorId, resolution, resolutions, totalBets, outcomeType } = + contract const answers = useAnswers(contract.id) ?? contract.answers const [winningAnswers, losingAnswers] = partition( answers.filter( - (answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001 + (answer) => + (answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') && + totalBets[answer.id] > 0.000000001 ), (answer) => answer.id === resolution || (resolutions && resolutions[answer.id]) @@ -131,7 +136,8 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) { <div className="pb-4 text-gray-500">No answers yet...</div> )} - {tradingAllowed(contract) && + {outcomeType === 'FREE_RESPONSE' && + tradingAllowed(contract) && (!resolveOption || resolveOption === 'CANCEL') && ( <CreateAnswerPanel contract={contract} /> )} @@ -152,7 +158,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) { } function getAnswerItems( - contract: FreeResponseContract, + contract: FreeResponseContract | MultipleChoiceContract, answers: Answer[], user: User | undefined | null ) { @@ -178,7 +184,7 @@ function getAnswerItems( } function OpenAnswer(props: { - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract answer: Answer items: ActivityItem[] type: string diff --git a/web/components/answers/multiple-choice-answers.tsx b/web/components/answers/multiple-choice-answers.tsx new file mode 100644 index 00000000..450c221a --- /dev/null +++ b/web/components/answers/multiple-choice-answers.tsx @@ -0,0 +1,65 @@ +import { MAX_ANSWER_LENGTH } from 'common/answer' +import { useState } from 'react' +import Textarea from 'react-expanding-textarea' +import { XIcon } from '@heroicons/react/solid' + +import { Col } from '../layout/col' +import { Row } from '../layout/row' + +export function MultipleChoiceAnswers(props: { + setAnswers: (answers: string[]) => void +}) { + const [answers, setInternalAnswers] = useState(['', '', '']) + + const setAnswer = (i: number, answer: string) => { + const newAnswers = setElement(answers, i, answer) + setInternalAnswers(newAnswers) + props.setAnswers(newAnswers) + } + + const removeAnswer = (i: number) => { + const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1)) + setInternalAnswers(newAnswers) + props.setAnswers(newAnswers) + } + + const addAnswer = () => setAnswer(answers.length, '') + + return ( + <Col> + {answers.map((answer, i) => ( + <Row className="mb-2 items-center align-middle"> + {i + 1}.{' '} + <Textarea + value={answer} + onChange={(e) => setAnswer(i, e.target.value)} + className="textarea textarea-bordered ml-2 w-full resize-none" + placeholder="Type your answer..." + rows={1} + maxLength={MAX_ANSWER_LENGTH} + /> + {answers.length > 2 && ( + <button + className="btn btn-xs btn-outline ml-2" + onClick={() => removeAnswer(i)} + > + <XIcon className="h-4 w-4 flex-shrink-0" /> + </button> + )} + </Row> + ))} + + <Row className="justify-end"> + <button className="btn btn-outline btn-xs" onClick={addAnswer}> + Add answer + </button> + </Row> + </Col> + ) +} + +const setElement = <T,>(array: T[], i: number, elem: T) => { + const newArray = array.concat() + newArray[i] = elem + return newArray +} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index f3f9807c..164f3f27 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -8,6 +8,7 @@ import { BinaryContract, Contract, FreeResponseContract, + MultipleChoiceContract, NumericContract, PseudoNumericContract, } from 'common/contract' @@ -227,7 +228,7 @@ function FreeResponseTopAnswer(props: { } export function FreeResponseResolutionOrChance(props: { - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract truncate: 'short' | 'long' | 'none' className?: string }) { diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index d976253f..a1f79479 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -41,6 +41,8 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { ? 'YES / NO' : outcomeType === 'FREE_RESPONSE' ? 'Free response' + : outcomeType === 'MULTIPLE_CHOICE' + ? 'Multiple choice' : 'Numeric' return ( diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 1fc8e077..50c5a7e6 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -85,7 +85,8 @@ export const ContractOverview = (props: { {tradingAllowed(contract) && <BetRow contract={contract} />} </Row> ) : ( - outcomeType === 'FREE_RESPONSE' && + (outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && resolution && ( <FreeResponseResolutionOrChance contract={contract} @@ -110,7 +111,8 @@ export const ContractOverview = (props: { {(isBinary || isPseudoNumeric) && ( <ContractProbGraph contract={contract} bets={bets} /> )}{' '} - {outcomeType === 'FREE_RESPONSE' && ( + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && ( <AnswersGraph contract={contract} bets={bets} /> )} {outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />} diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 9ecda16f..a6c3a563 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -7,6 +7,7 @@ import { BinaryContract, Contract, FreeResponseContract, + MultipleChoiceContract, resolution, } from 'common/contract' import { formatLargeNumber, formatPercent } from 'common/util/format' @@ -77,7 +78,7 @@ export function BinaryContractOutcomeLabel(props: { } export function FreeResponseOutcomeLabel(props: { - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract resolution: string | 'CANCEL' | 'MKT' truncate: 'short' | 'long' | 'none' answerClassName?: string diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 43dd0ad7..58e7c2e8 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -217,7 +217,8 @@ export function ContractPageContent( /> )} - {outcomeType === 'FREE_RESPONSE' && ( + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && ( <> <Spacer h={4} /> <AnswersPanel contract={contract} /> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 5e50bbbb..84ac82da 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -31,6 +31,7 @@ import { Checkbox } from 'web/components/checkbox' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { Title } from 'web/components/title' import { SEO } from 'web/components/SEO' +import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers' export const getServerSideProps = redirectIfLoggedOut('/') @@ -116,6 +117,8 @@ export function NewContract(props: { const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale) const [initialValueString, setInitialValueString] = useState(initValue) + const [answers, setAnswers] = useState<string[]>([]) // for multiple choice + useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { @@ -160,6 +163,10 @@ export function NewContract(props: { // get days from today until the end of this year: const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') + const isValidMultipleChoice = answers.every( + (answer) => answer.trim().length > 0 + ) + const isValid = (outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) && question.length > 0 && @@ -178,7 +185,8 @@ export function NewContract(props: { min < max && max - min > 0.01 && min < initialValue && - initialValue < max)) + initialValue < max)) && + (outcomeType !== 'MULTIPLE_CHOICE' || isValidMultipleChoice) const [errorText, setErrorText] = useState<string>('') useEffect(() => { @@ -221,6 +229,7 @@ export function NewContract(props: { max, initialValue, isLogScale, + answers, groupId: selectedGroup?.id, }) ) @@ -259,10 +268,11 @@ export function NewContract(props: { 'Users can submit their own answers to this market.' ) else setMarketInfoText('') - setOutcomeType(choice as 'BINARY' | 'FREE_RESPONSE') + setOutcomeType(choice as outcomeType) }} choicesMap={{ 'Yes / No': 'BINARY', + 'Multiple choice': 'MULTIPLE_CHOICE', 'Free response': 'FREE_RESPONSE', Numeric: 'PSEUDO_NUMERIC', }} @@ -277,6 +287,10 @@ export function NewContract(props: { <Spacer h={6} /> + {outcomeType === 'MULTIPLE_CHOICE' && ( + <MultipleChoiceAnswers setAnswers={setAnswers} /> + )} + {outcomeType === 'PSEUDO_NUMERIC' && ( <> <div className="form-control mb-2 items-start"> From b6a70641a031ca424f81f2ce2566291ff2fae1ec Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 27 Jul 2022 19:51:34 -0700 Subject: [PATCH 366/519] fix modal --- web/components/layout/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index aac9e4c8..d1a65607 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -57,7 +57,7 @@ export function Modal(props: { > <div className={clsx( - 'my-8 mx-6 inline-block w-full transform overflow-hidden text-left align-bottom transition-all sm:align-middle', + 'my-8 mx-6 inline-block w-full transform overflow-hidden text-left align-bottom transition-all sm:self-center sm:align-middle', sizeClass, className )} From 05b0ca5cdb9538ad47b2ffb7da0ea67ad460698c Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 28 Jul 2022 11:16:48 -0700 Subject: [PATCH 367/519] I want to see others' referrals --- web/components/user-page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 035536b5..d628e92d 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -214,6 +214,10 @@ export function UserPage(props: { user: User; currentUser?: User }) { <Row className="gap-4"> <FollowingButton user={user} /> <FollowersButton user={user} /> + {currentUser && + ['ian', 'Austin', 'SG', 'JamesGrugett'].includes( + currentUser.username + ) && <ReferralsButton user={user} />} <GroupsButton user={user} /> </Row> From aa6d0d175002134b69094389f9e7c91280caed27 Mon Sep 17 00:00:00 2001 From: marsteralex <bob.masteralex@gmail.com> Date: Thu, 28 Jul 2022 11:31:58 -0700 Subject: [PATCH 368/519] add beasts (#700) * fix https * add beasts * Remove extra file * Prettier-ify code * Prettier-ify Co-authored-by: Austin Chen <akrolsmir@gmail.com> --- web/public/mtg/app.js | 3 +++ web/public/mtg/index.html | 10 ++++++++++ web/public/mtg/jsons/beast1.json | 1 + web/public/mtg/jsons/beast2.json | 1 + web/public/mtg/jsons/beast3.json | 1 + web/public/mtg/jsons/counterspell3.json | 2 +- 6 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 web/public/mtg/jsons/beast1.json create mode 100644 web/public/mtg/jsons/beast2.json create mode 100644 web/public/mtg/jsons/beast3.json diff --git a/web/public/mtg/app.js b/web/public/mtg/app.js index fc7711d0..d4c98f70 100644 --- a/web/public/mtg/app.js +++ b/web/public/mtg/app.js @@ -59,6 +59,9 @@ function putIntoMapAndFetch(data) { document.getElementById('guess-type').innerText = 'Counterspell Guesser' } else if (whichGuesser === 'burn') { document.getElementById('guess-type').innerText = 'Match With Hot Singles' + } else if (whichGuesser === 'beast') { + document.getElementById('guess-type').innerText = + 'Finding Fantastic Beasts' } setUpNewGame() } diff --git a/web/public/mtg/index.html b/web/public/mtg/index.html index 62849462..dde69c64 100644 --- a/web/public/mtg/index.html +++ b/web/public/mtg/index.html @@ -149,6 +149,16 @@ <h3>Match With Hot Singles</h3></label ><br /> + <input type="radio" id="beast" name="whichguesser" value="beast" /> + <label class="radio-label" for="beast"> + <img + class="thumbnail" + src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469" + /> + <h3>Finding Fantastic Beasts</h3></label + > + <br /> + <details id="addl-options"> <summary> <img diff --git a/web/public/mtg/jsons/beast1.json b/web/public/mtg/jsons/beast1.json new file mode 100644 index 00000000..6a5b26c0 --- /dev/null +++ b/web/public/mtg/jsons/beast1.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Adaptive Snapjaw", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d3c0c43-2d6d-49b8-a112-07611a23ae69.jpg?1561815740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aeromoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a304f7e-0b9e-4ef6-9ad8-34350839f7d9.jpg?1626094228"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Affectionate Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4c8ddc1-d95c-499f-b1d1-f608f8f07b02.jpg?1572893293"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alms Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce441759-cd4c-4bcc-925e-08e8b60853c0.jpg?1561846666"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Alpha Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a2e5279-f28c-4a78-9f8a-16c9f72f8d38.jpg?1562817224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Barkripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33255dfd-f8a9-4a15-aac5-c53dc0257859.jpg?1562629272"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Brushhopper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b09204c7-3e3d-484a-a4f7-da1b818e3884.jpg?1562631503"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Murkdiver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e43d62c-488a-4c8d-b193-bacbf8037761.jpg?1562932427"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Scavenger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/21a21190-3c05-40fe-9310-493ed0f9e42e.jpg?1562628898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Anurid Swarmsnapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/6/3636a9f8-d1d7-4452-8a53-788b514fdb97.jpg?1562629337"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquamoeba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1243552a-ca57-42ce-817e-d6268fc673e0.jpg?1562628647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aquus Steed", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af643949-7a9b-4195-8ab8-d43b1928b85a.jpg?1562791584"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66aed11a-0831-4619-931f-7dfded999c66.jpg?1562826029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arashin War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70fd6e2c-201d-436b-ad54-c9403295ec85.jpg?1562634168"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arborback Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/788b9d55-6679-4fcc-a3af-11d31e477421.jpg?1576382341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arboreal Grazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4a5f86f-44a8-4735-909a-770586d33a15.jpg?1586962989"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Hybrid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2f33f9d-dffd-4742-92c6-be7fe6463dca.jpg?1562638550"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Lancer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7ff3241b-49ba-4243-b8fc-fef600836c8c.jpg?1562637774"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c0c33a92-5621-40b4-a3a2-b67893edbc01.jpg?1561968545"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/72c1a731-7854-42b1-8719-ac3c2a269c1f.jpg?1562637545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a898fbf-5c73-4a50-8bf5-126051747659.jpg?1599332547"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/1/211b1279-0f37-47a9-8eb5-db91159d0cf2.jpg?1562636700"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Arcbound Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/eda7bda4-51cf-4648-8489-352d28d591fb.jpg?1562945052"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Arc-Slogger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3dd67e0-72b4-4c55-b49b-c69950feccb1.jpg?1562158892"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Armguard Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/7497f147-146d-4a76-b670-bd84e07352b3.jpg?1654566610"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ashen Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/ebaef0bd-8288-49ba-a889-d897a4aae64c.jpg?1562939159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Assault Zeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12bf6443-c941-418a-a766-05bba088a117.jpg?1593273548"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Aura Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f8dbb4f-4b01-4666-b62f-a2323dac7a19.jpg?1562706262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a39ae1e4-d4dd-4691-af5a-5fa25ace4ebe.jpg?1591227516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Auspicious Starrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7b41cfa-b22e-4d34-bfe9-68c9d8740704.jpg?1604781846"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Avarax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae76705f-ec95-48b0-9e26-84ce40c9514b.jpg?1562936224"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Axebane Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2f420b35-1f73-41c8-a15f-1aee4af0999c.jpg?1584831084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/504090bb-d183-4833-aea5-d4193b5c57a1.jpg?1562735490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Null", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/8811d210-23e2-4318-9730-7ee3b2021c68.jpg?1562922516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Packhunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61b22c5d-3b29-47c1-8a04-13586461a143.jpg?1597684060"}, "reprint": false, "digital": true, "set_type": "starter"}, {"name": "Baloth Pup", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f9c87f4-4fa5-4c97-9654-c4acd250f850.jpg?1562907761"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Baloth Woodcrasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/8223dc6a-2bee-4be9-86d5-f0a17a24c33e.jpg?1562613874"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bannerhide Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1271251b-7d79-4cb4-80bb-98574aa63249.jpg?1626097186"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Barbarian Outcast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d67b5c-ab20-456e-8ff5-7521be8273b2.jpg?1562631722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Barkhide Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9196ce7-3ff4-4dda-a628-559ada11c9ba.jpg?1562938641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Batterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a7b40f74-893f-4bfc-87b2-7f8df4c912d8.jpg?1562791147"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9ef71f42-87e5-4b1d-aac1-3752b81cee7c.jpg?1562932547"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battering Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d9aa740-9adf-412a-b6ec-0b9bb1b4618b.jpg?1587306439"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Battlefront Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3b425cd-c5a5-48e9-b697-3860dfa6d5d3.jpg?1562830855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bazaar Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b07bb2fe-3a9b-47d0-864b-99a662d9544b.jpg?1562791650"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beacon Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cc42e33-7489-4a32-bb30-adc80ec13521.jpg?1562799353"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35ed069c-410f-4b30-afd1-8d04742068e7.jpg?1562906387"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/7693877c-958f-4c67-93d5-7db8f2dd87e7.jpg?1562919934"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c90b6269-7406-40c9-8d4c-3448698a1fdd.jpg?1562937465"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beast in Show", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f7191d7-2c2c-470e-a2b6-eeb8f3031cc2.jpg?1562928685"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Beasts of Bogardan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f885d776-2953-4ed4-b63f-91dc2b42783b.jpg?1562861851"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Beast Walkers", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99b42f6c-5c7e-4ba8-b0fb-ac8564aaf825.jpg?1562587770"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Berserk Murlodont", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/499c4674-dd9f-4848-8447-721f842a0213.jpg?1562909903"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/1354ca60-7183-47ae-ba7b-0871311cba66.jpg?1562089277"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Blastoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9db5d6c2-b11f-442a-b172-c0c99c9bec07.jpg?1562631252"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blight-Breath Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/7865c079-1d91-48d4-852d-d104b6e0c157.jpg?1616399490"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blind Creeper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86d5440a-7460-4b4f-a167-a6c4fb2d855e.jpg?1562878236"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bloodstoke Howler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/743779d4-fee8-4b8d-a5ac-27f355e006e5.jpg?1562918274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Blossoming Bogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/332153ab-1b8e-40a8-b0b4-01f94866d368.jpg?1625192204"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Bog Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f230831-023c-41aa-832e-16ac81e68588.jpg?1562909815"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bogstomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05145a8d-0bfb-4f07-87cf-65875310bdb4.jpg?1562300265"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Bonethorn Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/297d7326-ad03-464d-97e2-443042d48f92.jpg?1562526649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/37e4df5b-ec53-4f8a-8c26-272b3177c0a6.jpg?1591227954"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boneyard Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/e/2e0232c0-0867-4217-8e5d-b3454c0c8dab.jpg?1604781908"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Book Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/01dfe640-5bd2-4d0b-8977-887b2ed4c2dd.jpg?1572893108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Boot Nipper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cff5a5b8-f823-4429-acd8-c4f34a676cb4.jpg?1591226621"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brackish Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90ba37ee-159f-421f-8d37-a7b5f1b562f0.jpg?1624590775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Branchsnap Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52118ff1-ad76-4b97-9fdc-6adfe80140f8.jpg?1562911651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brontotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a171f5e2-ed3d-4675-a4fc-953ebb907aa0.jpg?1562927638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Broodstar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/07a194cb-53c9-4690-ba63-79beecaebe0e.jpg?1562134726"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59bd1534-52d1-4946-b430-d26f039a9067.jpg?1562786763"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/206a9e7b-45c1-4213-8fc4-27d90e2ab0e9.jpg?1627707159"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bulette", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a76c993-7cc5-428f-bfbc-7747c6a566d0.jpg?1627711855"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Bull Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bbae0fe2-5d52-434c-8ad1-4a5e42f4b7c4.jpg?1562708388"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Bumbling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/4930b9d5-939f-4463-9f9a-235aa3a4f8c4.jpg?1562910270"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/1585bb24-41de-48a7-820e-d99ee76aec01.jpg?1580013629"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Calciderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/387adc65-5d18-4291-85b1-f49f556781c7.jpg?1561756925"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Caller of the Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/1286208b-896b-4f41-a837-1c8a2b199a0f.jpg?1562701494"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b04160c-89a7-4dcd-b05d-5dc846824d64.jpg?1604198638"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Canopy Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d52e90d3-d356-4b23-8f5c-a4004b20394c.jpg?1604202724"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Canopy Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0ccdc9d7-71b5-4304-8d19-a63952e17a6b.jpg?1562897615"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnassid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae10e7fe-ee51-4c39-86ec-503324d19f6c.jpg?1562597351"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Carnivorous Moss-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/d/bd814ce3-9555-4e9d-a212-e40717f4e546.jpg?1562793539"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Cavern Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/adfb0804-50d6-4bca-8733-72e01030a543.jpg?1562931741"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cavern Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34748acb-7045-42b6-a93f-a3f11a1bc839.jpg?1562702691"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cerodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f6a85165-5aed-4e26-a314-1370d4638deb.jpg?1562645142"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chainflinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670a5bba-a10f-41f6-88cd-cef1dfe4bfa9.jpg?1562914041"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chambered Nautilus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/860c613d-d031-4c2a-922b-39f4eec04e18.jpg?1562381838"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chancellor of the Tangle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/d/6d129aa8-b637-451e-8123-5221e08cc2cc.jpg?1562878494"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Charging Binox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68222ab7-7b9c-43e5-b80e-db643d80a6d9.jpg?1562915983"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Charging Slateback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2cfff37-655f-4107-abf3-e6f63d0e4de2.jpg?1562945225"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/0/b0960bdb-baa7-4b9a-a377-d350eb9c1d3b.jpg?1581708552"}, "reprint": true, "digital": false, "set_type": "duel_deck"}, {"name": "Chartooth Cougar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6b2c9c07-c3db-46ca-a204-b710c3a34ae9.jpg?1562530181"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c91cf95f-5007-409c-b891-00e10a3477e0.jpg?1568003959"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Chromeshell Crab", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e02a40a4-fa61-4595-810a-3796e0d71507.jpg?1562940039"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cliffrunner Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764c1a14-143f-4601-92c5-ebeabf3e375d.jpg?1562801821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/27f916a2-0ace-44b5-99dc-72979af34db9.jpg?1559591318"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Clockwork Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d5e5ae63-4963-485e-b40c-3450ee46674b.jpg?1562940262"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Clockwork Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/e/7e876938-1b8e-44cf-ade2-a42f8acdf24c.jpg?1562148654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Coalhauler Swine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc001cef-3afd-4128-989f-ac99dc76b243.jpg?1598915417"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colossodon Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2c60e63-0b86-4100-a932-bb9e9b197610.jpg?1562795540"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Colos Yearling", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/d/1d68eb62-9f86-4c85-8696-46a248c744ff.jpg?1562443334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Copperhoof Vorrac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/81fff4cc-b2ab-4a41-bede-0d807552ba46.jpg?1562149121"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cosmic Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/deaa0b9b-258e-4daf-8fec-ce64864d6bbf.jpg?1562880234"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cragplate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab62382d-2dc9-4a60-b031-c845ebad0357.jpg?1604198667"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Crater Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2382e525-1750-484a-bf95-dbb42bbb30ae.jpg?1562902530"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a249be17-73ed-4108-89c0-f7e87939beb8.jpg?1592709311"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/7/2750bee4-7dfa-4128-989c-5f81af1b322a.jpg?1645561147"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Craterhoof Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/640be32d-dcc8-408a-b8a6-077472f1e70b.jpg?1645561142"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Creature Guy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/13ac8bde-7a3e-4d14-91f4-f4325c93f6a8.jpg?1562487893"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Crested Craghorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aadb40c8-3d54-4705-82dc-54e8d6e315d5.jpg?1562929450"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cryptic Annelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a51026a-ae3c-4fa1-ac1e-96d44ae55b82.jpg?1562916366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cultivator Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62dffe04-c431-440d-a8da-33c74b4bb683.jpg?1643592511"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Cystbearer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6c10302-f0b3-4076-ae5c-a8c8c09a7d41.jpg?1562822162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Darba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d82636dc-4b3e-44a8-bc72-dab1275dfb6d.jpg?1562935433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathbringer Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f09f166f-dd3c-4cf5-b5f9-3989f46f050c.jpg?1562645019"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deathmist Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74c40df1-3f63-49e7-a869-1ce14f94a753.jpg?1562788391"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Deepwood Tantiv", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bfa2028e-4e73-4ff2-a9e2-9ac347d67893.jpg?1562382576"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Desert Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2047c2e5-8b3b-4c6b-91cf-3484f21e52f0.jpg?1543675549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d5c36c-bcc8-459c-9f4b-b265ccdb1f06.jpg?1627703119"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Displacer Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/8646ae5c-e757-4d16-bf2a-d48770d620fa.jpg?1627711276"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Displacer Kitten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/7/c7a401b8-29fb-46ef-a663-427f66724d5c.jpg?1653329945"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Domri's Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/a/1abe58d8-67d1-4719-8e84-27747dea3506.jpg?1584832471"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dreg Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7771eba-bc2d-40f2-bab4-5e9cc4fe8f34.jpg?1562710204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drekavac", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/459d8cb7-cbb8-4e73-9571-44277f1d1be2.jpg?1593272880"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Dromad Purebred", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0106caf1-2201-4661-96a5-56af02963fa6.jpg?1598913635"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Drooling Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/e/de33c222-0d74-4eb5-8794-39f3601eb8f4.jpg?1598916987"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Durkwood Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/7/670521c3-df02-487d-a299-49419e41889f.jpg?1562916541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Earthshaking Si", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/418df457-4aab-486c-b691-41f03ec8a6df.jpg?1562131512"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Elder Gargaroth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/5/d51269cf-a333-4a64-94cd-245798d840d2.jpg?1594736944"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Electryte", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/85c3d04f-4010-4db3-9e4e-afa8116b263d.jpg?1562923240"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8a6d9cab-b07b-456b-9562-7ea7f6bec7f3.jpg?1561835467"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Ember Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25080720-612f-40c0-8894-cda8e3e8afb8.jpg?1562901920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Enormous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/cebfb5a6-9052-47be-b931-834b5064df31.jpg?1562936577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Erithizon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ec4ea4e2-2102-4b99-bea5-6fc4203f2b26.jpg?1562383536"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Essence Symbiote", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8d09ddf0-91f0-4e76-809f-c39ca7418ed5.jpg?1591227575"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ettercap", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f5228dc-ec9d-456f-a89c-1bc592a1bbab.jpg?1653970287"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Excavating Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d353d315-5790-417d-adf5-270df1ff34b0.jpg?1562202067"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Fangren Firstborn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97d5fc3c-7f6b-42a5-a482-d789a2a421c7.jpg?1562638300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Hunter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2dbc8eef-f032-490a-b487-da1af71b7ff2.jpg?1562139685"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Marauder", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5cf62a2-d03a-495d-924a-bf79524175fa.jpg?1562615957"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fangren Pathcutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/59679bcf-4436-48f8-bc6a-d7e0ec6b04c9.jpg?1562877169"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea76a183-e15c-4968-b29d-91c074aa8681.jpg?1562950859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Guardian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44bdbed8-5d21-4bf5-8a32-9623b1139c85.jpg?1576381396"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Felidar Sovereign", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/8/78769295-e1e3-4bd7-9ece-b60e124efbba.jpg?1562920314"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Feral Hydra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46f76986-e9fb-4c51-b946-880b501775b0.jpg?1562703397"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5041996b-c265-4c4f-a52c-dfe29b2e282d.jpg?1562825098"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Feral Throwback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5111a9a3-a92d-4677-8974-20800256dd4f.jpg?1606849574"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ferocious Zheng", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a6d1184-15e0-4b41-ba2d-4f68e91c61d4.jpg?1562131565"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Ferrovore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dcc7170-38d9-4b9e-a5f9-73ac1208c439.jpg?1636491206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fledgling Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c464923e-ae6e-4c1d-9315-0ddb86c07b40.jpg?1562936522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c57abdab-d99c-418c-818d-b06a8722d733.jpg?1562941643"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c93f0066-1ff0-4e52-9959-9eb0def60957.jpg?1562631986"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/680ccbc7-aa97-4f01-9d26-0df184af3c3e.jpg?1562596853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/3/a3165251-6ac6-4294-8bca-595c362f4ceb.jpg?1562597338"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Overseer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3e644ab8-3cc3-413d-a918-44fc636087ae.jpg?1562629522"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Flowstone Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f2b70a5-db13-4c3f-829d-d4b9e0a16245.jpg?1562596859"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenetic Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/f/8f6bc3c0-2d6e-4a09-84c4-b26a352186bb.jpg?1562923949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frenzied Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bce2eef7-03a4-415f-8bb7-a29d50ce1b0f.jpg?1584831519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Frondland Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/ab220695-e1a9-45ec-a1b1-5a82c9c90a03.jpg?1591605277"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fungal Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/b/1b65f96b-019b-40a9-9b4d-acd4abf4a0f9.jpg?1562901457"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Furnace Scamp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97538294-058c-47d4-b7a8-4db3753a6628.jpg?1562879991"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Fylamarid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/d/8dd4f686-79e3-4067-81f9-7fae0c25dc8f.jpg?1562055416"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Galvanoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc1a696b-642a-419f-bd43-09af39a9401b.jpg?1562616123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd0a61c9-8b14-4255-8453-4b74d90fe0a3.jpg?1562248146"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Gang of Elk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/a/5a84177f-43a3-4d14-9a4c-2ca931cfe092.jpg?1562863261"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b672c59-7376-455d-961e-ce94d47a5ca4.jpg?1626096673"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88167b74-c25f-4a9b-a4f5-33a51e01d498.jpg?1626101678"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Garruk's Companion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/863c9a10-d83f-415b-adf2-2d0f870410b2.jpg?1562466784"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Gorehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/9/3928bbce-87b7-4b28-9af4-20362935c909.jpg?1594736993"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Harbinger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e0fa0b6-5f3f-4669-84e8-2c38c9593d88.jpg?1595022082"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/563c6959-9131-40a6-97ec-12baf6fb7ca0.jpg?1562643185"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Garruk's Horde", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3313f4ea-1275-4835-b4ff-73d3601c04e1.jpg?1605361688"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Garruk's Packleader", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dfaef299-7879-4f52-8ee4-701ed150b930.jpg?1562478545"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/0/0095245c-a30e-4e2a-88c9-632c678e9f03.jpg?1591227650"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast2.json b/web/public/mtg/jsons/beast2.json new file mode 100644 index 00000000..8f8969cf --- /dev/null +++ b/web/public/mtg/jsons/beast2.json @@ -0,0 +1 @@ +{"has_more": true, "data": [{"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d75546a5-81fd-41c1-a081-d8980f6bd60a.jpg?1604781861"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Gemrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c811c0d4-e2fc-45eb-8a76-b89c38a95536.jpg?1604783022"}, "flavor_name": "Anguirus, Armored Killer", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Geyser Glider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b8aec169-4c62-4d53-a19c-68baa20c8e59.jpg?1562615855"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/8/382048ec-0bf5-49a5-90d5-f80fbda08962.jpg?1561822913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ghor-Clan Rampager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5dacb6f8-20f7-4ed4-aa9f-8c1d55f09357.jpg?1562497081"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Giant Warthog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c402ef0e-51e7-4da6-a434-b99c5d435698.jpg?1562631879"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gilded Cerodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f68c8fbd-9223-447d-a85c-fa6222c75277.jpg?1562820187"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glade Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee38eeae-918b-4d19-b37a-175ac5db37a4.jpg?1562951582"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glademuse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89a40dc1-3bd8-4c7e-9446-5abc8c1f6995.jpg?1591319670"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Gloomshrieker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a2b50751-7f65-4321-86da-eef735bf8b67.jpg?1654568435"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowering Rogon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/974b0881-bd26-4074-93dd-a1e3600347c4.jpg?1562925487"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Glowing Anemone", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/708593e6-787b-4f76-a86c-1d52857493ea.jpg?1562381361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gluetius Maximus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aa7626ff-814f-4d9f-9595-ac7fa5334d4b.jpg?1562489356"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Gnarlid Colony", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/7327289d-eed8-44b1-8495-7172e2b49d5f.jpg?1604198764"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gnarlid Pack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68716387-c5ec-4967-be5f-723783722c64.jpg?1562288938"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Godsire", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2539ff7-2b7d-47e3-bd77-3138a6c42d2b.jpg?1562710016"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Goretusk Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9919d2dd-d6a1-4d45-b6aa-227ed05d7051.jpg?1562631090"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Graf Mole", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25a40334-65d8-46d2-9c56-389e9b32107c.jpg?1576385088"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grave Sifter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/598fe7f1-bcc2-4909-9933-06bf02372adc.jpg?1561943333"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Graxiplon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c16e565-0b7f-46b1-a091-64c47c923a9f.jpg?1562897735"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grazing Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68ccef2d-9a1f-4011-89e1-911bcc109b9d.jpg?1562916942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Greater Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/653ddfa0-2088-4503-a3ab-b0f1d55d8351.jpg?1562916161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Great-Horn Krushok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/122e08cb-407b-4b3d-8af0-077ff96bf160.jpg?1562822577"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gristleback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b82f763a-c960-4b59-8c77-f3bea7bd8c8b.jpg?1593272456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Groffskithur", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75e84098-c15c-40f4-9d8a-3fa5da26a268.jpg?1562148057"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Grollub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47f6301a-d581-4aaf-9993-3013323074aa.jpg?1562087828"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gruul Nodorog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/8/9855ce83-ae26-4b1d-ab7f-637cde09d679.jpg?1593272463"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Gruul Ragebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/080ef367-7904-4e5c-a8b4-1fb62f951f3e.jpg?1561814762"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Guardian Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/9941f83b-2903-4eab-ac6d-5313e3978fa3.jpg?1562923479"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gulf Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf424982-a0ab-4db9-8889-f3cef10966c6.jpg?1562930718"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Gurzigost", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f4e672c6-6ddc-4dd2-b4c7-5083d7566e87.jpg?1562632734"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Helium Squirter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/6/764e3d28-1876-46da-b927-b98089d62776.jpg?1593272686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Herald of the Forgotten", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/3/c3dba1c4-ee9a-4ea6-bf66-f639d38711cd.jpg?1591319371"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Herd Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1e9cef5-c55f-47d9-9d2f-300dab8fcb0b.jpg?1626097560"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Herd Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9cf4fd75-34b1-4afa-b8cd-777dfc9e6376.jpg?1562928115"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Highcliff Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/c/ecbeac44-9392-4522-8ff5-87079386bd0a.jpg?1576267130"}, "reprint": false, "digital": false, "set_type": "box"}, {"name": "Hollowhenge Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/052ab91f-ac01-43f4-9276-9af35dbfbf71.jpg?1562896231"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hundroog", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525c356-88ca-4e2e-8f06-663be101e34f.jpg?1562944359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/edda2de4-22f6-4d33-b182-3ae5d105f1f6.jpg?1562942777"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Hunted Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/2/b21c8b2d-ef0f-4839-acfc-20fd248c62cf.jpg?1562382549"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hunting Moa", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/2/926cefa1-3c5c-4bd6-859b-de620a3ee777.jpg?1555789722"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hydroid Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/801dd9c6-b159-4e1c-af2c-214c1f573633.jpg?1584833616"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c964473-7c54-4c2d-a3eb-dba01c842103.jpg?1562901719"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Indrik Stomphowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/e/fe57b3a2-0fd9-4f99-bb2b-828979dbcfc3.jpg?1593273398"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Infernal Spawn of Evil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/9/99711b5b-3cb2-4d57-ac9a-f43cc86a7ca9.jpg?1562799128"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Infernius Spawnington III, Esq.", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/e/5e3b1317-f024-4e34-89ad-538fc148cd5c.jpg?1584348881"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Insatiable Souleater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/7/171d5213-5bb4-4f5b-9ddd-e2a7ac092ec6.jpg?1562875704"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Intrusive Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/9/49266f3c-4b43-4175-8bac-16789ba6f4b9.jpg?1572892585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Iron-Barb Hellion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0cb36352-2f16-4572-b1aa-dc28b11f4229.jpg?1562875415"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ironclad Krovod", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afb16895-6542-405e-9793-154ffc439f23.jpg?1569418805"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Jackalope Herd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb80105c-d2c0-4f8c-9302-5e6152a60f54.jpg?1562088801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kalonian Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/7/77064471-d0c1-4988-8c47-f767bf9635f3.jpg?1561984952"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Kalonian Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/135946fc-fe67-401f-821d-d7145c63f030.jpg?1562826250"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Karplusan Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/602610ce-8f42-4a1d-8f6e-92424d9d637c.jpg?1593275267"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Karstoderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/028c52f2-c45b-42da-89bd-cdd5cd7850f3.jpg?1562635162"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kazandu Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/afdfe5aa-8b15-4a89-a22a-03baf6afa4e7.jpg?1604199049"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kelpie Guide", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/1/0112ebfb-55ad-401c-9dc5-ffd829f5b5bf.jpg?1624590206"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kezzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b95d3a-bb19-474d-9939-8817038fe9fc.jpg?1562052813"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kiln Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c584268-67c3-411b-a26c-aee3adf23872.jpg?1562701033"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kjeldoran Frostbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fccb1d0-b324-4780-bb9e-4533240da06d.jpg?1562903801"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krakilin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/9/a90442e8-9d22-4767-9e08-bd314169ea70.jpg?1562055913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kranioceros", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52aece74-cc1f-4f32-ad1f-00733eb79007.jpg?1562801006"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af822507-fd4c-454b-ab07-106c81c535bf.jpg?1562927648"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47ea2f2d-14ca-4b57-b973-5ce7db35bebf.jpg?1615254642"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Krosan Cloudscraper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/51ef4cda-e55b-45a8-9c02-4e77e5b15a9e.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Colossus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/8/a804f3c0-5ebf-43ca-b200-09f7c1bbe902.jpg?1562934820"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Groundshaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/2/82105090-5f71-4690-9ade-187354311ae3.jpg?1562925715"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b872f85-60c5-44c4-956d-a8aa8132908b.jpg?1562897602"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Vorine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d1c6c6-16b3-4a52-aeda-683b1aeb0e7f.jpg?1562931992"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Krosan Warchief", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435b700b-2072-47c0-9725-ad04414d2474.jpg?1562528085"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Kurgadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52a1758c-849a-4de3-b674-857c3c9bf399.jpg?1562529070"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Grunt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f27fd65a-5631-491f-b158-45012832ccf1.jpg?1562632792"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Titan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e36bc466-0f74-46fd-add2-c1cf3b3fe46b.jpg?1562632509"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Warrior", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a13b103f-482b-47d5-84a2-3621ba23bd20.jpg?1562631306"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Laccolith Whelp", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86eb5b9e-320f-40de-8668-ee0c08f63ec1.jpg?1562630877"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Landscaper Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/4/f45a9e86-133e-4626-a239-73ef88d9ae12.jpg?1626093695"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Lazotep Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/9/594bbe43-a8aa-42aa-bc49-cb4f3bc05cad.jpg?1557576504"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/5/55f97b4c-42c7-4986-a150-0b8de11f0537.jpg?1562287740"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leatherback Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2c621ad-7109-4e07-b0cf-49fc243bc175.jpg?1562448787"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Leery Fogbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56125660-2307-4270-a947-f1f4ad63841c.jpg?1562915161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Leopard-Spotted Jiao", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/91df110f-85d2-41cb-96b6-6c79cebfada7.jpg?1562131600"}, "reprint": false, "digital": false, "set_type": "duel_deck"}, {"name": "Lesser Gargadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/3/63ed7aec-a513-418e-9cef-e0c51203055b.jpg?1562913496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lexivore", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39db7a3-028e-4c01-8ff9-64d2a1397379.jpg?1562799143"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Leyline Prowler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c56b4e8f-d48e-4bb0-883d-29f978033f65.jpg?1557577175"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lightning Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24a0860d-d3b9-4a00-a8cb-617bc317b93d.jpg?1562640145"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Loathsome Catoblepas", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4a8cff2f-ba52-4d22-83e8-13c56368f1df.jpg?1562817730"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Longhorn Firebeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf0dcf33-8d3f-429c-8ad8-a65d07d7c790.jpg?1562631821"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83e035ca-eccd-4b63-817c-f2c676b9c98d.jpg?1591228108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lore Drakkis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e938fac3-544a-4f27-9726-a67153392031.jpg?1604781920"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4ccdef9c-1e85-4358-8059-8972479f7556.jpg?1572490606"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lovestruck Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/341110e5-577d-45ee-bf62-53373a331c87.jpg?1571399806"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Lullmage's Familiar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b31a81e8-df0e-4540-93c1-c30c31ea9be9.jpg?1604200204"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Battlement", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/2469bc93-57ca-4077-bda2-160b4160adad.jpg?1584829942"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lumbering Satyr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d897088-0667-4864-91c3-5f0ac7f9b220.jpg?1562380887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurching Rotbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f06be97-71c8-46c8-a1c2-5da3af25e6de.jpg?1562808809"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/3/b39eb671-e17e-4c5a-8913-1e3be7faedfb.jpg?1587910787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Arynx", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/f/7f59bc0b-88de-4580-bfc8-5af911d9ee99.jpg?1562788949"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Lurking Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/b/abdbaa34-1ee5-4a2a-bdb3-2f04809a5b42.jpg?1562561935"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Macetail Hystrodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/8451ab3f-5d61-4f35-ab70-5a5060caf53d.jpg?1562921768"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Makindi Sliderunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e6da400-ee4e-44d1-887d-1e2fb59b9322.jpg?1562932470"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Manglehorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/a/0aa3a844-97e6-4f5d-a36f-56fea4e06932.jpg?1543675886"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Marauding Maulhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7d5e3dc-f307-4f91-a5ee-e7c5d03d8102.jpg?1562834221"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Marsh Lurker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/90c4b759-f53d-4977-8d97-a93762622e75.jpg?1562055419"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48494f33-34b5-4c76-bb24-23a78b856e3c.jpg?1562237337"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Mawcor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/f/9f50971e-2a18-4db7-8b5b-83dd5e85766e.jpg?1562055468"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Megatherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c58a1e43-a173-45d6-ac55-363664bf6e1b.jpg?1562383029"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Meglonoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b69e32b7-87d6-44a8-a544-5dabcd64c9f3.jpg?1562803314"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a2a287b-b83f-444f-84f7-e388beb616c2.jpg?1591227787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Migratory Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e31f56d-bf75-4e14-94de-5c77193abf3a.jpg?1604781892"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Mischievous Quanar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/c/dc48c2db-f5b4-4c24-a5fa-00750b7ff56f.jpg?1562535674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mockery of Nature", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/1/3118737f-2fd9-4fe5-bd0f-43c9ef2166e2.jpg?1576383753"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1340a63-f549-440b-aad3-14247113896a.jpg?1562823428"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Molder Slug", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee355d1b-5d64-4328-94d6-7a58889b99bc.jpg?1562162474"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mold Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/0/903cb570-d769-4d7f-afbe-90ebad96657c.jpg?1562614361"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Mosscoat Goriak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c23139d4-0db5-4683-8d49-f4600fbe29e2.jpg?1591227812"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Muck Drubb", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5bda3fc-89e8-44c2-bcfb-d17064bbc391.jpg?1562584674"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Murasa Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/480ddde1-81d3-4939-b232-cb1ced6cfc4d.jpg?1562202132"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Murasa Rootgrazer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e70b3b78-9bdc-449b-82a9-c2fc3dd7f120.jpg?1604200243"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nalfeshnee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7717617-706a-4338-a207-dd8c08feb1c3.jpg?1654036022"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Naya Soulbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/0/f0e4b468-096b-4f80-9e78-022fe24a7e45.jpg?1562945827"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Needleshot Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f9b1628d-aacd-4e19-9ebb-bcd9b2842c91.jpg?1562945371"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Demolok", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/ee0683b2-8bc2-4c6a-964e-b909693b68c1.jpg?1593092523"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nessian Game Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/5099d18d-c8b5-4706-bc93-40d1bb12988d.jpg?1593096253"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Noxious Groodion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6cb3d78-1a60-4e9b-b387-afeb58677536.jpg?1584830637"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nucklavee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50f54b0a-b0e1-44f1-bb91-523cc9e1c298.jpg?1562911924"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nullhide Ferox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/4/24c30bb0-06ba-432b-a20c-6fa79b0dc68a.jpg?1572893406"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nulltread Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a263f594-621e-46af-8561-f7eee565a19a.jpg?1562643297"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Nylea's Forerunner", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2cf2b6be-80a8-4464-a909-8cc658196a14.jpg?1581480774"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Obstinate Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/6694496c-45b9-4ddf-bfcd-b632441b8811.jpg?1562462698"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Ondu Greathorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95d9668e-05dc-41c4-9326-ef4c0e15dd80.jpg?1562930312"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oraxid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c05609a-f32d-4454-af24-a24452997dcb.jpg?1562630387"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Oxidda Scrapmelter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c64fe85b-e471-489a-8c38-2357da1c7969.jpg?1562822847"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Paleoloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b83ad801-44e7-48d0-9f34-0d10536bb4dc.jpg?1562803341"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pallimud", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/61adc314-cfb2-4fdd-925c-cc1dc4692992.jpg?1562054248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/1/610bb98c-d66a-44cc-92e2-a80d700b59e4.jpg?1591228161"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Parcelbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5ac98e5-a22c-41b5-94a9-b37b5aeb124f.jpg?1604781949"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Petradon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ac6311-8516-4db2-8c1f-626f0db0d36f.jpg?1562630404"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Petravark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/f/ffc98d09-439e-426b-8403-4a3e12167336.jpg?1562632920"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phantom Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/572df99b-af44-4128-8b2c-e40b1cea816b.jpg?1562460582"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Phantom Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian Ingester", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/7/376e9829-23eb-4b43-9ec7-246cb3156e95.jpg?1562876645"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6c7576e2-1a95-453f-aab5-b08e21f28ba4.jpg?1559592288"}, "reprint": true, "digital": true, "set_type": "masters"}, {"name": "Phyrexian War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/7/e7d651f6-50be-4df9-80f8-4c62bb860e71.jpg?1562770649"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Belcher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/280ae211-f025-4971-83e6-118ca08a1911.jpg?1543675375"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaguemaw Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/52341830-8cea-421f-b901-9229004f2d45.jpg?1562611301"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plague Reaver", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/230b9bc8-29c8-49cb-b4f5-1aceeda8bf45.jpg?1608909892"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Plated Crusher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/d/cd68e01c-4a09-450b-bfa0-8fbac8721764.jpg?1562943464"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plated Seastrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97171611-c677-48a6-b081-98a27ecef979.jpg?1562820641"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Plaxmanta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/a/8ae3598d-4d76-45ac-ab96-00d27a8de6c8.jpg?1593272724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/5/856892c8-ba47-46d0-aec2-0416b55b9e88.jpg?1591227333"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Porcuparrot", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6373fe1-c834-419e-8a0b-590fb5dc555e.jpg?1604781828"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c859b339-b55b-41fe-948c-27502e3b3ea8.jpg?1591226459"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Pouncing Shoreshark", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54428228-83a0-440f-afe9-573c9d8640cc.jpg?1604781667"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Primal Huntbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/b/eb77f6a8-a9d6-4fdd-996e-70877199ebab.jpg?1562561489"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Primoc Escapee", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e6cb3e72-bb64-4b1e-a54b-1fe4fb4ad4c9.jpg?1562941357"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/d/3d978332-95bf-4f86-9e67-06f10983c267.jpg?1593273433"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Protean Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/8/88269739-8a38-4f75-a53e-4b4ce70f2aef.jpg?1658282664"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b9d1c11a-a32c-449c-95c6-450dce6c26d2.jpg?1604193011"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Prowling Felidar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e8df0aed-dd2b-4f1e-8dfe-aec07462b1e1.jpg?1604202426"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b6bf8191-3154-48d7-a49b-4d07b5e35a15.jpg?1580014350"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Prowling Pangolin", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/f/0f037e99-75fb-4a2a-b4c6-448ef21b16a3.jpg?1562898495"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Putrid Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/9127942b-d73d-42a9-9f97-6a39fa798a8b.jpg?1562532123"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quagnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/335c3aa3-af89-44ce-955a-69e12d83175f.jpg?1562905350"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quartzwood Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8e4c609-19c9-433b-a852-7999e375ee4f.jpg?1591605359"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quicksilver Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/4/645bfe2d-845b-4cf3-88b6-b2b62b8531e4.jpg?1562637248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Quillspike", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/14cb4054-d5d6-4015-ae86-6f99280afe0a.jpg?1562899380"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Qumulox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/54102e68-dded-440c-b9b1-28771c8033d4.jpg?1562877043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Raging Kronch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/e/ae38aa2d-6c0e-409a-bfc7-ed4281457670.jpg?1557576793"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rakeclaw Gargantuan", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d1995ab8-7382-4c2a-b8c7-8b9272cab4fb.jpg?1562709274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/6/66ae703d-b133-4749-9d38-216abe6c6647.jpg?1562612913"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rampaging Baloths", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aac9448c-c802-476a-87ef-e1d745fd862a.jpg?1605370770"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Rampaging Rendhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12c1b820-0f06-41f6-804f-5c98f60c1529.jpg?1584831217"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/9/c98182d6-5b25-4493-9286-f29633e1bec4.jpg?1592666556"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ravenous Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/8/68c1142a-58c1-4a8e-808b-d47a45abb76b.jpg?1592666558"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/2/02551196-ecea-472f-9547-3c9658d0489e.jpg?1555040291"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/beast3.json b/web/public/mtg/jsons/beast3.json new file mode 100644 index 00000000..d726db0d --- /dev/null +++ b/web/public/mtg/jsons/beast3.json @@ -0,0 +1 @@ +{"has_more": false, "data": [{"name": "Ravenous Chupacabra", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e2af348-e768-44ca-b847-d541a0b0e6e0.jpg?1645141508"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Ravenous Gigantotherium", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca260253-40b8-4846-9e41-4e9cfc56d691.jpg?1591319695"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Ravenous Leucrocota", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/e/1e91524b-4885-45fc-b22d-f9e5ee55845d.jpg?1593096288"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Razing Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/2/d2090b80-2ce2-4c9a-87fe-d221f3c677b4.jpg?1562939456"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Realm Razer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da3ecfc6-1f9e-443e-a445-51df518025a5.jpg?1562709702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Relic Sloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c1cb483f-c567-4cfd-9fe8-1503e7b40542.jpg?1624739702"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Renegade Krasis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/23b68921-0c34-4d92-83c3-21542f62c7f6.jpg?1562901608"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/8/58388a29-b2a6-4d16-b872-f198563721d9.jpg?1562630034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rhox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/d/5d5f3f57-410f-4ee2-b93c-f5051a068828.jpg?1655270060"}, "reprint": true, "digital": false, "set_type": "starter"}, {"name": "Ridgeline Rager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5f663a4a-592a-4a3b-bbaf-e9c5c3049021.jpg?1562912585"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridge Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/4275a8dd-f777-4160-b773-9a868e743218.jpg?1562703177"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgescale Tusker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/84b689cc-35ef-4a23-bb1e-4d81b9fb8455.jpg?1579814138"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ridgetop Raptor", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/1013cbc4-09f4-484f-b328-9f7403225149.jpg?1562898258"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Riptide Mangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/3/5314a802-85d6-4d7b-ae9a-ca64eec652cf.jpg?1562911887"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "River Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/970adaaf-1534-4529-8da4-c4dcf7c08b7b.jpg?1562833446"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Roaring Primadox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19529b2f-03f0-469d-92d4-e2a2a933d5dc.jpg?1562550917"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rock Badger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/dff05df8-76f5-48c6-ac96-7b4e6a7050f6.jpg?1562383505"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Ronom Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e5b4b14c-e6fa-4cd2-9be7-fa2a2df05de1.jpg?1593275458"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Root Greevil", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/0/306e3429-b3b4-4186-935b-18cfc308d22c.jpg?1562905210"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rotted Hystrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/b/7bcae97d-468a-4e16-bfed-d2946f64784c.jpg?1562879013"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rumbling Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8610ff1-064b-4c75-a8df-d3b076370d1e.jpg?1562835728"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/a/6a7c6b2c-9ba0-4fc1-9922-0988acf2dfde.jpg?1627706779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Rust Monster", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/f/bf004dae-c411-4b0e-b695-fd727f475948.jpg?1627711737"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Sabertooth Nishoba", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8338c296-cf3f-41d7-b380-3fb4237cb41c.jpg?1562921586"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sagu Mauler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4c64af58-963d-497b-ab95-104839d96b94.jpg?1562786271"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctuary Smasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/c/cc634c10-42c5-4bdc-bc22-f862ae285492.jpg?1591227414"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sanctum Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/3/73887514-7644-4b2b-8c67-4b7e64150478.jpg?1562642111"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Squid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4efd7ce9-b920-409d-a4d2-a07fff280712.jpg?1562380860"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sandstorm Charger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9757be26-4480-43b7-a38a-8e4bde4e2d50.jpg?1562790274"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sand Strangler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd7153be-ad6c-47ff-8f45-bc8df17973cb.jpg?1562817478"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saprazzan Breaker", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2de7bf0f-5ad5-467b-ad80-28517951bbe1.jpg?1562379910"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sawtusk Demolisher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/574d1a02-a403-4b6e-8ce0-a472325c9c2c.jpg?1591319710"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Scalpelexis", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29c3b7fa-78e7-4a0c-bcdc-4b829638e3f6.jpg?1562629108"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scragnoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d80f7fa7-e7c4-4fc4-99bf-8a8502965fc8.jpg?1562056876"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Screeching Harpy", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/0/10c02902-4e3a-445e-9dd9-116806ddc966.jpg?1562052779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sea Snidd", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/a/ca11015e-200b-488c-8bf5-662dcc03cd2d.jpg?1562937660"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Bruiser", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc2de8a4-0d84-4f7c-bbe4-3a31172186ab.jpg?1562954767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shaleskin Plower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42658b33-9a12-403b-bc7d-807fbe1f1a36.jpg?1562908348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Shivan Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7958a1e5-b671-4ecb-95de-240ffaf5021e.jpg?1562574880"}, "reprint": false, "frame_effects": ["colorshifted"], "digital": false, "set_type": "expansion"}, {"name": "Shore Snapper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/5/157e5763-4892-47e4-8fd5-f576844c0a0d.jpg?1562701373"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Siege Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/1/918fb717-8ad3-4804-a62e-902baea58cfb.jpg?1561950184"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Sigiled Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0195ee6-c5d9-402e-8339-2caa50c4e46b.jpg?1562644651"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silt Crawler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f334e864-4e62-4bc3-9470-661be3d879e2.jpg?1562940692"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Six-y Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0379c99c-94b1-4c48-b62d-7accb594ef1a.jpg?1562487439"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/b/2b2dcafd-eb72-4f3a-9c1c-ba17fe30bf0f.jpg?1561820572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skarrg Goliath", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/0357e2ce-da68-46ff-a7e6-86df8a8ce91c.jpg?1605371304"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Skittish Valesk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/c/4cc8a6e6-ed62-4784-ba9a-b1f703fc6119.jpg?1562912967"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c01d17e-45a2-4b6f-aaa5-2af9c8f26181.jpg?1562628866"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Cutter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/5/a558c4f5-a716-4e46-9234-5f84f1bd57aa.jpg?1562631366"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud Ridgeback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/410896ab-d3dc-478c-bfd1-c0cad5b1180a.jpg?1562629551"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Skyshroud War Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19d809c1-e674-40b8-816d-c45d77c66722.jpg?1562087347"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slaughterhorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3fcc7a-ff5b-4695-aa86-9166f6cba565.jpg?1561853432"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/4/c4e4bbea-7e3f-4de0-bb01-dfd67f21c254.jpg?1547518325"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Slippery Bogle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/9/19714d6c-2bfa-4ee0-aa2f-5ccc196bc5d8.jpg?1562900327"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Slipstream Eel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/9/e9d06a1f-00b7-440d-849d-efc466d73f29.jpg?1562950698"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Gnarlid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/834409e3-134e-4a34-89cb-53e2a039e980.jpg?1562925959"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snapping Thragg", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/8/c8a47d41-b893-46b9-90c9-ccd8f9f78855.jpg?1562942401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snarling Undorak", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/5/05788d63-6210-44f2-9ae4-e55e9507a3a9.jpg?1562896264"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Snorting Gahr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e568503e-a886-4c8b-9d46-8520c2cdda48.jpg?1562383519"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ead79d2c-170e-4106-962d-d69c4b5fead0.jpg?1562770654"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soldevi Steam Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9de5e730-1d5c-4326-b3fc-2f0f97edc07e.jpg?1575874846"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spark Fiend", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea73a7ef-e9da-4d5b-aa4d-a953cbacd6c2.jpg?1562799182"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spearbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/3/132367ee-22e9-48e2-82e0-62ad9aaa62f3.jpg?1562701266"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Species Gorger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/0/e0087a98-55cf-4c8b-a180-fb0d9c336eb2.jpg?1562936816"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellbreaker Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a197e3f2-e69f-4716-9979-a304a87506c3.jpg?1562643286"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiked Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/2/522777b1-a89f-4969-a962-0137018ec86c.jpg?1562553788"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spinal Villain", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6d5e36f-0049-4be8-bf85-8dc0186339a4.jpg?1562861348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spinebiter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/f/cfc79ac6-ffc6-4506-9dea-e20176f960ea.jpg?1562881679"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spined Basher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/d/4d0d666a-8e31-466c-937f-54df910f664e.jpg?1562913024"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spirespine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/c/ac71491f-3027-4257-a18f-ba4de6041feb.jpg?1593096345"}, "reprint": false, "frame_effects": ["nyxtouched"], "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/9/b96d6e67-f690-4f19-bb25-a7c2d2aaf42f.jpg?1562938690"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spiritmonger", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/e/ce20919e-cdc7-465d-8653-4b912ff08997.jpg?1561929929"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Spitting Gourna", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/746b98bf-5398-4a00-b4fe-a990ea9cfd77.jpg?1562922510"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sproutback Trudge", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/b/dbf26e54-bdfe-4da8-acbb-4f1a98faba49.jpg?1625192442"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spur Grappler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/0/50bf91a7-4d04-437c-a290-6adb52f25312.jpg?1562909787"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spurred Wolverine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/6/46d7aaea-226b-4820-8db2-89dcdcbcc557.jpg?1562911611"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Serow", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/7/47c63065-6051-4193-8457-713a8a800393.jpg?1562493496"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stampeding Wildebeests", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddb5f524-fad6-4a63-b20f-3348a844fefa.jpg?1562278656"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stomper Cub", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/9/89be64a8-dd78-48c3-bb47-4f2a5ad9ec10.jpg?1562706034"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stonework Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/2/a29e17ba-d584-4296-9f43-17467edaa25f.jpg?1604201060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/324bc757-9942-4862-b691-5af42e07f682.jpg?1562905516"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stratozeppelid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/c/7ccfc49d-2a07-4088-a288-ba7be4da7bc2.jpg?1593272091"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swarm Shambler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/a/7a7e4f99-ece4-473e-b712-40e4c53558e8.jpg?1604199508"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sylvan Brushstrider", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8bc288a3-ea56-450a-96fd-c2123121f663.jpg?1584831296"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Symbiotic Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb61443d-e47a-4fe1-b777-67a3670a5a56.jpg?1562939214"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tangle Hulk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/e/8ed3c301-8d8e-45fe-902a-af03a79525be.jpg?1562612950"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tenement Crasher", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/4/44af9170-bd99-4fde-b673-62d988312b2d.jpg?1562785527"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tephraderm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/1/41b65eba-140b-4c1d-b796-8134b7c1ede8.jpg?1562910455"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Terra Ravager", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/124dd668-ad84-45b9-9e04-1ea7cd2d7024.jpg?1562898786"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Terra Stomper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/a/4ab062f4-e4b1-4129-9027-d0ca1a723273.jpg?1562611988"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/c/0c3d4afc-5bb7-4159-9a11-f9c989dd9043.jpg?1562897795"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Territorial Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/5/45033b8a-f3a8-4a23-b6b0-e011e3e7a4c1.jpg?1562611772"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbound Primoc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/8/e89156b5-8bdb-41d1-a7aa-63f770a9b070.jpg?1562950377"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Devourer", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba7a96ee-e2d1-4d76-a09e-d6868ddd9282.jpg?1562929803"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Eater", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/e/4e05f63c-f93d-44b9-98e9-c5e3e3aad6b9.jpg?1562909299"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Nibbler", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7284a7fd-cda8-43ac-b119-ad47b33c2ec4.jpg?1562916262"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/8/28667c8b-d02c-4e57-a050-1549207b65d1.jpg?1562551691"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Thragtusk", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43e1e3f3-a9b8-4185-9be9-798fe3cddd5c.jpg?1640744362"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Thrashing Mudspawn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/a/da84de0e-a4cd-4dff-8ee3-87c9debf0969.jpg?1562947056"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thrashing Wumpus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/6/86bc07c6-2ba7-41f8-90ab-f9bbac86dd08.jpg?1562381841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thresher Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/57996732-c9e4-4271-9d5f-2a8c77f8d177.jpg?1562911143"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e376a953-2075-4595-a3ef-85d0f68aa8b2.jpg?1650426042"}, "reprint": true, "digital": false, "set_type": "commander"}, {"name": "Thunderfoot Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/9730de49-efa9-42ec-8531-43313fb58a44.jpg?1561951126"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Thundering Tanadon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2fab443-0f4b-45ea-8a6d-435b93803409.jpg?1562882228"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Timbermaw Larva", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d68fc3bc-eb3b-4504-93a3-8943d07b23f8.jpg?1562617126"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanic Bulvox", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/f/3f42c4d7-b555-449c-a539-119c1ae62232.jpg?1562528017"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/d/9d02e1e8-b85b-4e26-8ab8-ca2f49d05b88.jpg?1591227898"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Titanoth Rex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/4/b4817b86-d55a-4334-82ee-603f8c4b3e93.jpg?1590879818"}, "flavor_name": "Godzilla, Primeval Champion", "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/a/2a8cc948-28ff-4bbe-b8c9-71de37478023.jpg?1562905065"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Towering Indrik", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6049e92-6c52-44be-a3c7-aa8e8bf9c10a.jpg?1562792972"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trapjaw Kelpie", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/2/62615f86-0431-4709-b41c-af43f7793fdb.jpg?1562915541"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Treespring Lorian", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f525d7ce-37d3-4989-beb4-173447cb5294.jpg?1562953129"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trove Warden", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/3336593c-c83c-48e7-9173-2c2b74b94d3b.jpg?1604195307"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/6/063a95ee-3fda-436f-9ff8-de80cc874dde.jpg?1591228292"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trumpeting Gnarr", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/f/2fe88a45-a420-4998-b242-b475c6b5b0bc.jpg?1604781989"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "expansion"}, {"name": "Trusty Packbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/8320e35b-15b9-4f98-b9b8-9c951696408b.jpg?1562302921"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b14a8b3-1a85-400b-b17c-a28ed145d720.jpg?1561967848"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Trygon Predator", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f31f54bf-7bf0-48f0-853d-1468713784eb.jpg?1593273791"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tusked Colossodon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d511407-0c1e-4342-a578-ca557c6886fd.jpg?1562784330"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tyrranax", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/c/5cb0cc0e-f71f-456f-a6ec-6a70cf838c35.jpg?1562877248"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undying Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/c/9c95c752-3add-4830-8159-036b8689f40a.jpg?1562447348"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Ursapine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba547810-c82a-498b-81eb-e81a8dcbbd42.jpg?1598916680"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Vagrant Plowbeasts", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/4/546b0a74-ebef-4596-b730-2190e20b2e66.jpg?1562801037"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Valley Rannet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2027335a-224b-411d-a59f-f4ad39b38a69.jpg?1562640043"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Venomspout Brackus", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/0774771c-5373-4636-9174-d06e7d635183.jpg?1562896736"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vigilant Baloth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/4/34ad8e5d-0c26-4588-8161-b22197715d63.jpg?1562301653"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2c681e3-fc54-4da1-80ff-13507688dbc3.jpg?1562247258"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Vizzerdrix", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/5/25711022-7270-4335-a48b-9f2b8275ceeb.jpg?1562873595"}, "reprint": false, "digital": false, "set_type": "starter"}, {"name": "Voracious Typhon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa2bccb-0e01-4629-b9a8-5c0ea26239b3.jpg?1581480923"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Vulshok War Boar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb6b232a-834c-4c9a-bf36-821d125dc318.jpg?1562639233"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "War Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/5/652109b9-d607-42b6-945d-0c0dd5bba89c.jpg?1562787724"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wayward Guide-Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/0/d00f8ab0-61cd-4721-b974-a2516da77d39.jpg?1604198443"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Weaver of Lies", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/2/12172d0e-0c73-4482-9f83-2c23ace9b7a0.jpg?1562898647"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wild Colos", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d39f746-7b82-476a-9774-3375debb47bd.jpg?1562443743"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woodland Bellower", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/7/a706d4bb-0b44-4e43-b340-7de799c086b8.jpg?1562034880"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Woodripper", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/1/5126b782-d74c-40ca-a9b2-a6c78f94d138.jpg?1562629900"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Razorback", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/5/95ed6354-161e-496e-9ac7-74432f9b0818.jpg?1593274871"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/d/7d5907d5-ae5c-4c9d-a5df-61f1c94f979d.jpg?1562705775"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Woolly Thoctar", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/b/fb3a2bb2-3ba7-4486-84c9-3aab85c368e1.jpg?1561758467"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Wormfang Behemoth", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/c/1c7f29aa-c069-4adb-b313-6a56849905d4.jpg?1562628869"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Manta", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bc9bf91d-6f7c-4fb5-bbc6-c012212e62e9.jpg?1562631728"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Newt", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/f/df8012c1-76ec-4c36-8b38-5bc41ce5e156.jpg?1562632319"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wormfang Turtle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/8/48404362-7579-4896-a71a-8eb40e5ac416.jpg?1562629707"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wrecking Beast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/4/74e6f7be-4493-4081-ac67-d782ab2b3723.jpg?1584831344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wretched Anurid", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/a/aab525ad-1f62-4d9c-9b74-c7b0048da452.jpg?1562935315"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Yoked Plowbeast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/ddbbc7dc-efdf-46e8-bf19-0daa4034f6ec.jpg?1562709823"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Zhur-Taa Ancient", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/0/2076308f-0f4e-4b31-9e75-c2965942e7d1.jpg?1562900996"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file diff --git a/web/public/mtg/jsons/counterspell3.json b/web/public/mtg/jsons/counterspell3.json index 97aeadc5..536dc98a 100644 --- a/web/public/mtg/jsons/counterspell3.json +++ b/web/public/mtg/jsons/counterspell3.json @@ -1 +1 @@ -{"has_more": false, "data": [{"name": "Rust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sage's Dousing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saw It Coming", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter Arc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scattering Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter to the Winds", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scent of Brine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Second Guess", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sinister Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Manipulation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Contortion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Counter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Crumple", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spelljack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Spell Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellshift", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Shrivel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Spell Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snip", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Spell Suck", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Swindle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Syphon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Split Decision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Squelch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Statute of Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Steel Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Stoic Rebuttal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stubborn Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stymied Hopes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sublime Epiphany", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Suffocating Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summary Dismissal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summoner's Bane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Swift Silence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syphon Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tale's End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Teferi's Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Temur Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Test of Talents", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Rebuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Collapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thwart", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tibalt's Trickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Time Stop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trap Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Traumatic Visions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trickbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undermine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undersimplify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Unified Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Unwind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Very Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Vex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Void Shatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Warping Wail", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wash Away", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Wild Ricochet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Withering Boon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wizard's Retort", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "You Find the Villains' Lair", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file +{"has_more": false, "data": [{"name": "Rust", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad4974c8-34c5-4290-b325-7586a67f6d56.jpg?1592364545"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sage's Dousing", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/5/75ccd5f6-b363-433f-9e98-f65e10b10bc9.jpg?1562879335"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Saw It Coming", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/7/877a1bb9-5eae-453a-bec0-a9de20ea6815.jpg?1631047574"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter Arc", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/2/32ed969f-2c8e-4421-9448-dc5a2afdc81d.jpg?1561821983"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scattering Stroke", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/5/c536c1ce-a012-4d77-ab29-8574be164731.jpg?1562367009"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scatter to the Winds", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d73ad49f-fe15-4fe5-9731-fd71d31c1e7f.jpg?1562946348"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Scent of Brine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/1/d117bf8d-23ec-4f9d-99d0-3a990c5f7075.jpg?1562445215"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Second Guess", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/d/0d22d093-8e89-4d54-ac04-14c8759de3ea.jpg?1592708686"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba26dbbc-d4a2-44a1-8e6b-affe61f43a34.jpg?1562792137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Silumgar's Scorn", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/077bee72-62f6-4d90-8557-ff9cac42ec9a.jpg?1562782102"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sinister Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cbef36d-7170-424f-8fb1-8e7e112b7f0b.jpg?1572892841"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Soul Manipulation", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/c/bcd3cb05-c6f9-435a-a0e7-1f85da4a36eb.jpg?1562643969"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/2/42d7af6a-bfd1-4e89-965a-68336507a9ee.jpg?1562828463"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/f/5fe58a24-f6a6-4858-82a5-0ca1d524efe1.jpg?1562054243"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/0/70e4584f-6e44-4ff8-8313-c8791e0156af.jpg?1562591827"}, "reprint": true, "digital": false, "set_type": "core"}, {"name": "Spell Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/4/845734da-ab03-4dbc-bb5f-96481d3b8e88.jpg?1559591342"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/8169929c-641f-41c8-a48e-1a7d0c57726b.jpg?1619394723"}, "reprint": true, "digital": false, "set_type": "masters"}, {"name": "Spell Burst", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f95c8015-fd7d-4329-ab23-aec37a824083.jpg?1562947751"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Contortion", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4b748d8b-898f-4b55-bc33-f5bbbc823c45.jpg?1562286779"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Counter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/3/e3d323f0-334f-49d1-b338-24c4b854a112.jpg?1562489832"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Crumple", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/2/2247df4a-c5d8-4b34-b3a6-3c958eb65f94.jpg?1592713127"}, "reprint": false, "digital": false, "set_type": "commander"}, {"name": "Spelljack", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eda8c7b-ce35-482a-bece-52a30cc78a9a.jpg?1562629500"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/e/beb42273-935b-4bda-849e-c163606cf89e.jpg?1654566963"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/b/6bf4dfc0-c58b-4535-b660-54ceaa6e0217.jpg?1562557054"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/b/cb3d3901-e4a6-45ab-a7b5-c65d91e1875e.jpg?1562616640"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/c/fc802c6b-5269-4f02-8b61-342931fea828.jpg?1658430060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/c/fc802c6b-5269-4f02-8b61-342931fea828.jpg?1658430060"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "promo"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/3/d3c8f1c8-2b57-41a3-abeb-77ac7de62fa1.jpg?1656006437"}, "reprint": true, "frame_effects": ["inverted"], "digital": false, "set_type": "masters"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/4/a4f8b11a-6b21-4532-96c9-bdb2cad603e8.jpg?1599332212"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Spell Pierce", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/e/eef1f68a-b27c-4e81-9a3c-dccb86771bec.jpg?1562942998"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Spell Rupture", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/2/7267fcec-0879-4743-a45f-35057ccb2596.jpg?1561831328"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spellshift", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/5/f5c897a6-5835-42ac-8cc7-e8d9fc1e7c77.jpg?1562586074"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Shrivel", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efa110cb-f091-48f0-bc62-80f5f18568e8.jpg?1562951938"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Spell Snare", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/5/35554fdf-c70a-4baa-a35a-414caa9978be.jpg?1593272766"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snip", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/6/d6870203-ece9-4fe0-912b-2dcf685f3eb0.jpg?1562709543"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Snuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efadce19-07f4-47af-abc0-a436bafcdd65.jpg?1562201508"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Spell Suck", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/6/f631bd92-2046-468d-8b10-d583a318ed24.jpg?1562946926"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Spell Swindle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e619ada-e9ce-4758-afd8-8def853877eb.jpg?1562557238"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Spell Syphon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/8/b883113c-e52b-4633-b4a4-016093327b6a.jpg?1562835117"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Split Decision", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83ed7ebe-48be-4e6e-a293-b81484f85142.jpg?1562865914"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Squelch", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/9/29421dd2-70a7-4623-afe0-ca4cb415ec87.jpg?1562758853"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Statute of Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/f/af13770d-dddb-4b78-9cd3-4a0dc50472f4.jpg?1562792750"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Steel Sabotage", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/b/bb40de7c-1905-4615-844b-4abc231fb01e.jpg?1562614249"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/d/2d7643c0-b2db-478f-944e-b27b77bad3eb.jpg?1562527068"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/a/ea24228f-da16-46eb-9dcf-a377286b6168.jpg?1562942013"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Stifle", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6228e16-72d4-4771-9e3f-a83ec856d315.jpg?1562636845"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Stoic Rebuttal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2805239-f30a-4eca-a10b-41673daaa287.jpg?1562825062"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stubborn Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/f/6f8626c4-306f-4e9d-8840-2bb73fe87e87.jpg?1562788344"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Stymied Hopes", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/7/5702b757-5be5-4a48-bc73-a87ec4f3193b.jpg?1562818334"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Sublime Epiphany", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/d/ad1bcb44-a562-4f66-b862-6d0ef3546ab4.jpg?1594735795"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Suffocating Blast", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/2/c2a70297-2a7b-4a0c-ace5-cd61bfe6dafd.jpg?1562940975"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summary Dismissal", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/b/0b75794d-3334-4b4d-9446-0a251dd3bd15.jpg?1576384222"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Summoner's Bane", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/d/ed82afba-df51-4bd9-853c-d3ef323095a6.jpg?1562618060"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Supreme Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/6/b677e7cb-7b5d-4993-8f13-881493c498ce.jpg?1562811958"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/f/efd26041-059b-4a1e-9ce8-c3cfd69a3721.jpg?1562837218"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Swan Song", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/0/40fc6412-df1c-4bfa-842b-8c3a6f14e19d.jpg?1599358784"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Swift Silence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/a/1/a1c5f733-e126-4c22-b528-18bdb90b509b.jpg?1593273784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/8/08375017-4432-4296-9799-966db145ed7c.jpg?1643588741"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/8/f81739a5-35a7-4812-a7af-e1951bf5579c.jpg?1617884773"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba6f218f-83b0-4b68-a00f-0327cd79f32a.jpg?1562792232"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Syncopate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/7/b7850794-4c85-4844-a461-650cd4eaec93.jpg?1562929140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Syphon Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/435a2d31-ac2c-45aa-8369-6c2d6fbba4e4.jpg?1643588767"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tale's End", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/1/4/1421115b-9a98-4ab2-bcb2-7d8899ce12db.jpg?1592516519"}, "reprint": false, "digital": false, "set_type": "core"}, {"name": "Teferi's Response", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/3/f3bb2df8-c559-4a34-83b0-d48fbc694cc8.jpg?1562944007"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Temur Charm", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e2ee3e36-a849-42b0-b84b-027a08427c35.jpg?1562794960"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Test of Talents", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e2b6236-b40c-430c-98b0-7940b942657a.jpg?1624590572"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Intervention", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/c/2c1241d0-20d4-4eab-970d-74e476f023b4.jpg?1584279765"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thassa's Rebuff", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/1/816a6ff7-cede-4346-b3e6-aee33aefac3a.jpg?1593091807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thoughtbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/9/7919cf41-67bb-4dc4-90de-cf3fa2096c2e.jpg?1593860622"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thought Collapse", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/4/948b569b-6341-418b-99b5-f79dfb3fe8dd.jpg?1584830401"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Thwart", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/1/c12a0717-e9ea-4be3-a29f-179671ed4489.jpg?1562383015"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Tibalt's Trickery", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/d/dd921e27-3e08-438c-bec2-723226d35175.jpg?1652278784"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Time Stop", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/9/f968c5e9-12a8-4542-90b4-84e0238fa375.jpg?1562766084"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trap Essence", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/0/c063b2b8-5243-43a8-8cb0-927116003bda.jpg?1562701652"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Traumatic Visions", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/1/f1e8b03d-9265-4699-b626-5efa73292d43.jpg?1562804612"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Trickbind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2e58ff2-dea3-42b3-8c22-3e6202a7d433.jpg?1562946300"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/b/3b7573c2-484c-4b4e-9c26-0f005bd1daee.jpg?1576384240"}, "reprint": true, "digital": false, "set_type": "expansion"}, {"name": "Turn Aside", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/5/6/56226f57-6ff0-430e-aba6-6b3dd51f8d3c.jpg?1562817712"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undermine", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/3/2334bc71-5f85-47ff-b393-601a1e746a4e.jpg?1562902053"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Undersimplify", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/e/3eaebdc1-7a20-45db-9d45-0238fc917496.jpg?1656479084"}, "reprint": false, "digital": true, "set_type": "alchemy"}, {"name": "Unified Will", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/c/6cb50db7-f1d4-4f9d-ac60-564398af79ea.jpg?1562704807"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/ba5dac3d-4b49-44c4-a7b2-0a99485252c9.jpg?1576384246"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Unsubstantiate", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/b/8b184d7e-46ae-450e-9228-eb605ac3ad41.jpg?1562924384"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Unwind", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/7/97da6607-9131-4f8b-8af3-63439a59b78b.jpg?1562739909"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/3/83031ea8-a6c9-4318-af16-bba701dd76bb.jpg?1626097990"}, "reprint": false, "digital": false, "set_type": "draft_innovation"}, {"name": "Verdant Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/7/070a3f30-0839-4678-a37c-475ee189811e.jpg?1626101883"}, "reprint": false, "frame_effects": ["showcase"], "digital": false, "set_type": "draft_innovation"}, {"name": "Very Cryptic Command", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/8/d8e84dd2-01f9-4fad-8a24-cc86424d09a2.jpg?1562940811"}, "reprint": false, "digital": false, "set_type": "funny"}, {"name": "Vex", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/2/e28a9f15-5469-4dc2-8a73-646f854fec7e.jpg?1562640140"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Void Shatter", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/b/4bf13c5e-3968-48ad-ba08-99ba58873223.jpg?1562910363"}, "reprint": false, "frame_effects": ["devoid"], "digital": false, "set_type": "expansion"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/6/e640664f-5cc7-4970-b966-6e6e5ae09c5a.jpg?1640462194"}, "reprint": true, "digital": false, "set_type": "box"}, {"name": "Voidslime", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/2/6/265c269e-1b5e-4e5f-873f-7733bd4142aa.jpg?1562384947"}, "reprint": true, "digital": false, "set_type": "promo"}, {"name": "Warping Wail", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/2/f2ef4db8-b51c-4f52-84f1-6fee31c4a14c.jpg?1562943843"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wash Away", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/4/3/43411ade-be80-4535-8baa-7055e78496df.jpg?1643588844"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/9/e/9e127856-bedd-40a9-9e8e-d1f9fbefe07d.jpg?1581479658"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Whirlwind Denial", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/f/7/f7a0c25a-8760-44ea-a418-fcd4a9761632.jpg?1623594049"}, "reprint": true, "digital": false, "set_type": "masterpiece"}, {"name": "Wild Ricochet", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/7/d76f09bc-b49a-4ad2-be2d-2a191d41b86d.jpg?1562370137"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Withering Boon", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/e/6e6499cb-6073-4c94-8c82-47f489094df5.jpg?1562719780"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "Wizard's Retort", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/b/a/bae30b7d-9306-46ef-adea-c4057f59c9c1.jpg?1562741944"}, "reprint": false, "digital": false, "set_type": "expansion"}, {"name": "You Find the Villains' Lair", "image_uris": {"normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159", "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/c/6/c6704458-6e9e-4795-a56d-25b68fbf9672.jpg?1627704159"}, "reprint": false, "digital": false, "set_type": "expansion"}]} \ No newline at end of file From d3da6de5dd5bec5b6b180f97d71a00d0c889626a Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 28 Jul 2022 11:37:26 -0700 Subject: [PATCH 369/519] Groups default open --- web/components/groups/create-group-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/groups/create-group-button.tsx b/web/components/groups/create-group-button.tsx index 0685d8e4..360c4ea8 100644 --- a/web/components/groups/create-group-button.tsx +++ b/web/components/groups/create-group-button.tsx @@ -46,7 +46,7 @@ export function CreateGroupButton(props: { const newGroup = { name: groupName, memberIds: memberUsers.map((user) => user.id), - anyoneCanJoin: false, + anyoneCanJoin: true, } const result = await createGroup(newGroup).catch((e) => { const errorDetails = e.details[0] From ada3f0787c06d0f2f85355f2e29557d9b80e242f Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 28 Jul 2022 11:44:07 -0700 Subject: [PATCH 370/519] create: add bottom margin --- web/pages/create.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 84ac82da..ca29cba9 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -477,6 +477,8 @@ export function NewContract(props: { {isSubmitting ? 'Creating...' : 'Create question'} </button> </Row> + + <Spacer h={6} /> </div> ) } From 693eb96d23da49fc1f28e16bcb7b02a5be3e128f Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 27 Jul 2022 19:47:25 -0700 Subject: [PATCH 371/519] Include groupId when duplicating markets --- web/components/copy-contract-button.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/components/copy-contract-button.tsx b/web/components/copy-contract-button.tsx index fcb3a347..8536df71 100644 --- a/web/components/copy-contract-button.tsx +++ b/web/components/copy-contract-button.tsx @@ -49,6 +49,10 @@ function duplicateContractHref(contract: Contract) { params.initValue = getMappedValue(contract)(contract.initialProbability) } + if (contract.groupLinks && contract.groupLinks.length > 0) { + params.groupId = contract.groupLinks[0].groupId + } + return ( `/create?` + Object.entries(params) From bdea739c5516778037b11cd1dc5b5d3c1a0ad5ed Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 29 Jul 2022 09:20:21 -0700 Subject: [PATCH 372/519] multiple choice contract card --- web/components/contract/contract-card.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 164f3f27..e418178c 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -115,7 +115,8 @@ export function ContractCard(props: { {question} </p> - {outcomeType === 'FREE_RESPONSE' && + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && (resolution ? ( <FreeResponseOutcomeLabel contract={contract} @@ -158,7 +159,8 @@ export function ContractCard(props: { /> )} - {outcomeType === 'FREE_RESPONSE' && ( + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && ( <FreeResponseResolutionOrChance className="self-end text-gray-600" contract={contract} @@ -210,7 +212,7 @@ export function BinaryResolutionOrChance(props: { } function FreeResponseTopAnswer(props: { - contract: FreeResponseContract + contract: FreeResponseContract | MultipleChoiceContract truncate: 'short' | 'long' | 'none' className?: string }) { From 779b6dfc0c8bfa1d1231cf177b8786601ae484ab Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 29 Jul 2022 15:09:48 -0700 Subject: [PATCH 373/519] manalink referrals --- web/pages/link/[slug].tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index 119fec77..fa728c85 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -1,14 +1,17 @@ import { useRouter } from 'next/router' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' import { claimManalink } from 'web/lib/firebase/api' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' -import { firebaseLogin } from 'web/lib/firebase/users' +import { firebaseLogin, getUser } from 'web/lib/firebase/users' import { Row } from 'web/components/layout/row' import { Button } from 'web/components/button' +import { useSaveReferral } from 'web/hooks/use-save-referral' +import { User } from 'common/lib/user' +import { Manalink } from 'common/manalink' export default function ClaimPage() { const user = useUser() @@ -18,6 +21,8 @@ export default function ClaimPage() { const [claiming, setClaiming] = useState(false) const [error, setError] = useState<string | undefined>(undefined) + useReferral(user, manalink) + if (!manalink) { return <></> } @@ -76,3 +81,13 @@ export default function ClaimPage() { </> ) } + +const useReferral = (user: User | undefined | null, manalink?: Manalink) => { + const [creator, setCreator] = useState<User | undefined>(undefined) + + useEffect(() => { + if (manalink?.fromId) getUser(manalink.fromId).then(setCreator) + }, [manalink]) + + useSaveReferral(user, { defaultReferrer: creator?.username }) +} From 5812d8ed2ebde8557f8e09e1b9c038cfabdd3222 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 29 Jul 2022 16:02:18 -0700 Subject: [PATCH 374/519] manalink qr code --- web/components/manalink-card.tsx | 18 +++++++++++++++++- .../manalinks/create-links-button.tsx | 11 +++++++---- web/components/qr-code.tsx | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 web/components/qr-code.tsx diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index 51880f5d..c8529609 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -10,6 +10,7 @@ import { DotsHorizontalIcon } from '@heroicons/react/solid' import { contractDetailsButtonClassName } from './contract/contract-info-dialog' import { useUserById } from 'web/hooks/use-user' import getManalinkUrl from 'web/get-manalink-url' +import { QrcodeIcon } from '@heroicons/react/outline' export type ManalinkInfo = { expiresTime: number | null maxUses: number | null @@ -78,7 +79,9 @@ export function ManalinkCardFromView(props: { const { className, link, highlightedSlug } = props const { message, amount, expiresTime, maxUses, claims } = link const [showDetails, setShowDetails] = useState(false) - + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${200}x${200}&data=${getManalinkUrl( + link.slug + )}` return ( <Col> <Col @@ -127,6 +130,19 @@ export function ManalinkCardFromView(props: { > {formatMoney(amount)} </div> + + <button + onClick={() => (window.location.href = qrUrl)} + className={clsx( + contractDetailsButtonClassName, + showDetails + ? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600' + : '' + )} + > + <QrcodeIcon className="h-6 w-6" /> + </button> + <ShareIconButton toastClassName={'-left-48 min-w-[250%]'} buttonClassName={'transition-colors'} diff --git a/web/components/manalinks/create-links-button.tsx b/web/components/manalinks/create-links-button.tsx index 656aff29..449d6c76 100644 --- a/web/components/manalinks/create-links-button.tsx +++ b/web/components/manalinks/create-links-button.tsx @@ -12,6 +12,7 @@ import dayjs from 'dayjs' import { Button } from '../button' import { getManalinkUrl } from 'web/pages/links' import { DuplicateIcon } from '@heroicons/react/outline' +import { QRCode } from '../qr-code' export function CreateLinksButton(props: { user: User @@ -98,6 +99,8 @@ function CreateManalinkForm(props: { }) } + const url = getManalinkUrl(highlightedSlug) + return ( <> {!finishedCreating && ( @@ -199,17 +202,17 @@ function CreateManalinkForm(props: { copyPressed ? 'bg-indigo-50 text-indigo-500 transition-none' : '' )} > - <div className="w-full select-text truncate"> - {getManalinkUrl(highlightedSlug)} - </div> + <div className="w-full select-text truncate">{url}</div> <DuplicateIcon onClick={() => { - navigator.clipboard.writeText(getManalinkUrl(highlightedSlug)) + navigator.clipboard.writeText(url) setCopyPressed(true) }} className="my-auto ml-2 h-5 w-5 cursor-pointer transition hover:opacity-50" /> </Row> + + <QRCode url={url} className="self-center" /> </> )} </> diff --git a/web/components/qr-code.tsx b/web/components/qr-code.tsx new file mode 100644 index 00000000..a10f8886 --- /dev/null +++ b/web/components/qr-code.tsx @@ -0,0 +1,16 @@ +export function QRCode(props: { + url: string + className?: string + width?: number + height?: number +}) { + const { url, className, width, height } = { + width: 200, + height: 200, + ...props, + } + + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${width}x${height}&data=${url}` + + return <img src={qrUrl} width={width} height={height} className={className} /> +} From 079a2a3936426edc70c1319dd0ddeddc79261ea6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 29 Jul 2022 16:06:22 -0700 Subject: [PATCH 375/519] eslint --- web/pages/link/[slug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index fa728c85..af3f01a8 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -10,7 +10,7 @@ import { firebaseLogin, getUser } from 'web/lib/firebase/users' import { Row } from 'web/components/layout/row' import { Button } from 'web/components/button' import { useSaveReferral } from 'web/hooks/use-save-referral' -import { User } from 'common/lib/user' +import { User } from 'common/user' import { Manalink } from 'common/manalink' export default function ClaimPage() { From be01a152305a769c5b2859d1e2ca01c25e872524 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 29 Jul 2022 19:08:51 -0500 Subject: [PATCH 376/519] Refactor search to not use Algolia components (#705) * In progress refactor to not use Algolia components * Cleanup * Only query when necessary * Read and update url params for query and sort * Don't push router * Don't update url if using default sort * Add popstate listener to improve home navigation * Remove console.logs --- web/components/contract-search.tsx | 366 +++++++++++------------- web/hooks/use-sort-and-query-params.tsx | 64 +++-- web/pages/home.tsx | 10 +- 3 files changed, 213 insertions(+), 227 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index c7660138..8596aa2e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -1,26 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ import algoliasearch from 'algoliasearch/lite' -import { - Configure, - InstantSearch, - SearchBox, - SortBy, - useInfiniteHits, - useSortBy, -} from 'react-instantsearch-hooks-web' import { Contract } from 'common/contract' -import { - Sort, - useInitialQueryAndSort, - useUpdateQueryAndSort, -} from '../hooks/use-sort-and-query-params' +import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, ContractsGrid, } from './contract/contracts-list' import { Row } from './layout/row' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useUser } from 'web/hooks/use-user' @@ -30,8 +18,9 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { Group, NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' -import { sortBy } from 'lodash' +import { range, sortBy } from 'lodash' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' +import { Col } from './layout/col' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -40,16 +29,15 @@ const searchClient = algoliasearch( const indexPrefix = ENV === 'DEV' ? 'dev-' : '' -const sortIndexes = [ - { label: 'Newest', value: indexPrefix + 'contracts-newest' }, - // { label: 'Oldest', value: indexPrefix + 'contracts-oldest' }, - { label: 'Most popular', value: indexPrefix + 'contracts-score' }, - { label: 'Most traded', value: indexPrefix + 'contracts-most-traded' }, - { label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' }, - { label: 'Last updated', value: indexPrefix + 'contracts-last-updated' }, - { label: 'Subsidy', value: indexPrefix + 'contracts-liquidity' }, - { label: 'Close date', value: indexPrefix + 'contracts-close-date' }, - { label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' }, +const sortOptions = [ + { label: 'Newest', value: 'newest' }, + { label: 'Most popular', value: 'score' }, + { label: 'Most traded', value: 'most-traded' }, + { label: '24h volume', value: '24-hour-vol' }, + { label: 'Last updated', value: 'last-updated' }, + { label: 'Subsidy', value: 'liquidity' }, + { label: 'Close date', value: 'close-date' }, + { label: 'Resolve date', value: 'resolve-date' }, ] export const DEFAULT_SORT = 'score' @@ -108,13 +96,12 @@ export function ContractSearch(props: { memberPillGroups.length > 0 ? memberPillGroups : defaultPillGroups const follows = useFollows(user?.id) - const { initialSort } = useInitialQueryAndSort(querySortOptions) - const sort = sortIndexes - .map(({ value }) => value) - .includes(`${indexPrefix}contracts-${initialSort ?? ''}`) - ? initialSort - : querySortOptions?.defaultSort ?? DEFAULT_SORT + const { shouldLoadFromStorage, defaultSort } = querySortOptions ?? {} + const { query, setQuery, sort, setSort } = useQueryAndSortParams({ + defaultSort, + shouldLoadFromStorage, + }) const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' @@ -123,62 +110,129 @@ export function ContractSearch(props: { const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) - const selectFilter = (pill: string | undefined) => () => { + const selectPill = (pill: string | undefined) => () => { setPillFilter(pill) + setPage(0) track('select search category', { category: pill ?? 'all' }) } - const { filters, numericFilters } = useMemo(() => { - let filters = [ - filter === 'open' ? 'isResolved:false' : '', - filter === 'closed' ? 'isResolved:false' : '', - filter === 'resolved' ? 'isResolved:true' : '', - additionalFilter?.creatorId - ? `creatorId:${additionalFilter.creatorId}` - : '', - additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', - additionalFilter?.groupSlug - ? `groupLinks.slug:${additionalFilter.groupSlug}` - : '', - pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' - ? `groupLinks.slug:${pillFilter}` - : '', - pillFilter === 'personal' - ? // Show contracts in groups that the user is a member of - memberGroupSlugs - .map((slug) => `groupLinks.slug:${slug}`) - // Show contracts created by users the user follows - .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) - // Show contracts bet on by users the user follows - .concat( - follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] - ) - : '', - // Subtract contracts you bet on from For you. - pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', - pillFilter === 'your-bets' && user - ? // Show contracts bet on by the user - `uniqueBettorIds:${user.id}` - : '', - ].filter((f) => f) - // Hack to make Algolia work. - filters = ['', ...filters] + let facetFilters = [ + filter === 'open' ? 'isResolved:false' : '', + filter === 'closed' ? 'isResolved:false' : '', + filter === 'resolved' ? 'isResolved:true' : '', + additionalFilter?.creatorId + ? `creatorId:${additionalFilter.creatorId}` + : '', + additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', + additionalFilter?.groupSlug + ? `groupLinks.slug:${additionalFilter.groupSlug}` + : '', + pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' + ? `groupLinks.slug:${pillFilter}` + : '', + pillFilter === 'personal' + ? // Show contracts in groups that the user is a member of + memberGroupSlugs + .map((slug) => `groupLinks.slug:${slug}`) + // Show contracts created by users the user follows + .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) + // Show contracts bet on by users the user follows + .concat( + follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] + ) + : '', + // Subtract contracts you bet on from For you. + pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', + pillFilter === 'your-bets' && user + ? // Show contracts bet on by the user + `uniqueBettorIds:${user.id}` + : '', + ].filter((f) => f) + // Hack to make Algolia work. + facetFilters = ['', ...facetFilters] - const numericFilters = [ - filter === 'open' ? `closeTime > ${Date.now()}` : '', - filter === 'closed' ? `closeTime <= ${Date.now()}` : '', - ].filter((f) => f) - - return { filters, numericFilters } - }, [ - filter, - Object.values(additionalFilter ?? {}).join(','), - memberGroupSlugs.join(','), - (follows ?? []).join(','), - pillFilter, - ]) + const numericFilters = [ + filter === 'open' ? `closeTime > ${Date.now()}` : '', + filter === 'closed' ? `closeTime <= ${Date.now()}` : '', + ].filter((f) => f) const indexName = `${indexPrefix}contracts-${sort}` + const index = useMemo(() => searchClient.initIndex(indexName), [indexName]) + + const [page, setPage] = useState(0) + const [numPages, setNumPages] = useState(1) + const [hitsByPage, setHitsByPage] = useState<{ [page: string]: Contract[] }>( + {} + ) + + useEffect(() => { + let wasMostRecentQuery = true + index + .search(query, { + facetFilters, + numericFilters, + page, + hitsPerPage: 20, + }) + .then((results) => { + if (!wasMostRecentQuery) return + + if (page === 0) { + setHitsByPage({ + [0]: results.hits as any as Contract[], + }) + } else { + setHitsByPage((hitsByPage) => ({ + ...hitsByPage, + [page]: results.hits, + })) + } + setNumPages(results.nbPages) + }) + return () => { + wasMostRecentQuery = false + } + // Note numeric filters are unique based on current time, so can't compare + // them by value. + }, [query, page, index, JSON.stringify(facetFilters), filter]) + + const loadMore = () => { + if (page >= numPages - 1) return + + const haveLoadedCurrentPage = hitsByPage[page] + if (haveLoadedCurrentPage) setPage(page + 1) + } + + const hits = range(0, page + 1) + .map((p) => hitsByPage[p] ?? []) + .flat() + + const contracts = hits.filter( + (c) => !additionalFilter?.excludeContractIds?.includes(c.id) + ) + + const showTime = + sort === 'close-date' || sort === 'resolve-date' ? sort : undefined + + const updateQuery = (newQuery: string) => { + setQuery(newQuery) + setPage(0) + } + + const selectFilter = (newFilter: filter) => { + if (newFilter === filter) return + setFilter(newFilter) + setPage(0) + trackCallback('select search filter', { filter: newFilter }) + } + + const selectSort = (newSort: Sort) => { + if (newSort === sort) return + + setPage(0) + setSort(newSort) + track('select sort', { sort: newSort }) + } if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { return ( @@ -190,23 +244,19 @@ export function ContractSearch(props: { } return ( - <InstantSearch searchClient={searchClient} indexName={indexName}> + <Col> <Row className="gap-1 sm:gap-2"> - <SearchBox - className="flex-1" - placeholder={showPlaceHolder ? `Search ${filter} contracts` : ''} - classNames={{ - form: 'before:top-6', - input: '!pl-10 !input !input-bordered shadow-none w-[100px]', - resetIcon: 'mt-2 hidden sm:flex', - }} + <input + type="text" + value={query} + onChange={(e) => updateQuery(e.target.value)} + placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} + className="input input-bordered w-full" /> - {/*// TODO track WHICH filter users are using*/} <select - className="!select !select-bordered" + className="select select-bordered" value={filter} - onChange={(e) => setFilter(e.target.value as filter)} - onBlur={trackCallback('select search filter', { filter })} + onChange={(e) => selectFilter(e.target.value as filter)} > <option value="open">Open</option> <option value="closed">Closed</option> @@ -214,20 +264,18 @@ export function ContractSearch(props: { <option value="all">All</option> </select> {!hideOrderSelector && ( - <SortBy - items={sortIndexes} - classNames={{ - select: '!select !select-bordered', - }} - onBlur={trackCallback('select search sort', { sort })} - /> + <select + className="select select-bordered" + value={sort} + onChange={(e) => selectSort(e.target.value as Sort)} + > + {sortOptions.map((option) => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </select> )} - <Configure - facetFilters={filters} - numericFilters={numericFilters} - // Page resets on filters change. - page={0} - /> </Row> <Spacer h={3} /> @@ -237,14 +285,14 @@ export function ContractSearch(props: { <PillButton key={'all'} selected={pillFilter === undefined} - onSelect={selectFilter(undefined)} + onSelect={selectPill(undefined)} > All </PillButton> <PillButton key={'personal'} selected={pillFilter === 'personal'} - onSelect={selectFilter('personal')} + onSelect={selectPill('personal')} > {user ? 'For you' : 'Featured'} </PillButton> @@ -253,7 +301,7 @@ export function ContractSearch(props: { <PillButton key={'your-bets'} selected={pillFilter === 'your-bets'} - onSelect={selectFilter('your-bets')} + onSelect={selectPill('your-bets')} > Your bets </PillButton> @@ -264,7 +312,7 @@ export function ContractSearch(props: { <PillButton key={slug} selected={pillFilter === slug} - onSelect={selectFilter(slug)} + onSelect={selectPill(slug)} > {name} </PillButton> @@ -280,103 +328,17 @@ export function ContractSearch(props: { memberGroupSlugs.length === 0 ? ( <>You're not following anyone, nor in any of your own groups yet.</> ) : ( - <ContractSearchInner - querySortOptions={querySortOptions} + <ContractsGrid + contracts={contracts} + loadMore={loadMore} + hasMore={true} + showTime={showTime} onContractClick={onContractClick} overrideGridClassName={overrideGridClassName} - excludeContractIds={additionalFilter?.excludeContractIds} highlightOptions={highlightOptions} cardHideOptions={cardHideOptions} /> )} - </InstantSearch> - ) -} - -export function ContractSearchInner(props: { - querySortOptions?: { - defaultSort: Sort - shouldLoadFromStorage?: boolean - } - onContractClick?: (contract: Contract) => void - overrideGridClassName?: string - hideQuickBet?: boolean - excludeContractIds?: string[] - highlightOptions?: ContractHighlightOptions - cardHideOptions?: { - hideQuickBet?: boolean - hideGroupLink?: boolean - } -}) { - const { - querySortOptions, - onContractClick, - overrideGridClassName, - cardHideOptions, - excludeContractIds, - highlightOptions, - } = props - const { initialQuery } = useInitialQueryAndSort(querySortOptions) - - const { query, setQuery, setSort } = useUpdateQueryAndSort({ - shouldLoadFromStorage: true, - }) - - useEffect(() => { - setQuery(initialQuery) - }, [initialQuery]) - - const { currentRefinement: index } = useSortBy({ - items: [], - }) - - useEffect(() => { - setQuery(query) - }, [query]) - - const isFirstRender = useRef(true) - useEffect(() => { - if (isFirstRender.current) { - isFirstRender.current = false - return - } - - const sort = index.split('contracts-')[1] as Sort - if (sort) { - setSort(sort) - } - }, [index]) - - const [isInitialLoad, setIsInitialLoad] = useState(true) - useEffect(() => { - const id = setTimeout(() => setIsInitialLoad(false), 1000) - return () => clearTimeout(id) - }, []) - - const { showMore, hits, isLastPage } = useInfiniteHits() - let contracts = hits as any as Contract[] - - if (isInitialLoad && contracts.length === 0) return <></> - - const showTime = index.endsWith('close-date') - ? 'close-date' - : index.endsWith('resolve-date') - ? 'resolve-date' - : undefined - - if (excludeContractIds) - contracts = contracts.filter((c) => !excludeContractIds.includes(c.id)) - - return ( - <ContractsGrid - contracts={contracts} - loadMore={showMore} - hasMore={!isLastPage} - showTime={showTime} - onContractClick={onContractClick} - overrideGridClassName={overrideGridClassName} - highlightOptions={highlightOptions} - cardHideOptions={cardHideOptions} - /> + </Col> ) } diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index 9023dc1a..fb5bf29b 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -1,8 +1,6 @@ import { defaults, debounce } from 'lodash' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' -import { useSearchBox } from 'react-instantsearch-hooks-web' -import { track } from 'web/lib/service/analytics' import { DEFAULT_SORT } from 'web/components/contract-search' const MARKETS_SORT = 'markets_sort' @@ -74,51 +72,69 @@ export function useInitialQueryAndSort(options?: { } } -export function useUpdateQueryAndSort(props: { - shouldLoadFromStorage: boolean +export function useQueryAndSortParams(options?: { + defaultSort?: Sort + shouldLoadFromStorage?: boolean }) { - const { shouldLoadFromStorage } = props + const { defaultSort = DEFAULT_SORT, shouldLoadFromStorage = true } = + options ?? {} const router = useRouter() + const { s: sort, q: query } = router.query as { + q?: string + s?: Sort + } + const setSort = (sort: Sort | undefined) => { - if (sort !== router.query.s) { - router.query.s = sort - router.replace({ query: { ...router.query, s: sort } }, undefined, { - shallow: true, - }) - if (shouldLoadFromStorage) { - localStorage.setItem(MARKETS_SORT, sort || '') - } + router.replace({ query: { ...router.query, s: sort } }, undefined, { + shallow: true, + }) + if (shouldLoadFromStorage) { + localStorage.setItem(MARKETS_SORT, sort || '') } } - const { query, refine } = useSearchBox() + const [queryState, setQueryState] = useState(query) + + useEffect(() => { + setQueryState(query) + }, [query]) // Debounce router query update. const pushQuery = useMemo( () => debounce((query: string | undefined) => { - if (query) { - router.query.q = query - } else { - delete router.query.q - } - router.replace({ query: router.query }, undefined, { + router.replace({ query: { ...router.query, q: query } }, undefined, { shallow: true, }) - track('search', { query }) - }, 500), + }, 100), [router] ) const setQuery = (query: string | undefined) => { - refine(query ?? '') + setQueryState(query) pushQuery(query) } + useEffect(() => { + // If there's no sort option, then set the one from localstorage + if (router.isReady && !sort && shouldLoadFromStorage) { + const localSort = localStorage.getItem(MARKETS_SORT) as Sort + if (localSort && localSort !== defaultSort) { + // Use replace to not break navigating back. + router.replace( + { query: { ...router.query, s: localSort } }, + undefined, + { shallow: true } + ) + } + } + }) + return { + sort: sort ?? defaultSort, + query: queryState ?? '', setSort, setQuery, - query, } } diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 61003895..ab915ae3 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -81,11 +81,18 @@ const useContractPage = () => { if (!username || !contractSlug) setContract(undefined) else { // Show contract if route is to a contract: '/[username]/[contractSlug]'. - getContractFromSlug(contractSlug).then(setContract) + getContractFromSlug(contractSlug).then((contract) => { + const path = location.pathname.split('/').slice(1) + const [_username, contractSlug] = path + // Make sure we're still on the same contract. + if (contract?.slug === contractSlug) setContract(contract) + }) } } } + addEventListener('popstate', updateContract) + const { pushState, replaceState } = window.history window.history.pushState = function () { @@ -101,6 +108,7 @@ const useContractPage = () => { } return () => { + removeEventListener('popstate', updateContract) window.history.pushState = pushState window.history.replaceState = replaceState } From d6cf4332da7a58dbd10c3d571711b2c779e27aa1 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 29 Jul 2022 17:37:34 -0700 Subject: [PATCH 377/519] Delete query param when empty --- web/hooks/use-sort-and-query-params.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index fb5bf29b..ae226e87 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -104,7 +104,9 @@ export function useQueryAndSortParams(options?: { const pushQuery = useMemo( () => debounce((query: string | undefined) => { - router.replace({ query: { ...router.query, q: query } }, undefined, { + const queryObj = { ...router.query, q: query || undefined } + if (!query) delete queryObj.q + router.replace({ query: queryObj }, undefined, { shallow: true, }) }, 100), From 003301762c884f3e1abca501cfac49873f556ac5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 29 Jul 2022 17:37:53 -0700 Subject: [PATCH 378/519] Ignore filter on contract status when searching --- web/components/contract-search.tsx | 98 ++++++++++++++++-------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 8596aa2e..4202618f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -116,45 +116,49 @@ export function ContractSearch(props: { track('select search category', { category: pill ?? 'all' }) } - let facetFilters = [ - filter === 'open' ? 'isResolved:false' : '', - filter === 'closed' ? 'isResolved:false' : '', - filter === 'resolved' ? 'isResolved:true' : '', - additionalFilter?.creatorId - ? `creatorId:${additionalFilter.creatorId}` - : '', - additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', - additionalFilter?.groupSlug - ? `groupLinks.slug:${additionalFilter.groupSlug}` - : '', - pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' - ? `groupLinks.slug:${pillFilter}` - : '', - pillFilter === 'personal' - ? // Show contracts in groups that the user is a member of - memberGroupSlugs - .map((slug) => `groupLinks.slug:${slug}`) - // Show contracts created by users the user follows - .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) - // Show contracts bet on by users the user follows - .concat( - follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] - ) - : '', - // Subtract contracts you bet on from For you. - pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', - pillFilter === 'your-bets' && user - ? // Show contracts bet on by the user - `uniqueBettorIds:${user.id}` - : '', - ].filter((f) => f) + let facetFilters = query + ? [] + : [ + filter === 'open' ? 'isResolved:false' : '', + filter === 'closed' ? 'isResolved:false' : '', + filter === 'resolved' ? 'isResolved:true' : '', + additionalFilter?.creatorId + ? `creatorId:${additionalFilter.creatorId}` + : '', + additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', + additionalFilter?.groupSlug + ? `groupLinks.slug:${additionalFilter.groupSlug}` + : '', + pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' + ? `groupLinks.slug:${pillFilter}` + : '', + pillFilter === 'personal' + ? // Show contracts in groups that the user is a member of + memberGroupSlugs + .map((slug) => `groupLinks.slug:${slug}`) + // Show contracts created by users the user follows + .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) + // Show contracts bet on by users the user follows + .concat( + follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? [] + ) + : '', + // Subtract contracts you bet on from For you. + pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', + pillFilter === 'your-bets' && user + ? // Show contracts bet on by the user + `uniqueBettorIds:${user.id}` + : '', + ].filter((f) => f) // Hack to make Algolia work. facetFilters = ['', ...facetFilters] - const numericFilters = [ - filter === 'open' ? `closeTime > ${Date.now()}` : '', - filter === 'closed' ? `closeTime <= ${Date.now()}` : '', - ].filter((f) => f) + const numericFilters = query + ? [] + : [ + filter === 'open' ? `closeTime > ${Date.now()}` : '', + filter === 'closed' ? `closeTime <= ${Date.now()}` : '', + ].filter((f) => f) const indexName = `${indexPrefix}contracts-${sort}` const index = useMemo(() => searchClient.initIndex(indexName), [indexName]) @@ -253,16 +257,18 @@ export function ContractSearch(props: { placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} className="input input-bordered w-full" /> - <select - className="select select-bordered" - value={filter} - onChange={(e) => selectFilter(e.target.value as filter)} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> + {!query && ( + <select + className="select select-bordered" + value={filter} + onChange={(e) => selectFilter(e.target.value as filter)} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + )} {!hideOrderSelector && ( <select className="select select-bordered" From 87f6949d807aa206230d0287256a49a332bad835 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 29 Jul 2022 18:13:53 -0700 Subject: [PATCH 379/519] Use custom search index for search results. Hide sort options while there's a query. --- web/components/contract-search.tsx | 32 ++++++++++++++++--------- web/hooks/use-sort-and-query-params.tsx | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 4202618f..4581e1d8 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -28,6 +28,7 @@ const searchClient = algoliasearch( ) const indexPrefix = ENV === 'DEV' ? 'dev-' : '' +const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' const sortOptions = [ { label: 'Newest', value: 'newest' }, @@ -116,19 +117,22 @@ export function ContractSearch(props: { track('select search category', { category: pill ?? 'all' }) } + const additionalFilters = [ + additionalFilter?.creatorId + ? `creatorId:${additionalFilter.creatorId}` + : '', + additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', + additionalFilter?.groupSlug + ? `groupLinks.slug:${additionalFilter.groupSlug}` + : '', + ] let facetFilters = query - ? [] + ? additionalFilters : [ + ...additionalFilters, filter === 'open' ? 'isResolved:false' : '', filter === 'closed' ? 'isResolved:false' : '', filter === 'resolved' ? 'isResolved:true' : '', - additionalFilter?.creatorId - ? `creatorId:${additionalFilter.creatorId}` - : '', - additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '', - additionalFilter?.groupSlug - ? `groupLinks.slug:${additionalFilter.groupSlug}` - : '', pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' ? `groupLinks.slug:${pillFilter}` : '', @@ -162,6 +166,10 @@ export function ContractSearch(props: { const indexName = `${indexPrefix}contracts-${sort}` const index = useMemo(() => searchClient.initIndex(indexName), [indexName]) + const searchIndex = useMemo( + () => searchClient.initIndex(searchIndexName), + [searchIndexName] + ) const [page, setPage] = useState(0) const [numPages, setNumPages] = useState(1) @@ -171,7 +179,9 @@ export function ContractSearch(props: { useEffect(() => { let wasMostRecentQuery = true - index + const algoliaIndex = query ? searchIndex : index + + algoliaIndex .search(query, { facetFilters, numericFilters, @@ -198,7 +208,7 @@ export function ContractSearch(props: { } // Note numeric filters are unique based on current time, so can't compare // them by value. - }, [query, page, index, JSON.stringify(facetFilters), filter]) + }, [query, page, index, searchIndex, JSON.stringify(facetFilters), filter]) const loadMore = () => { if (page >= numPages - 1) return @@ -269,7 +279,7 @@ export function ContractSearch(props: { <option value="all">All</option> </select> )} - {!hideOrderSelector && ( + {!hideOrderSelector && !query && ( <select className="select select-bordered" value={sort} diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index ae226e87..ad009443 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -104,7 +104,7 @@ export function useQueryAndSortParams(options?: { const pushQuery = useMemo( () => debounce((query: string | undefined) => { - const queryObj = { ...router.query, q: query || undefined } + const queryObj = { ...router.query, q: query } if (!query) delete queryObj.q router.replace({ query: queryObj }, undefined, { shallow: true, From ae2e7dfe3078bdb81cabdd08f17b107d585010da Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Sat, 30 Jul 2022 00:50:03 -0700 Subject: [PATCH 380/519] noobify welcome demo (#699) * noobify welcome * also beginning to add greyscale color scheme --- common/user.ts | 1 + firestore.rules | 2 +- functions/src/create-user.ts | 1 + web/components/button.tsx | 6 +- web/components/buttons/pill-button.tsx | 4 +- web/components/onboarding/welcome.tsx | 173 +++++++++++++++++++++++++ web/components/title.tsx | 2 +- web/pages/_app.tsx | 3 +- web/pages/group/[...slugs]/index.tsx | 1 + web/public/welcome/charity.mp4 | Bin 0 -> 364471 bytes web/public/welcome/mana-example.mp4 | Bin 0 -> 1085007 bytes web/public/welcome/manipurple.png | Bin 0 -> 20971 bytes web/public/welcome/treasure.png | Bin 0 -> 42269 bytes web/tailwind.config.js | 9 ++ 14 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 web/components/onboarding/welcome.tsx create mode 100755 web/public/welcome/charity.mp4 create mode 100755 web/public/welcome/mana-example.mp4 create mode 100644 web/public/welcome/manipurple.png create mode 100644 web/public/welcome/treasure.png diff --git a/common/user.ts b/common/user.ts index 0dac5a19..78b76511 100644 --- a/common/user.ts +++ b/common/user.ts @@ -40,6 +40,7 @@ export type User = { referredByContractId?: string referredByGroupId?: string lastPingTime?: number + shouldShowWelcome?: boolean } export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000 diff --git a/firestore.rules b/firestore.rules index 0f28ca80..05721dcf 100644 --- a/firestore.rules +++ b/firestore.rules @@ -22,7 +22,7 @@ service cloud.firestore { allow read; allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome']); // User referral rules allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index ab7c8e9a..70e81055 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -77,6 +77,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => { creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, followerCountCached: 0, followedCategories: DEFAULT_CATEGORIES, + shouldShowWelcome: true, } await firestore.collection('users').doc(auth.uid).create(user) diff --git a/web/components/button.tsx b/web/components/button.tsx index 8877c023..7eeca3d2 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -39,8 +39,10 @@ export function Button(props: { color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500', color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500', color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600', - color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200', - color === 'gray-white' && 'bg-white text-gray-500 hover:bg-gray-200', + color === 'gray' && + 'bg-greyscale-1 text-greyscale-7 hover:bg-greyscale-2', + color === 'gray-white' && + 'text-greyscale-6 hover:bg-greyscale-2 bg-white', className )} disabled={disabled} diff --git a/web/components/buttons/pill-button.tsx b/web/components/buttons/pill-button.tsx index 5b4962b7..8e47c94e 100644 --- a/web/components/buttons/pill-button.tsx +++ b/web/components/buttons/pill-button.tsx @@ -15,8 +15,8 @@ export function PillButton(props: { className={clsx( 'cursor-pointer select-none whitespace-nowrap rounded-full', selected - ? ['text-white', color ?? 'bg-gray-700'] - : 'bg-gray-100 hover:bg-gray-200', + ? ['text-white', color ?? 'bg-greyscale-6'] + : 'bg-greyscale-2 hover:bg-greyscale-3', big ? 'px-8 py-2' : 'px-3 py-1.5 text-sm' )} onClick={onSelect} diff --git a/web/components/onboarding/welcome.tsx b/web/components/onboarding/welcome.tsx new file mode 100644 index 00000000..5a187a24 --- /dev/null +++ b/web/components/onboarding/welcome.tsx @@ -0,0 +1,173 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid' +import clsx from 'clsx' +import { useState } from 'react' +import { useUser } from 'web/hooks/use-user' +import { updateUser } from 'web/lib/firebase/users' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { Title } from '../title' + +export default function Welcome() { + const user = useUser() + const [open, setOpen] = useState(true) + const [page, setPage] = useState(0) + const TOTAL_PAGES = 4 + + function increasePage() { + if (page < TOTAL_PAGES - 1) { + setPage(page + 1) + } + } + + function decreasePage() { + if (page > 0) { + setPage(page - 1) + } + } + + async function setUserHasSeenWelcome() { + if (user) { + await updateUser(user.id, { ['shouldShowWelcome']: false }) + } + } + + if (!user || !user.shouldShowWelcome) { + return <></> + } else + return ( + <Modal + open={open} + setOpen={(newOpen) => { + setUserHasSeenWelcome() + setOpen(newOpen) + }} + > + <Col className="h-[32rem] place-content-between rounded-md bg-white px-8 py-6 text-sm font-light md:h-[40rem] md:text-lg"> + {page === 0 && <Page0 />} + {page === 1 && <Page1 />} + {page === 2 && <Page2 />} + {page === 3 && <Page3 />} + <Col> + <Row className="place-content-between"> + <ChevronLeftIcon + className={clsx( + 'h-10 w-10 text-gray-400 hover:text-gray-500', + page === 0 ? 'disabled invisible' : '' + )} + onClick={decreasePage} + /> + <PageIndicator page={page} totalpages={TOTAL_PAGES} /> + <ChevronRightIcon + className={clsx( + 'h-10 w-10 text-indigo-500 hover:text-indigo-600', + page === TOTAL_PAGES - 1 ? 'disabled invisible' : '' + )} + onClick={increasePage} + /> + </Row> + <u + className="self-center text-xs text-gray-500" + onClick={() => { + setOpen(false) + setUserHasSeenWelcome() + }} + > + I got the gist, exit welcome + </u> + </Col> + </Col> + </Modal> + ) +} + +function PageIndicator(props: { page: number; totalpages: number }) { + const { page, totalpages } = props + return ( + <Row> + {[...Array(totalpages)].map((e, i) => ( + <div + className={clsx( + 'mx-1.5 my-auto h-1.5 w-1.5 rounded-full', + i === page ? 'bg-indigo-500' : 'bg-gray-300' + )} + /> + ))} + </Row> + ) +} + +function Page0() { + return ( + <> + <img + className="h-2/3 w-2/3 place-self-center object-contain" + src="/welcome/manipurple.png" + /> + <Title className="text-center" text="Welcome to Manifold Markets!" /> + <p> + Manifold Markets is a place where anyone can ask a question about the + future. + </p> + <div className="mt-4">For example,</div> + <div className="mt-2 font-normal text-indigo-700"> + “Will Michelle Obama be the next president of the United States?” + </div> + </> + ) +} + +function Page1() { + return ( + <> + <p> + Your question becomes a prediction market that people can bet{' '} + <span className="font-normal text-indigo-700">mana (M$)</span> on. + </p> + <div className="mt-8 font-semibold">The core idea</div> + <div className="mt-2"> + If people have to put their mana where their mouth is, you’ll get a + pretty accurate answer! + </div> + <video loop autoPlay className="my-4 h-full w-full"> + <source src="/welcome/mana-example.mp4" type="video/mp4" /> + Your browser does not support video + </video> + </> + ) +} + +function Page2() { + return ( + <> + <p> + <span className="mt-4 font-normal text-indigo-700">Mana (M$)</span> is + the play money you bet with. You can also turn it into a real donation + to charity, at a 100:1 ratio. + </p> + <div className="mt-8 font-semibold">Example</div> + <p className="mt-2"> + When you donate <span className="font-semibold">M$1000</span> to + Givewell, Manifold sends them{' '} + <span className="font-semibold">$10 USD</span>. + </p> + <video loop autoPlay className="my-4 h-full w-full"> + <source src="/welcome/charity.mp4" type="video/mp4" /> + Your browser does not support video + </video> + </> + ) +} + +function Page3() { + return ( + <> + <img className="mx-auto object-contain" src="/welcome/treasure.png" /> + <Title className="mx-auto" text="Let's start predicting!" /> + <p className="mb-8"> + As a thank you for signing up, we’ve sent you{' '} + <span className="font-normal text-indigo-700">M$1000 Mana</span>{' '} + </p> + </> + ) +} diff --git a/web/components/title.tsx b/web/components/title.tsx index e58aee39..e0a0be61 100644 --- a/web/components/title.tsx +++ b/web/components/title.tsx @@ -5,7 +5,7 @@ export function Title(props: { text: string; className?: string }) { return ( <h1 className={clsx( - 'my-4 inline-block text-2xl text-indigo-700 sm:my-6 sm:text-3xl', + 'my-4 inline-block text-2xl font-normal text-indigo-700 sm:my-6 sm:text-3xl', className )} > diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 52316eb0..14dd6cf0 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -6,6 +6,7 @@ import Script from 'next/script' import { usePreserveScroll } from 'web/hooks/use-preserve-scroll' import { QueryClient, QueryClientProvider } from 'react-query' import { AuthProvider } from 'web/components/auth-context' +import Welcome from 'web/components/onboarding/welcome' function firstLine(msg: string) { return msg.replace(/\r?\n.*/s, '') @@ -78,9 +79,9 @@ function MyApp({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1, maximum-scale=1" /> </Head> - <AuthProvider> <QueryClientProvider client={queryClient}> + <Welcome {...pageProps} /> <Component {...pageProps} /> </QueryClientProvider> </AuthProvider> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index dd712a36..5c52c7dc 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -242,6 +242,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) return ( diff --git a/web/public/welcome/charity.mp4 b/web/public/welcome/charity.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..e9ba5a8a3d4682cf78f78abdc8143b39799234dc GIT binary patch literal 364471 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 rDRkG;IRHB@Is64Z6(9etR zmn0oBxL-R^A#>{n#EN{trK2tN)rxV4Xc%q_?Y_&PH`3DuA0}tLrz0)Iv^^Vak7WdB zAS(!k8*_euZ7&)*Y6>0-rbzbS1|fX$smtLM)mBkNRh!M|fW=!$NggKFst+FgaC|)R z6yI86<`BG#YDgH9PzB8^2xr5F<e7@%%WrVII*=i)30JOk0ZTHRi<xPe;!bTc_lw|O z&k(Z*&-_-6fW59%mf1Z0<IHT(6VC7P*<6NkCeQLJ2#}+Xvg@>oLklxWP#JJy19V0u zUz&`&GsaZzrt}c<s)#Z2k{u-40TZs`-Rt#mCGX+I8t6BT>ek1wgm8(+ICj!e^`rhx z;QRWRbxtAL_*Xs?+!GrL2zv-;i$d6MSRrsMB69fSVK7yp@NRqIYOWG+8b){r_}x6p z!zG7z+#Ai^O*y~|zPXSAE|a025`}HGjm;+kdfa`XlX^D9@UU#JlvV%<Sq5x}euWvW zWXkd9OwL2lUL9lRF;D+N+^R-p+OaY4`;(a&b<U$E&Kkc|H3K5K$$s4KlZ~7y-<q7- zLR-~cio>WL#pYYXCL$tjO&E7vjuRV;{a@|-G(ERNO*myQ9v&M3fOGTP=B4=wPoC&( zO)m&#=qkE}k>Dr+*or1JqI?O<)O&N-A0YebU4$lc46<X8a^?j|3d-xi?iv;-I&V9| zxGWcOHqb!~^Q&LPD_^6ird*+lVXCsfuatl-nstF4xGl6Zhjky|mk}>7&I|G4BiVK7 zIaBM{;S}??Hr`(EM4#cF1<{A-q{{AI@k|q4Dv!lbhSvUoCVikQD!edRbGQbgBSgIV z3+(~u{s=zN(y{ejz2t1X)ov(Hc6bj$`OWGOgc>`A;4z#WWKC)mJFAC%D~w3}Vw}fg z+~y<#2bZ^zb0i65p=bXdr|}8KWWE#${5*zwpEGqBAgb<5G4z%^DVY+}El>*C6+>lD zMRSC`31B=1uaZ3Tdw_do@}|{pO)Z(tayv`>L3IiqcJ2a#NhBn`w80^^(v<)JWNf@F zPmiru25c6K6J$3!m|AIBUg!%e7d~0YBbuwxks;s0s(!7m**9Dzv}_%V8<-doK`o=v zaI634L`}L#5G3vw*A_ND(oofq`%EcOzUb0?kO`nS8B+$cr9_eJ&A2fJpd~SMsMOBU zM1pv&=19FA1WgWa#Jw0aw%xJwZCzJipMWjY{H>973+_T<OTj<Cgfe{4tv8}RoP_<? z&dh~VeyzJ4Z^H!bN`HMIbt);Una)8JNZyiMcG_gM6xhyii@!f<WB^G~_j!qqW)43a zb?@PPb__VRimh@!kP4X&m<R@n<B`NS#K!-VYPkSbs{IeCmP;<(k0%tp$6G>^;dc=v z5JK!&M6RAd_VI+cq9s(k{Vs(I+E{r?0cobYvf8qht&mQl3}#(0D+=Ab<<gW-E&s?} zv^hXLajkt^A@2DepBe*&{*7Ayi~T{aC69E@>Tq<DOQukysHaR-wR5a@Tvs`R5|tbd z07X83!ums8S%ugTptK~+j2~MnlIQ(Vra=LL2O4GWJa1AlK;o8$=HBC~ZsGw%uC8j8 z)h`IA*^(A9&cPGhLU7a;Nesn1&H-SeuRDqKyY`?k{Sin7@4^tl>>QnquNofLYU0Rb zS2UgCcay08TbbOfSGVM(&7iz*CP;?c5dz~G_|5e;<>;sCC!Uu&9Aj`<G$=KT?U{)E z&;{d#<G(<~<ReaGrj{(1<PwR2daZLcpqNmtvJH?Z5rgg{*ZBbQDHUKvhhx@p!yvwH z*v=T*P1cxhOs*JdxL5k=_B3x2yNn0iOrZu=>#0ME!7Xc(tXiAJs#f)8PifJww=4+R zg?ZI0@Pcd(!HHM+?E!`#({BZx>F*M_tf9qa<bRNe*PBZ`Cy3wP8YZ(&($jJYetNHR z2t2De$YS#X8?x^S9~3IIMBXk8{L~Yh0jw(dr(blnvZIxhbRUNPJJ5mmkQ!3M=sB!Q zXP}|PAz_XA`=t3F-!TiFNUk6kEWDPm`Ym?`G2PJwy14Vl(Px3h^V9Pp(o=?GLLvOi zGKI-e2k<%wGVQ0lkhf$E{mc9=QRHGHZRCJOQKPIDlc2{Kt`r!IxjJxoW6ncrrF$LB zL&cSKW=5^1D4ou#G|u$?(-tlxeFDlQqHOSK64n^TEZI;O&IN=EbilnErzcG04Xs-Y zIBmj`R1d9croh0<^m`tQeCO&1MDIB$X@?wn<L0Usy6G3>DoD19hTIwF6;>Js909>j z5XX)M4a)vse?&_MjL{N+pbrYXb*AGVH8c5L1-$w#AfHY&jCs4D-^C$`N3YjA#GgqH zKx!>~-ZYt3I>~PP5~c=K`p{NU^x$2CcsAV;7tINGh8g+BKL}Pm9f|#WfW@3LQN;Kq zzp@npv0KDev(wt9fXtVH1xOkmo;2OjDkAGRXWn(UvkKAJxxKl|t44*SVW0+bc)aR- z8HHcX<X94B3jF<A)z*rh8xcInFfSpzy+A!VcmYeU&dW>{=ycKV`*li}@3_%yCD>vZ z4V`Bwr94#?R0-l<`tP<2PudjX5O5`gl94pKI)!+*W+C@@d@9X8ZEeBl!yw19`BjJc zI08aqvUJOSO1HQ&m%AS|x<!!moNf;)@wN}|l+wgjkI_YmuiNHYELmlqlaZ`<@V(?k zFQD?c8a~$%lw6PLamgRV13pL)f|A5@-^Ou~;(jnszdf#aM$HvIfCkzF`A(3sbnd%L zmmkT|of#!DG=U!^sq^VoyPTfGEbCIRHPWWbz}e#`F#&GWBoLjIz>;j=(R|9>+dq8i zr`M;7%>8p3$47W+{=ei$?+zT~WzD3_)Kfn^n|h@T1TxxfcP|bamRql{&E9UqABL=^ zm?{Ca1}6Yslh+z>Gn)Y$TiK8^Oqc8)*O7U^h0D=2;s5C}*t113#t5aM)g0F99DIQ@ zsa+M;oX`Xxd<2)CNSPYrh9|+0CrqKwZF6R?o$!w7L}ltQ179#urALsN!PHcmNcJ3e zjv)AvMrhOYnRzIjBCpz#!(1mWJ;QKjD@;$<Wl|GC&WE5+ko_J<Ao{@CJaHuUc|pKE z@1JLuhN2awX!+Eh)ob4Du13QeCiG*`wI*E#wQhS3(Ci-h<(ZuJRj`^h%I|F}3UzBN zUE?j(a8?#IG2^L*1~rn+=%Keabo;mrZ(`i^3+U6ymKu1fRe|``8u8~p74Z?Bxh=4u znY8w^GYErdUFVk|irz01g-@BH%2({0S&sJYH9)_visl;Zta!G!$k9p}By`!1t3})? zgi;@BMeNkC+1IE6<*zTm{qnODPyIF^k*O39WAHdDZ07O!=_H=8>Se=LA~TNY%Qi?t zLKKv|IrR6%`NRRh6+ZG(Muk#09E_$ru>Vw*3OVsyI?tw{iDp0ayjFH&z`rg$S${}1 zr7tg_(T+VmUvK>R)j%F``*s1DeBV<(`1VVytP7zMJhLtf19bj~R|TYIE-n5CL0t(^ z-0uXjz4w>=X2su=s9&M^UfvuuN7<jvcQ&gUfM1{a7$|2K=ylfv0zo(C4qqL1t`Xeq zg;`iRE!_OY(k)U0`&F7ZJ#i8T;ppJCW-l(P%*f8F3Riu;_f1pr0i}IMlUym#nkBR0 zdaYX(Na-~t2DCpavzI>bBam~uA@TudL`py|mpwjjveJ%*Qz~7LbJ4^}J*EnjdGL#+ zH#XwJ6~Rr8zeIpQDKYqKc0LE4%DCx)J~b1P*3l1?k%IxD+2gSZaWa+QqHFc{pdXuZ z4cjJk`ob9XaNt5~mIiQLSN-&(rO6Cal{K`rPIZU1+Fo2`8cI|AN%E)FQ81kslcgik z=K-O7gbkg_<H=x?<I@%vWnrg-7%Pt9bTda-?NpqMG(aCMJn%1<g#)G#4-jRG6<_18 z*+nQh*sF_fL18C|^>hf3uk<2+hv>VWwaagT-3prcDG@b0WjBhRs5GWT{boaH#zmf@ z%`lHT7OeU4E5K+XTK5z#`4Mu{7wIF^ScbSo%21(a8O-#Z#jWv5i26<*WMZmDgj~|; z_(rm*hT+ndhht0H&nSw7RFTI$?<y5v3;_c_<=VK5(mtHOuze<7-Y^{`$<jO91n;)s zC$Gg|qQTS?hh+pE$)raZX3J`D@33u&iD_s0zSBl?Z6iH~IkI!<?5qJj)N`OVi4u7# zTxV{xHKn_q2Sq4H6KWqI4~u8xF^n%AqN}|=_h!+4=T>M8pbz*IGaB;(1=8I<=6-nm zY&sQSxbIfbpUUKxIY1N87br9--JJCHB3I6FIDYtsumbJ=KShDg<IVFDORnVcdmru* z7v?H4PlZH66nL42U!qsd#~=HLjO^8{N(lWcYrsHlLbRL9rXrXX4SCzeDlFt#$fq(e zFF#79vdCqjx8;QUdxUmHO-DONQHZKrrV%l4<qC~wuSGBW-#vKBpKd#Dvt!`eT|vk} zd^Dv&#bksB^Glm7KJ2Noe!<u)jYw23^!wZRR{APQ;Nuc@@IDD~{xpgJpZoCSi%-RW z|L}6ajT-7#ebT;Km%gKlG(A@zM~Ia4PtHlr&9A$P0VP!hIh@X{vqPUoaQ5mir8rEC z%vyv`5T!sCAl%2!(?tt0@Xb!B>nFptEsP|n%@h(YgEh6F=UkylyW%j@_}!Sy!bI^J zk~N5$2*KHS5jkzOy8)xCt%4oew+o-f1swL8mb#av(*J%vX7o9iNg^hmxXao<nDg;e z1(@ullYPA3zH#qWG~b{(^<+xAK-*5qD#He6w23l0zXKtP9UL};a+xgQ<&xI#kZ;g$ z&;Bv1!us$)E)cT3_tcK<*&I5^>813Nyw~MSnDI14n=7EfCChj$DgMVn=`qb7v`al> zY`;<;nU!4ni6gi<wH+R^{xv9T_Cw7OHlVbBSyUP}(S_Z_^BnSat=sodnsnAJewA7A zy3c+Mo(o#mT1H$);1{VZy1foeP}2wfB9d*5g$R{uf{?tNpV5eNb`VZi-~jqvXb{qj zm<UHjHDnnKU!-rb{dS2f2V6U?hDF)Pvtmjxb{8SC;;`(1B#`c#2+2}9mTlIez0rrU zQ0~Z=orN;!q$`pP977}crp1DKw*Bvc2e#3K^NJp{M1+=i9S-jR>kUOMHMR<C)vvJT zy*k9f4{f^0k9kAzmj0cY3t<qz0saiYKki^QjlEc#wIUNlhR6!k8-P18T&nA`hB*P0 zv)hc_ninylW*Dp{w^+o*J?(-kFJrx&As8rK7x!Q~A~xVc!lkj$iYooLVk5Eye%O%s z?QaHln}R!><+=XC4GgUu28*1#C$*+CW9UWT@XgwK%+g)IHq+l$Vkp}Lz%)+6NsBeT zY+G!$J1pq=`=nB&V8Y5R^Nlg?E<kRppmlw+vkk%N1zvAncN~yYhS{04+WT{smR{wV zD{v0CAl16N#4=^#@8Lvf+VGM~h2<;=7nDpfeVTjzX%qY^1>F~DH{5l$C)lHq9g*hP z;JM^QHbU%%?*REh!L?Ao@7(CMmr3rmr9Nk4^y)mq1e<Zjg!+PZ41jSz#spn<SEg_N zvUe=+?3dETCQ13_?F~Qx0WnV}_c4nnGDsMi1V!3B3aw58bO)yPV~x9`qSXoq`O<J7 z9<iM`#{*=4JD}9!H!vOdtgk&2C+-FOEH0MHSQWVSOW@5a4e@xab-SFP<ExtWNKt>b zWnVUrbrsI9C$4*u=*ODDocD>us_q4$FHXdiy~dmOT9mUGERoA2*cEodlO(K};fcU| z(GglzT4?nRu0KD3ZcI)F>qlUd_w?wI-<7vcOy5)1(t|-t<@>pR-i%?;A9^eF4H<yd z6-vIw{M2<C89ae&L_q$dhqf@~Ij-Qko?1(08-nyomr~a_h1^b<;w)hAi(SEmRl_O& z#6wB_!Q4u2w*oVR2mep%CJb1a@js+)eifDVcVxADiAYu<FrJVj)xxiR9Jr7&Vx$1x zOd37$06J0aFs3-rSH!W-bs@fd+*Zlbw*V1orc87byW+Ux1_<D&zq~zy4%gkDgDfsI zNgcP=O$<F=rC>8}CCGaNP1{}zr4g{}hDfRqMXS`_`HFQWlq@pP_KWAJK2I@!9IAnT zlJ4kUIFU#EoXh_61q<Obv*l#!@rIP^?(jR?MGdY91C5mxT=28A+TUqdO?zm)Mj-8@ zGLZy!F($vuXEX9$WWi0zP}{993)cA(%4}^wl-2O4k<9W#%4`#t4k6cKT05keRSj=1 z;(FLVr$3En1{!Ve71*?|#H+lEHrEKELT$XpK5JoTj4RGm;9AcmY`!d{f^m>|r(93G zlIdEhH-boOrVi%z$(DpWIDW&~SKgBiN;S=H;=CrIE6zCI8B<m6=OBj=*0CM}%KB6{ z#z6>fX@ne2`i}%&CU;0lv--^``t|n|8;<Hr;eaDBf4EIGJ>3G%TyL0_#LmoqB@%H` z0<A&r%OyBRUFddeGI?rvOIEk)AeQ-u1lZt0gpZ_0e5CawJ#NK_AkPrL#vy%m*72@! zx^=dn70_15pruSVY+}7J>?i^=ZcINVgl_cn2*a~6h32=%%#s{Fbr~LI5CF@u><8bD zi?xNcyrKX|1MdY#<gbWU#6k$woP2D6*JCPj^T;(T<ZuF@ph_?2b1Q2i&wBSmmb`W` zFumLZJ?Md@8Xc4$!th<uvC2|g{LwCSU!x#qnr#ELCyL1uvuLTRGf#uf_4&7B*LO{Q z^))|p{gHg~uM0WmzUHXN=>Ql_47UbGPT@=Q9ot8Kvk3?ErP)WR0h`tA&}D|*18TR# zYx+>N8dgf~Zz8EF5r*>Z(i~W9O8Bm7Osh6wE{-=(!{AJuLScx?A;sh<y*iPxj0S_` z=W@Z7PpFu=EW0*cjmC$tn#~g!&=ewP_|W0VOYfkFORhEFz2&@WxdoJ#HVL*FyJP^K z6!#^-`mY&{>Gdc0)p$=!B1*(Le`zzU5eT_7C7uT*enjQ>x2Y};+s%Y{&;5>k_w?|> zq}Z$Odsy1A+u7k^tMiG9!<n6XbJM`iX}F0E)NSw_ow%fQ`Z^{li~i#{upO0*2z9Wq z^5ldQZhm_7xgPc=nutNaFU@)Qu##K1Qmbr6ueQN^p*~KElWSdmRHr&G#+2o?P2fQ< zk6XIpP02Rb1W*HBMe8+ChWJCsf`*^4Dcb|XI@`vTz23%M3m5xsv*TvdZj}ALJt%?T zngWEuPinEy_q=|m_eYHye_;Ubw7iKyV<>GTSzEy;*~p`k8N{DN|JiY&hA^C8^+_fE zm*r8w9GP84KZ>RA@r;d2cjxWs$SD(RrNee|LU%9P=?ix2n~u+%l-+*J*2@w6bN>LM znefW~f6l}jjb?3WdFJti&XvF3P_f(7hQo}A$zb*TKoI;LD+8ALyE%V(Es0Qcv7SIV z@#$LAsN_-?HQugFiAugU@dJ3Y_=Cq4YLIQEwYbF<S-V+DZ(emuM5lb5Y)0n?P?SUM zlXdHX(RPgU@jXCFyPc#Qej<_uwT_F6qR@3{*`vg^2n2rlukckHrHh*rT>uuN+tinl z<YIUkB)JA=mMGB974>hA2>hv8d&?^rkXYBO`@sK0(?g_l9^Nw{<2Ga=1?)GOwDVQf zVb{H0@mm|7&dH73hPi*lj;EVl5l649e-#(dfemyyv;0iHByjv~)rF7J>V{4>bo_cl zH)BDUK~pCN!{E<LqT3h}{;%6#HX<o?iMN0*?Pf?p;!i8E&@f?^!f!lf-s43bmAfJu zdJ!E=-0)g1hSPEz;@EjgM>@f}<^WZ7Ej_&~w?mM{Cw+4tfTUpF7|T#?H{urvj9(_; zT!4u6C2TCs12!X&7}M8kN1N?Wyd-)1<VbUIAoJRF#J|uMi?3~8NaGYns`xME_V8Uh zR7~}Kgo0asz>{1??UZhghIzb=vUZl)Exwky`3e>AwS&aI;bp~v7hNHLM&~vRdS>XI zz)OXOzkzbwE~mtJ^4cu(r)u@aY=>QJzBFqIEiS*rvE@5O@EebaVK5ErSCMMqX_vgx zXd<J^n7Zr$?d|4Shi%+cU!%{Y1&kR3(2wapF4bVhEMyD%HG_5c#~+!t4aX5H7E$9u z4^*t1{G_zWa&fRp7IK#RDLYFP-_?Dk2^SO3_XS)_Ova1&e05aHPo8_0t1BfdC{p<t zb&lo}Qn$IMrbjD2Igy!^JC#c)5-<2+nCdlge``i$xnl(vjv?hVebsH2N676_QRc-w zQkEO5oRuo!5)_iIv6Cuwpy<ck$*Ut#mKBG)A}_y_6*O*)DZhdo%*VGF&FgCP2K#*J z+{X(bBAbz+xy>F>ODlK|*S3o=gV1Z3H<+OJ8vLuFhO6sL3R#H%2C%3Zo@s|dm5nea zh{zokPFI0<NaToMbEMpiA-#$<)RA?@J_0Fckm0F2)?Hf{-<D?;AHU)-{QzHB7=uGh zqXE;v$C^eNIG&9Ik(*rim+0u9QDo)z$JQy0%L)dSgUeocMg;&d;61vOP!9S8Ee4+@ zo(+3!6EeE9;$JB@7+y7r!Mx?q*MnSg?d67=fJsIU+EKm}{SlU;Hw!G(#Tgwwz)C}Q zy;wNUHv|OlLxKNX6ItC}<R5ojM7jGRy?4_))5#8#OkH&l`_n5&308`wNZf1b4>H8M znb})2mQ4nC+-)y1-Tn>bF(dq*S=YL=7hi;WARlomVKJ_m@~Ti^soouHE(G5jbYn5x zFJ@p;6MTe8Z6loVag}Td%A-f9%yx35ojOrb{KfssL86)6!%YBKd<zJ8kl#5_Hwp!% zLp&9JfIoC-QM%5$@U{~QmOchBys(e$or+8UV2}|xF<j9PN<h-e>}+k2p@t?=1V`J4 zA&SP^(G&x*BcVCb6#F37Cz}^JHmDrNrkkjm-by6aBSzl=h+uUc|6FbtR`>1<1EPeW z$-u7LLQ3p6@!hxk<&4Ia{d5oWSTK%^Fd-?3e&!Q&f$gpcSQA)P?z%Pd;6U|DOuHv1 z!Yx(8fx?As@a1xtVi?Eq3irMo$&P+)n#8d29oMQk83<6bv!sBy;QrTC4CEFE<L7c| z1VhM5W%2W$#Pq|Qj^|JK9oCd4=+NT9o;W5)b@890)465hjPX|6R#Vib)6O*=;6(4g zZOI&bYhM7xg829a*>@f7Tq$eYj*oO3r*3JoXc|#mHy6M}()L1N)}pa0rK)oCFx|%E zsiK-!=8YYLBRX|@?)RFv1hfJ>);<;7u)3)=-i@Uu++_tPOL)UO$#vrBX!Y?Ql_3%p z;1~sdn`_UB=*W!*xu1{?Py@Tq8>ez?J?NB3vW(A(8*%E1PP#F4D;106TRSv^yKl9U zk}`|RNIPUJLa6-~9e>K|dv7H?8KAfNSwgphH+ZGsj01#d0*L=YNoPAIq^XI!|Fg80 z>@^M@+*)Zn*LaG#4l^uNa}cwt)EdC!I>=~hj35tiivlXujeQ3#tmGD#p1XkHoSt)> z!lXafL>sVm2N6?}c8Ni=pnK&yIQ3;+@}>!*&<O#bdK+KmK6wL5=}sYU-z?^Jn}5y+ zfsC1H9M=PWe_n61--CqH#RoeKN|2=}Bm-GKoDu!xJ*bO6I7mv;m`XR|$162u!LPWW zkm^MJLN%XsYb@8fvf!3VHJ@Lk<l{to9YlD=LKwMPu))k-qMounFrv^W_Z`2eFYKtg zC67_BE-Oe1Qh8)ABWote>vBe?F-D`VStFSpf+&F4xzZI%XOU5t1dSe!6KDqCeiMy} z*SE>$lM{_!;XAxG#@N-+-bYNUKvbLf^2eb2@~88S^<BZ=!a@AbFOn+w$&ax3aGc1s zLBr|~?YG$#CFw!)5>o`VQx8@S&0R{R3w@~hrhWM4K#!BHy9OLCS(NaHB-<doHXF#W zuO@N&Qk)GPcV(}A$sjCoK^kr7!#15wOlZ+<J7x+I<kQcc_GJ}hcJ+Q*OF&kQsH zTb5B)&A{qP@l*a8t?&Q6T?m+ia&UF@Xz-H-nb!-J1!Vu%m0!Zvirf&G%GKx1kn<<W zHUl8O_QD|NZz5zY&-Vk|T6MD^H{*1Q&?wb({S-5Na(o2ElLL+-g^3m-<F;(j+y*%& z8Mz=r%OgN`5aQJ=?OFiT<R>Ne23v6_aMWVM(lU#R<=rY!ccTKSE?Fg>oZ}2^Dh8y~ zWWBn*L-kw<$YfmfXg}ngx!9l;D6N&XDNlh3PVo!ku+ct<?B9_}vYGORYgm4?_VqM= z#hQBl2GrX#qx$w9JU(^V%Z<%eG|A)r&i{V`wE2HefP!{kHfdvycO8s+1v^`uqElIU zn!+K@v_DeJ!1K{XonfT5!6>5ZmNMh%a=~LZ2Fh;)Gq5i1{dVyX*+zN*JciVVQLeTe z+G{4?r*P%b5V0x9EiW2cx9&rJPK;IW#{B*S&L4a#a6~2d^u6?}J^Oqo3LdK%m*<EY zR#D2y5*st+?_*o<XMKJyhfKFPk|1S5(-|L`|CM7rdmfkK$vVif=jp3`>vcW&P-`ap zMdOc4F~`pPsb#hJ#7?@bcgF!AwVx#AvYb;%H$I)|xMKGaTYcrRBEuKkD$G&>B&ZV- zEO=(18_hm+n=Y5m%dSXY0NHX@abDc!#IwnZ+S$4}&)q9U_4xu2EI_x<B@Y}1Yz9LR z#vV680_aVh{%F!s{)x8n6TvxZ;0h{E4XiFIP0)$1hwQm26s6o#<_Tuz+1!_wM|Mw+ zVz=+papa4_$9~UdahjDtWehi&Kv^@SZhuIAsfgQJCsO1w`@P7oo%N08$9#f+eCF&w zx7KT|9h(G_1Ga4yRLZZ4c!lBjEn<*Rk5%c6Wo(PeU5C8dvJxMO6z2lAouu7kr_=f> zV@)T(bEHfoh+t65)Hnpa0*|+2&g(=U0Q_U<*fa&>i^3L(7mn3Tcfo;nbs&)YfHN{U z_KAY-S?&WHoHXgHZexC1h|OKC_MBOdeTWFtzng=!lQWNDdEFl3N_rxzApG<A4UXnh zbv!r+)CbOgC&N6YEAoQ(IohdFq?L|I>O#|A^Ph)WxPc$5W}hFGV02d|>B<#(Dx^y1 z2I7B?AL;lrbG0}OC|aJhE9v(9U+irXA*L-Q;*{|Kurqf$2hqK<wMq2m7^T>4QVhmD zXthdf7zq}`_XUvD7@FcSHsmER-TWD@;V1oJLLT)xWlzp@5AlNg@mprD)cCGRCbTMj zo|SDjBEjKo80~=4!sDe7^cLQQ))-Mf9XB1z0v{m8m4xqt{F?)3IiD6kLxl}TEj?nP zvp!X>QO6lUwSan{xbZg{qeqUt2@Aobf5EwrRfn0_)tk_GRzeEd_}9Z=OSYUJd7|mk zp?LzZN#}45ogxS&GB~#VG&bBucBkvla@bpFR$a)cP(+O}_z^{+r#ts{16qV<=-&^Z zrACCV=gX~{c;73_a0<ME^Xun&+0AdudG=>K>8HE`K_)N-w(;a}mBhyzICCdqpP$Jm zU<v_utA+YUc?8S)1aFWI0!kBPx)P#*XoHI3HMG{3?)is-D<ymt8s*F5S*$z$2#4-N zHVt|V7ln3_(Jd+21I!eXWV7u_!MNwq87Wc?Zv%^EKAaa=6P(c^^8L}{wVyqy)RM_$ zRqF_2JK65klfRQVe6v-otFX^SwZ{b*Q$H&ppTh&J1w94wuZL5r{%N?`VR)8}?p7)R z_ii_yc(J_q&ruWs;kEn!k^sFsP-RK;VIJBnWln>$QL(@F=u{(t$}d4C&P?tuXX=OO z5&zQ2Xn$2_GBLc6n0<>ps>fU>#77k@zwiuW9&B~wR->THLai1`T{<N6*UOJ`Wk%$u z7+(*lpK^h3TaTl@K{9gb73a2q{j&L(3HITq(=w?7^k^heq?{N}<VWBQa?%cfV#g-^ z*+0(f%YMy_L&d(KUNxzC1$4ZY!!cL&?$LtN_VCry`RFA>V9_H|#xuWJD-??pR9S1p zKu9zcrz+n66ZOzT{-yU!jWG6oFFzQh7wf1tcYKj*8Jdm&O|o&#(!RVS;#KRPlkeMK z2qFtSN<keU#@Ce4Q#&SfY$lah1GmkvlydBPVUz=nCH=H?qj5!}S8t(BJNXzOm;FCa zUzPn+lU>&&Se_ISc%BsG_v(wrlwR%MI(yWVrph_mEty^G6$_dV$a4}n6hY?2YBNl? zw6kYUg;ocURUWk#9<QNvwjXyTbK4XH(=X$s2n0!N3Xb4r3LC@HP9p}a9D^^^KG>#- z*wov-xqaOo+yls`4!j<o=-GZqikN`3Ybtp>0ZYM{hhI+5c68NNzs<R^K>}0w`3ceW zdL-ydGugy-Use?CEOBVU%lQG=*(Mls&%1kz4hWKBzRV+5dcU<pw0*SlfF%2;!xA7q zQr3Aug(M|`qe3XTl=1=Nc=MT2g(P|=C5^ymMkF~ARZ%!;Dm-i;H~KHtx`DS5>j4Ir zlu_4tC?Y8{e=K*a5TsW?y*X*Qej0^kC;B{-cRTavhua3n|C;mt5?oxJ-mD^kE88AS zF5v71_K#GbbYa{IKuQ!(J(CUgf^aeFvdRE$fGpZIbbRmjJzoSFWj+S2Yh3<BC_Y$! z0~SHcaeaaMv8A+_cG7%?p|L!A!ZE%*8P{A~dHV4z3?u7m`~h3M_S$({N3_KuY=5Rr z*L^2v>oYy*qN1ylGqm}h_6iRw6jry~4l=h`(Ve(%u~J84w;?b^Os^>zm-p-#g3aj8 zBh4p&u_5uOh@RNM=RkKOTSF1xe)Z4S9T}ZcF*@tTLsNJsH4%GulUr&{2g$<Mvd3(Q zG!Nr4=lTr*^pyVz;tZsL+PSy2EQ9`bHE5=5S5$^KL)wo)mM{!e$J5!556EeF-{^{8 zhS<)=NH`WuxN1<RJxJynRqm;+N9~>7k6CKSJ+q88`XVCN`6qhTCSJb9WZD9?T%*A_ zo{{PpoyHT##ds^s@<)#4Z~pe%l*{F)qn2Ed2@=eLBwB<S(h{+l<)K7ov!Y*{_dxhO zpELQNiXx*`e9Q}YQHBDo(pyV7h7$7K45N{<K<;fVK#(7UT0U`!V-3{vw*GO!S_V02 z{?k=X+VfSb^-WJ*qYHfn+kfO5MJA_elhm!N`v<qSNgbe${PS`14Y&Iv`$euOOym^B zqk>=0#=d+WjjQ$K3%&B5WU8TH$U+a=_9lJT0RN~-7pWOI-gi)bxTO}Ln~77myK_n! zGM4eV$(ZBtsOw|a4CLBXos0qd!6k%4Urjdw`70KC-fdSoPz-vVv6LVg1oPrV&`2H% zEMrp5-6U6Y2!_%IK9px;(g*Q3_)tw0u9?5EjK5yk6iY|>3Z~=sFmYT(q^#RvB42fw zX$Ac9dO>n+NaSf*S4ugDBA^>4H7IPazzE>Qbg_OH>L1Wl3f}`8MLHOU?7XQ$H~H~J zrsHw=1D$o*;e^BROZ^;t1`*Eyu|-b(-Y9g?`{Vjtmw=l>QOgkW#M6%)`OoHH#pdsb zotYs_M9g^NQ)8zZg(e5!!jlO)%?tjuI$<FtU6qGy&Jne#!=88Qs<=g#SZS<;kV+z9 zT#m7bX22iQV87_^R&a9vANKAlD35ks7d7tgZV4{I-Q9x+cPB`&V8PvjySux)ySqCC zch{A*)@JM)|ClvL{!z2`$^JOG>8`gADEjKB?R|O;ibPcC5#?fvGK>_c4c;07k-yya zf(vX-Uz#i_8(fMAkuH(X%pcp|(TUkE1h{KAh?E~GGBgS$aLa`RUEQHWWu<FMnl#SQ z@0z($`m6|ci+|?f$9+dt0P@0Y#<PyccDAZWU$(J&uWrpTyR};*Ok3dKlRIPm1jkQG z37_pniuA$<ho8cP1SAg0&(2Lo4fsH+==M)^sSu~yRU0@~D~&~p#y^~mtV9glO?8B6 zCE2Dw&N5e3<pmJyn3cc58>W;@Fp8#qv)$go)U581xVDa#RP)hLx3)J9wld~;MTs+5 zd^O<hg7Zq`8%jOiexUvmZues3YgLpD^Nh{=Oz=g9I&+!FjbZr1{4=_83E}3qRA_5; zhcZQt!Z|7Ib8BZ-F%;rdMBy-@oVST&D;j<~HKo1Hqf|$^PlL{6Smlq2!PFAj)MN#$ z=+tDLHyjpFq>N#oF3p_CX^nY4lEq<kZgB}cYqiM-$%Aw}bl(}Sg0<f(k{x=GRg|XS zTfI=CfN4lReET)*dWAdJ0ppcIYyt&5d_+B@kMT9CUdXG)nsyhW?(mVHjn_%8$xySM zRTV+j{NX5lpvWW|v{-X#TLqKUvfoKxn^7Z@sy&lz@Cl-TeAV0??Xg5_FO6+Bu&S9$ zP*sUA=Qbf?i4B{OTs1-;A>~3rtfqn#YO}5D(Jqo99F?;i?h>R$kAl}=i^Qxs$xX#& zMD0T%om>su{Q4k5D9O(O*9dII$N3O_{D=bOvr1z=HUeujt5qaO(y^_VTG7Xy+zNxG z?1($qSyGf_s*e~U#K{<Xm!9D8v%$_ZrkA~Pi;5_&;_gaH%;K`b8W21bQzRky+^xDa ztF)@mUIn4%h)gZ2;`2rk!tK6q2-UkG%mer+_RJyNM5jbhLo6L|KiXms>_`6h^%n6g zodk@-qu&CV7djInfs6lC614GXMOkaikB{m)KcvIsn_pG7;Vz*#N8u7{Q8z}K4?I3E ze*^MV>T_I4wbS(z4SV|1z)SsOM^l&n&Y;lJbUKY=QI4N9wyUYku5L6|6YkrIS`Io1 zP=Yxr9zQxG4qrg7<rLc3XO?ZL@qOFn%j>M&571@UF`0e~mLh)R0XCnL=B8f+x^8fN zp2$B{BcLCTfUcqY9_mI-`H}@;RdOW#IvdTZlz^V}7Tx`RVMOVA1L~JpvK17Rw58;4 zO4b4RKGt7*+W<P)06N&bcdmKQKkZ-x&;-!*uIH~w_m&CszCZo{I{}*hs^m|X0k!E} z6+qMbF$B=8@K5#p%Qyfh0H=3G?+GYQ|H9-CR{%5tG`%Z&Pe5_{Lp=XF48RG%>7CJg z0*ceWI{C9T08Ic*?~2|Npy|)-{Oc%yCV-}QMehkX)%#Z`f4By~3Bc){(R%{EHur~k z{yGeR34rMx(R*rblc3f7$xjR52`<3XyPv-%;F;4uJZ*LTfhPc_zgmg|6sLDo08IbD zv<>nHm}+VOoB*8O_54c$IQ@gu*4CeJ0%!tg`WHp-D*&4Qp$Sl&{>8~3t^l0t0XV%g zde1+7<n((AJSA`m{(mzCem9K-5~Vv-vd7cg6KSLJZEcT{b`lGsaXHBQBf8YHnCWeg zlhynX_6@RA5bY|F8gXl~)g2RayFYjGh>Go0Ged`GA4E`P#(@McEQ$7kYE$D24V(GH z((xMe=V?0-M5e;$Knh#Nk~fP3@979wEXol%?!X#@jP};q4@n6XZ(NN0Lrp+eF^UEF zFqc6HcMOY^YV$;Dp&#k{&bhd<^+%d2Z-d~}j=vqz%D}1>m3RyLgC;rY=DTTWJ;*gz zIcUO#E(d*rPr<K+al%(m7O!M00_)YpI9{UknEpx$3(m>4(rH&>oXwDo<807O<!C0N z{v=ujRt*WB3tY&XvGOB?)#Gc@UJtNSwMLM~&EEL%&@tFus_lad?cCR!vx<JWmsK1} z$75;i6n0`B@7vRtb~E}|p^Q_i_?v{h;WoTGD-;Rx5+=w2FWJESEY%s#l6Gq}LSxoO zoZ77`tjN+}bmp6%^s%HrQeDHICXc`*?Zh}K4dk#KIZ{YbS_Ns`%JvhWwieu~#USqj z*T0P3eMt0KCekhPx1qS*kP3620^fS>oFz`by{vdeZZ38{yIWq79QsDSl@4k|t9U#p zaNDmS0kJjC2cNgfaZl@#+xAw4loYvFCZ<g7mm?Jiw6jx{uN7m9Ht&a}rWmd)w|%ej zQ!p%)rD))VD`YV0I@KDdTWD~woWh=WW%WaQkOs&emD;@bbjFjIdb*$hk4IuggO99U zi&gFd@}fz{LVc&;2_`X>`}E85ouOdv$3-)HuZgzYkkbZGj3!H?F-Q9B(n`ssS9(Ja z7VoG$#@@MRl%%t9J==!i?WdSljtcaL73ykr3G*x`o%l(%ym-X}Wz+Aow~`&1hzV$9 zKR@#`l|Nk*KDwSj4#X$n>L%G81&3&^c^2PzG-%(jE)-}lF4Mwr7J3Fex00t4@QoHz zcPBY(BA1W<d@2zJ0ww#jK;@WWBt_7*=F6i#B8;7?=(0;SJTelfi>TC-3)dn;8$Bi& zO3A3O3IYfDypQk&y^jrw$OO8=%QC`hi-38LL3j+rzpDnvogF%UbVYW<JMb$W>uS=) zy@IX<cfx}5GS64e2W0RRnto+@Lhw0gmD#&ULPM|%!S(H+j)yW*pd+#$@m|CDmrda4 zE_3}b?b5u%;enL4vwSR5njoce(cKx8<+EZx6j3N43^&%sQ`o;Qq11Ybj}ttP;huWK zch!eTWg{>lLmq>v_qt0(@gcf-VnB6Ks#!*L5>^jj{uB|SRIVGI(Q8h32T3x`qOb>s z6Uf0p`~3k6L1MIAA)NBx6BN1P;>oti)15}wgJMdEAwvRlRd_$C`P!^K3$jYz+DjdM zm2%Vc8bW*>6F*@Luj9V90RjTnCO-H4@8`Lf#euN~^3Zl08T*^(%%QGqLj~`3i}NN$ ze&Z7wP=owZ5Xwrg2R`9B^SbG1L6@zO2H$%>rnt3)@$s3NE2)jArDFv}(^FbnZd_r# z52WiD6y9@+V4@gOQa~F?6a8qTfPZ`Xq!VsE%~tqK3KA9k<@<M>fu&UQ4WgWc`z+Xk zsn3W>>>)5NaQV{}A~KOgyB(a@TnDimc?h!VAG~uHIeErYrUn(RtnChKa41xJl`_l_ zzWtaO!9RW5#W~_JA3>lo%|GGoacDw=B7_h+gP+(-E~fit(R?B71$Nbt{WQ6TGnK7~ z)cB2(igZ8958g;W6C@w9Z8VadvRz&z&i8jeG*I9s`p8~L6G*}phVAw^;dIM;+S%tq z9}G7g?i{g|4rCnyid4_++MGpi=i@L|S3!OlEk1Xfli!;HeFrYl33`JdAHA2VHsH5* zDQMMEQD(js=fKF<>WrDXl%Pin;GsZe!<l4Aj>sp@ae@<C_u^Sjkd6>a6K|%=j~)~~ zM+<>39!ns%p&X8^0r3xCQR@J6wP2-$yV`UXl=l*-sBhkzmK}ZuMjK$n$-?#xq{gyy z{N2RhC-Fs~*xMBm7*@R!IZIu6lXStnD;HZP{T{A}e1%ud7H7T5f`Lb{Pgfa)xle|R z<r1S=;K<VhmfA$Pn#Cs}I*7uxcHGrmHi40m?pDuFhqL(fEquU|a8gxfAFOt3jb23> zXwfc6e6%wq+IC;&kKDe;B?aoWhb;Jy?A!~2cPxKTd5uC8fe15XLY*$~)io=#Al^Pr ztC+7;5|~qt+qCu6IJ?Cs2n^LJD;)JZjL1P8uU_C5l1Gf;H=hNmgxp|&L~iAdO)|9U z-z6$j@#9yXp_=ZMAsOru-LecQJgKQJx-%5+Corhq5tO21)hvWf*UzC{gWs9DA+ZVa zg(_G@olNjrVOaXE!sta9TH|Z6sq_UbaTnhWEKw)by<G-f>#=X?D)C$18fr3BN=EUo z^7GH3eo=C#)exKCrS+_t?rZM98i?Fed%f7z>!?|aR5z{)3dqPz>#1D$9{X>=?|vfD z<=I9x8;ePV@?RAJnd}}iZVi9Q2UR=7M&T0|y~x!L;;mzQJw!Z1Jtd3|4f!C>;hgOm zn4a?3=U=G);;>B`BrfDR1=I2!_4(4qh3MFP79V|zGKAZ_u55+);~+iKTzqkOpP;XG zVyFxyTq7EIE_=;oR=5VT%&6&6Pvub+cbif|HT};=Ttpj5onKM~x_9H$qjz%<N#m1c zH(oB+auL87UKvcx9+k$C9%y_!TP<|spLbYBPuV;?IALcdD<HrL^~!jHyXBgc;LMxR z<;?2STWo(&H-d^*WGkX`WpZd{bIswzAIBu^)T(&jmyaNNfVMU4gm=Huq)pDzD|>sR zr;AxUEvkABk7&huwu_YD_V{?7g2JyBs0lAoXuxgA2fj6+iz9826Ddg}h%KxmS1g>u zoYKL0Hnw-@8xCz^mkpiVA4oOVd=JOyIU8(I5=j`M<v|(0&rWD$bGOj*$Ud{1VA5yc zz@?nC(qS=&h8T-=&W8W`WuY~G7utn;5?TGQPD%8~T6Ud|?y8MsshM_Izt_uH;bG8v zK;m_ja0xFDryN_AwhK3=Ebcyt-Q^2gvc4<>ebMNp3a<d66Zff>Ltnr6wT7Wm$d@&C zy1>ol6&<H0@QcmNZ>0^{TheMU1WJi!U6o(#3mQ4SKS->={4fR+i*TZAI`D_U)u4n} zA=-fu^<WmSFY~7j%gEb4jB;UH*TPGuf|&O~g9cJ3eB6?xpH>GBMl>aVP5U%v5ec>0 zH8kF8WWQR-S=_F%Ubg4Bz|$ltOxZfI>^4<X!))|&12I6R$oR`-vg}e)6^F!u@*KY> zW}ch|^IVHmjEy!W<vUdV20EKWK{#i%L?9_#3k$gpuyhDbiq;;dJw;n;6W@IUR<xtT zRuBGYt9I)eUO02Ouipfy2>Fjy3KZoy<?DmZp`nMT2w`)~BV1)?ep7$Mdd!u$;)E+B z#f-2rhdUut0)J#dQ=yYKyqo1@;Lkbz-s=rTuPh8p8ztkm3vlpf#@VJP=}a2uYv3hZ z^!P;2`}wiX6u7$nYxdtx_u#u$SAqSC;h&VTUF`O)4_w(9=k=$HR7LoCAdLnjFmf1= z#iF02n<lObnAwWPbw1cKu7y$wbq+IE{}?mXe$rSJ@C}KO=$4s9zSpqAZ6^O78fedG zY?^n6K!2+_=lb#M2VPSG#d8#3LAwF3kJb{Y3kru_R{>nS+UP$CTgsjKvEFp~3u!;p zxIHgmpG)1bD<@lh1%eUE)EkdY<5U}_iUNaB<bkr-gOvx{uYkVvzryCNo9@RWOBq*M zE9U}@sD+80gGFBA#c)LYSR?9jod#j!5oG*yi>%#1xS4m^u<%^QQ#kGiD(szxogXEn zHXLCbrxf5rlCap!|0pKi5HG5%vC+{1hMmLiH(QgVMfKQiw?h0QRovufLzh>SPP$G= zlFu=Po6m}&$n#fVG!|SSr<|Y)({I_hG_}+fWC}K|KRKs!t8Rm-*SoD{2lV#u5V~o% z_8?qrdwP>V3_nHlF0hsi7BJ(AefsL5mj%}ou=QEOGoaSnbibbB$3Wa-N6>evXcWv} zwuO-#p1_VkVn9kDLl8#1I7efhKbWgMi8fRO930hhr8<07fj~_q?Cuuoe7?fXT_*+s zRmz&^=1%9!bEmdfDG4fDL0C;@|Nc_++N|R;`=XKc=VqZ>^?9|P1ecp2Fp7|@7}wGv zTne{Od<Wk}EmHL~1R>a(f^D8VyQf<rPe1-hva8YM>y{0ZSt%$^Iw@|&7yYQ{D1Eji z5+W+NUg$AQ6;A|$>sHZk(sX3>%W2yPXx9BuEi5R1V{`1_k^LUCFKTCH#|Eefr3p%) z!rH#L)_}1;@dg67W=c(-uXZ46fkkg5cxEvT%y|K&(}bQAQyCf<y_{pHKq2#_L@H?n zDLgnbl`VQjeNv&8Zi6bdAn$9-k|a|ZZkC3gYG=<y-P<oGs3^JnK#921ZADaea)@gJ zjm&MN0}1KG!^ljcmo~xGw|jfldjy?J`NW|nd^b-&v=~qZ<`#=@Xzkt&3Oz2lKaF%f zS3XTXt!g2~JR>Ia5e2<u1HF;fEBKup;Tgsp7}aD%HRn`jP2mEIO2nPnWj#C4-uz%O z*5Ts&AyP1nmyiyein**vs^Qm@6wZkEpuL~|bTBbo6v3pwX$z9s@2Ks2hbkkt;%}kS z*GWI+wPb~bE*dVv-vySMAz8({RH6!NLR7`NUvVwlN_Iw{Yi+=6o#><v7vh_m2J$Hf z=4;J#1YKy@WeIKJH7OM;tYW_<R*%hxpqS~1VT#JrK&++hoe4Kt<Z<bt$Jwe+c=?<f z=i&Y53tG_AQt36KVEL&L>tLh{y083_P$wVLs`)0ZigeU9WXTa9DlLIegb5EaS1EhW zAPrbS`p1cYK21je%M+KdqDC#>?fKArUjh1}!QahPX34BPkM-P>Xj4Ur|IGcurOS)I z(HG9qs@Fz?qMk9<p#i@i5+2_Tf%A&4C(=!lkgq~{ZO-*tL~kLqp8vV3SRrb%J%Kr1 z*&_m`tonI2if1~nTE;!s;d@Z}NVoa66#1Rx#I2y3mDi(fEImBPo*ujaxO5DBgfXwD zUwmPj9V<Mns#UOcKPpX;`*Bl|f9gf9h7iX_=W-$U=tY(WW1*q@L$}-de6dp-%kP83 zX=ctX%1)=_c-Q%Tmp?Lb!2s&h6ocgo-!M6QzV=VP?48GeK8RnW-CbwAm@zT<1EG_Q zq?H=zP^>f+Unp~w-o(x>Sm}v=pKtR3mt+Q}gIA9rNrkCr$R$b56K*majPN<#gVyme z3~gsJqE0%PlmZ1Up(+W*QRnMhHykuC6X-e(*===DlN_S(^P2v2IFt~8#!OYCDzc3@ zHkQHMdOmQZkCDk@{(jcXRGrDzsw(hhhXpltd-7w&{byE~zH9F-LDl%ObR3sCxWJZQ z17U}G0w%ZzAwL_zM;~q;!p!?7JVGETzu)n4)QuYqm)(cWFPQR!(cWlPl}Z6cZ*QT- z%P45VMtF|Zp5;kNn3B+Sb<n6Y_OqdKQ#|aa|MGK^RekH}OCz1*)qWyW(zu=rk5!)n z)|V)xHzn46z^{NLO5Pn1IvJihYOm(k1-d_tuOEqRcy&^Cx~XDI{>;5$z?C-(H(ory zi%KI)l4N4qFk2tJ^0N*a|5Ynz#;c|yi>&lig#Y(BEj#gLgdpFeDsWXQ5_kcrJ~Mvf z7eufU9<MPWC6_dESC+KPF3F}>F75bCk5_BINoQiBv0dPS%)L#)*qyB|`5(Y?J^Vo@ z82uO?+VNu;R#(QCzBBoJzMCbN_NC7I-_d&E@ohhibv)>=A0-JCU~oxN2`wuG^Es{{ zayzmh9H<s#lc6p%^bx@iMpL<>?pqH~v%7G0+b3Vy@rL~5NJ@r;t36k&^!`pdp24%p zBN|72ZLDwDu0a&4B|d#?b3H!nI6#nYsCj**JVXl(BR3p)s4#J5hK*x-+B^>YRJ_Ri z@>s_7eDdWRMFo}euTf|nryF|;lTLajqO{EUE!Q_BP3|yhnTBS`qq*+NS@NU~)vf}P z5%_bT4{-5TY&r$X?PjrMDOo4uep^@9@;Rnd8@X9pUpPMGH?~L!O4V*0jkv17Tb)Tm z@<zEZm7>E0kTYsHzVti4d@iQUK@^*x(r<4l9R<ZD9Yb1t$n|%s`H_irj7(bz7377? z!0MCb2jVe5lCvAMe9y@VvFwUWf{yodbAiFPbT%>G0%FERNVqT8qdaUUI&`1{`pI~N zIqH|{ByQE@l>2Li7;or6ecn#=o7^IOuKE*hV0eFx{)h(jpeScQD#k20KF<%k8ETw5 zl6w((&xPv<6aT^Cm&{{ZRft6*p;%r<?x_33iCGh)N=!HQHkz#*MOWK}DZO>EpgfU1 zCU77p_Q+%@aSQTlTSj-kj7MvIrcspoiCd=Rgsh-?Z?d!xEwZ1DVCj-6qLzSI%&Ghn zt*)pSt3NSrnXc$Q{n~+Jx)AA-!3{{j?Sjs^oXASJeDXm>&Wiz+S<+HC2%~iC&;8MU zx!=@=37^;uV%RF^zi|vhpFI^O!s+md-MLdXLLMv3mop@+q`E!!BI<Y_GpRzkW>a_g z_s16SuQxMnp6#4|O1oT?Aw!}Omx@xt94{WRAhYpCwH!um9y~GhZhbcoVTc_d;B`#k z&fEQUa&iinL{L5VSg$w)0c@F#r1p%a++~s@K-Bxow~ONuz36l~4e>l=NTD>9z@Ns2 zkPE^`;rnAlFgOd;LsJxww^+HmG<F;E{+G>lC)>{4EN<yg_L8tyznYXY<vv_k*v2e| zRWftJ-_3*|`+;J%v0%2&y=7Q{W-`{pwGlC_D;?*Py1bQGep%&?K)c<WgUFFJT?}vV zz};mwyuNAJ=T6q<$|MMLtZ#DN_{p7`yHXRTf$P61Q^EBXkE7aOW3ti;9x`Rq<%o4^ z$4udNoxpB;VpbE@D0W|zpV~q<fi&f>f5}3b=oVBScfpqob+U}Q$GLq-m=Kb-P(=I) zw(Du5?7E}Fz-mc*8krnwi|4|Sp%9#7%AidLF<geOJ>cib7syNV)w-<K44Qe+UZ{}c zn}iFgHV0R_d>0maIYKv`vRY+a3ZM9p9ue<wuu6Ejq+-mp>-AO-9Lt--x6DP1U|Ts> zZ9jGwm=_fl8}FXB4;~7Z%*aVscan^(9wOzO7gh5wxocN2f|1@;siu<GBtm4xLbi@5 zBAHTJ8zA_mk94Ea&P|-~Fc<?EsTv%7SOn1S7C-4u_R5k)dldD*!zzRlfbtqW@ZYV^ zsZJUc2iHh*5=<GB&hd*(Z&dGvDHG<vVo}2kf4`!dPcg|OndN^=1hPimY8OV{Em4VV z+%~pu@u;dDZb{$ql}m>+_K1RIHkk;>E)p0&)%%VoZlF!fPbuxsKSxKGNtxVF5(lXm zFa{gJLlNN0ue~o19cBdTF@CNale?>SbYP0f1^K88_QAx&4Btb{|73B1;hW5%D_uC4 zP{-G~d=6p1kwCJ(ZT`$8I0MfHZ=vvmLtC!ivrp|FJp^H@cDqX&fu4qqZa1<w@W)Vf zlCnZx3vw|B4vBH8$?;XU2As4!ryUAAV1976W^gI#c4r91M$bJTXYn6$67^?VCd8Vn zB#5tHpC$9SF$F?)-DVS~L84KwsYWUvC?hzLH#Y=vnn3-N8yq%BuCbtye_de?EzC;I zFe}=>=IOn`sBbeg*H6HyM}{)RQ#;!2=@?N7JfbooK5&mQN?c2Nb_s#FL6c7M)$P(8 z+*y@!n)A2_E$;=Yaumb4tmucZyY2h5rK;W38zCZh?s *Is`0>Kj$w{i4%)<_t1B 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ΝSx^;H~g`+Gr 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 UrShIhASWOv03$%Y^8xPv0(mcnTL1t6 literal 0 HcmV?d00001 diff --git a/web/public/welcome/mana-example.mp4 b/web/public/welcome/mana-example.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..bb28a4bd0fde008c47ddafff4d1df859a3549345 GIT binary patch literal 1085007 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�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 ek!ypIE2e?t9=%9Q1%`0aN?jQQw7(Hq}X~c)m_L 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 z�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 y5De`A~nLuU}w$8*)}MP75O% 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 JG$|jP{sYdZ7+L@T literal 0 HcmV?d00001 diff --git a/web/public/welcome/manipurple.png b/web/public/welcome/manipurple.png new file mode 100644 index 0000000000000000000000000000000000000000..97d361b98f1a23b41a02c97aaf182f05877d5615 GIT binary patch literal 20971 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 literal 0 HcmV?d00001 diff --git a/web/public/welcome/treasure.png b/web/public/welcome/treasure.png new file mode 100644 index 0000000000000000000000000000000000000000..9c590d634125cbb1d4b559e15db908e82b80f256 GIT binary patch literal 42269 zcmZU3byQT}7cU^ffW!<TT|<X7NXG=--O?c-sesZsq@;AG(gF(7e8(9=S|k-18bMMz z-uQd#{q<P0?wUF5`0O})?|bfz*V9oYC8j6F!NDO_SA!Yg;NXLCa2|vM9$;7Ig2C8- z;9coy8NsnXot;s3cK<%MwrF#cL0emty*;X^2#x)+ICuKLb=3TiQ|wRd6(%!`y#@uM zSC&sL&CrgHsM+r)VPR+odn`N3*9YC&yy;|*UYI?_@-EJwVntO{py4vZZf+<ySG0#a zy1X1+UAelna9UD=PJKJSFn0=-9@bSGni@Zfjz&YJhOr;4il2+8;o+!nLkGsXLw<g! znwqhuCUj-Rimxv!IT?*5=;_+Y%^loWKZQvT=VUKog=j*Cv3du;{_*lcXJ(>bzeZt2 zA>z>W_2{Um;o#t*oSc91YioymebEI4Xe@hT!pPjr307Kt{a{AMa6IBiVc{?qAy!~U z#%NFw8Y|Y@8yy!r>gkEf%KFC@5rHx{8Vm_RS5^)p`}eS@u-3hNiN<;ci^ReVjlE)5 zFsrAQ=4dlxbo-~x&!0A;BBwt#ZT?>$VliMX^7ckyWhNw`Lqk!qv8b)B(~JysQWE<4 zb5vB+KV2_hqEb`QZ{DDSf=~egsFV~m)*((#|DuzfjRlXku}0<Rqp|MQRUb4pLKhaI zi;K|~=ICe7P{sym1MNX`6Le`Q+R75`>WVVb9mEC;8->V7l#2^0JsqtLA3`8dny?{N zg`uh{baOLWUvp4Hd8oD)t*$gwQ-jt~9a51S4h@~cI?%;s2y0YhBiif5H*BOj+qdgJ ztPP_N3i5vr^!=_VTZxVN9`I@+IcXM~53gR0)z@Lt)8?}>7F(J(iwl2!pFI9He6X?c zFDrg7oGvdPtI7|zw{4-n{^|a*laMfjM(wPxowk15%*dFnsQ5WPw)Z+{e0AkCGGf}t z=iBV;K~MMQr?#~ZHLJ@%Psc_Mb90vR^A~EWFwKn{6QhU7f!|GybDuvg^mHv&S4|=R zTOAqNcC-K9+B%V!JCcG}wl*3sESP-mIuaE%rm6fbAz{q$uiblW_P_Qw&@sdTDuo}j z_6Y+^fD^WdEBy!IN9X%`=f|nue|Gmul!Tjvfvcx`M|-SFXA>o72Y*(SR`rB`?_*w{ zV9rjSt+Sq=?c45qWBOxb!%uf7poe|g<9!w7A5+_H&U9xkma4sNdrBs}ZO>P2cPE;> zg(rGb_4dLiYC6u(I>$6maBxb^)M3g-feU*-o0A7{fcSly1%?lrAJjx7At-ooB3OT6 zSRO_66~K>Iwdgu)JMpl%|Np-WMF(5~I#H^=NcT?<CoCI33Wfg?l%`hl%J3jAX&bf1 zZAEb$Ym_{7^c4t4DNwHQmb4A8>*08=(FJift)$2K;kG7kIhx}5IWTKAMR@&Q8sd-m zrJ!4jmovXUGz74He<a>dKp*!{M!;BN2B>c-=V>;cE4?nIM~ROXxDh{8vPee-$M19S zm&biw|09M{1aTgA9{G#nU4-^a1n`HCqo(m0%v|L*vXs2?X}H<LI}GW6;|03QX*Wfn zB;va$F6IMe=#J38n95;PP<<TjTV=>&TjE;WC$QN=27dnRQd?Q`F7^S{*sjBBq6dCs zqQ1}mJS2$m=VK3NJ@Stv#gDH(*RVY}C#ell5;`)X!-Y{==`=;8EF-7ia5g<XYF!}o z=vh#4!Uu8`Vb@uYPHl)z<y+IX@RwRYf6}MJ#a|XHuzjG6+3BVLbbW5F#$Rd}|F;f{ zucq{{)y25Ak#B2zzfZ!!APt@Hz5jIH`b3iMsms-$;dVo0vOs;T|Fq&$TA99z45{IE zzjUeRj>FfY<}Nxhy?$q*c{z9~b)4?U+bDrci66hUK8&br;8bK5^36V&{lZb=0Ac%C zF^ph2`?J{=&ziJ$5eRhmC$iI*%Ta^Dk_qbxGoX3`xM|^l^s5va<@Y?hLm_7-<kDBi zLV#b7<v1@Qk%`Rs8CFL0nw}P=F88?AFAF{Me)@A?<NI7+GiF?8=Y}}Dh*b_%D2G^V zK4?+@&Cr<HRDwfWxcEv<^<k$LZy~$l=Fs@`h!!z-E`$q5#lD5<$IE_R9}iLWJBzBl zs9LxkT^3l931{!mydBf#H)smMnX-sFAs=o@gac+M%n7ElX?Lr@Q}Nzc3kN5kNLCJH zUS=lZf5p1bVZFvHz=?CgUBuoiic#WK&kITInFdZt**@$OKl&~VdQUE0ufWrb01}0V zZ8X)dYDM41ykN;}JaCnA^LUCw%Ux97DGJVzJiRXY)>X+y$BT<oSG<Dh6$R_lSF>u* z#M!dce->|DVs6%70^+bw$R4ftirVX1w2+@X`gzt`-R&3>2nBhh#vtcn`0>J*qwBY7 zCJeNmpRKN0RsU>>uCe?}08d{6=yH5CycT+d6B^(t?;;*R`#L12{+z&mHO^^Nk_XIw zm!BM58mx_TDEr*~3zGtnH;N~iW#LiFk31!RCsrFF1AmooZ!J@-y@zKKv0Cgbr_<CT z0Du0Yarb2H85=78MftW2(d;qLt0*tag)_I`)&?hnd9ze}^$Lk--<7Y1vEywpnIm@H za7zw#<SvU%ABQ{;sUc_iA|G+lfY;B}FwVZt^&hWXt%)uGCsfrV=3p4{EwEjwMxpDj zwPkDklJ0V?@+oo{vE{QD6{8p<C8RVT%{c?$D)KN@VN^?J5o;okqp#9s{RR45fGG7# zY3;SBXQnFg=+33-yCf1EW1r(aK9{Z`|IUCoTTxmenR(7#b@v#La@_cng5uroI~;<5 zJ-h8akxXYLmLCLVhNgsU7hgSVeL7auoXi1Qa;Ectt++78COE&585_e0B*()`>3aWe z1-;*nVXVIldYuz#Ip76Jg?{g{ScV|Bn4S5%Ku*v@@klq!doPF!QEBkYYhlBH>k_=u z3xYDIFHh}?E4DHI@K>*f_1*u;r?KY*wJlJ696BQstYV+b5JyPN?g)&&{~JXN8#n(w zOMbmpl0#)~fmG>Pc?=N*3m={C1$Lz)EJ%}W^8!E0H_D}!M<|%5??#2OJ~n1#4)i(H z^Y;lV)ntw}(+}i(6cJzTGV($Q)aHi=?(pKDY)q1Jo{*t8)h)dG&Poj^&K5CZyxFf9 z)Br#J==2#l?#PDK;4yzbr->ZNcEvwIiVc`v8&s6P$6GCIv+;N@g`ZnJ$Ok4nPWio8 zu4=3$C!c@beUX9qDGD4BzFKcjO<4uJDudAHIeW3b>+%0`GHs{lkGbc5O4o2J3d{+7 zE$R7Yl?yYZN%O<BcV4z-0`);SW%&yx;c6wJeYKrcP_GU2^iY}cg9A0($jZB)kmgi* zRn~_+<@*O}v2RMM-}Q|WT&ek1H8OOjoEF?vJuy?xUn<?|4YQvrvjiV%`uX|3IgsvH zjQ=$v-Am(HSlxGXGj@JooDm*hofEQ=M&=HvWeq-gvdTWHo&3oDC3FQ?N}-#<rqIe; zW(_X}_eAoqWPn@_KkAHWliq(z>x1>=EKZu1Gm&D+D4&M9n!>_5kE`2}(_5d<3Vld? zbVXLv2fkhScGzB8C*W_IKbf*!`|<)%VaQpO8K>}xzpS_S-ev=^8ec6oDuKKiwo-pa zE3_B~z)c9$Nfezyy{rdD#OlRY4*@0Nr;X+Iq^smp(tf$RP^o&hVKKkyM>w`e4?>9E zKwiL&7-n1lN6fvkVp^ZD{q(|0dVuCF`oUqiA~f|krIj;kRzn77kz6fEhcM(R(KllY z$M=#AZfs>$h&YLuD*20D75H9Rq>|AouhfzMrkkQUfq`ui|GTZK?2_TWh+O#D$Q+LF zJNEvLUA-q|SUbjEQAKOWjm9?dfX!*}%X$?0WOdQJ%6xObNR)>^WvzP<eb{~<-@gPt z*^oJXkWSA5rd3c&<WQlDL=_r=EX8A9UDSHOgI_eM!;5<;##gR~r{kZVJ(>1{J8=>j zqf}aQe>y&Io*a1x?Tn{2sCLy~uX`2CW`r>pLv~2Q{qyjf9ndM-d_NBFS*^pl*&EoH zhm_pNcqjefe6qRh2v^0!h>SMC)DIU4{Hga8<9Td0jywY#02sU&LaFOgR9dUyl>|xt zj@0AQMwlr|`AIWY!O7$tbKIVilv0fIJ=y9S)WfHa?zhf^1Y?F=#H?X~9ND8O&iq>m z^oF%$p}>(K@4Cklu4`Xx5K1z}7<XCz{KIlGwR6=#bIjUy`kmjUx`gSSW4IAZRrS z#}!mw?)9Kq#{r=i&n<xu1(`BLmh3H5mg$zpSD$2nwpdP<FYWkxzqGfv=W#1{(BhO) zyG@|lI)D-WACN7>xk94UM>)noM1q2Ki!^giOLqlb#Eo#9Mcw@*hK~ptgyY@%m@Ns_ zR{1{$zrr}16`ldUe+D;53o_@7jB(>CDGHlag}U0NA5^K~5a6uXJ`tTjNjMpf0{M=_ zNmgqB&k=>P#;3e;N9!%peZVvqBD`m)PlZCqi9`gr1o6{2(uRq|bM2e-5Y8fM=HPyF zP$$VNFyECw+VE&aPA9l!cZ@tIhHGw}G-5s8lmJIsBN5;!@`)`~Co7OXc7-pJ4FEgS z)e6K6Z4qPg586Y$6_O@eP|rd4m1<o+%tjM!YT-@*{{6>L4o+RUNg!E5Nu==hj(!TT zAn=&2$7hw$QKY}xO#D+ml9GE@uhc*JG49y&E&514%q#8Z7P_7#2>#qyCFV&gvH~WW z0gfZ}U%P}M;!rgxRG=|Sqq5C+03y9nZ}#%3QTzb^oM(uny$O_KLkFvhl*)(95{USq z3ec9j++z<lpWi0jrB7iSc9${AZP_S~LOz&{RKB=(US<1FMZnD@`ZfLigkiB)4WNoO zb>9-ZZH`|P6jL0H%o%KOWq&0%>{54tH{NvgYdL#+y#6H~h9jyh=j}5iuD`8gx3-}3 zaaIPVRv7qc>8y(ZzSl};e|`4k<CYROHx<GtdT&Soe;@qR3e9%$6gR~&$iN@UKX^Ub zcp%ipB&Nd3m$1F<FySBuGuXr-CtXzmktnm0kK7DN`?*^Ikxx4w5PpQxU9sPG_C9|L zA1To_P|P_D4O@dC5a1@CQCq`CZIHpi=0EqY5aXo<k1ytMua#Y-;m0}Wi<dNYWysfV zeGBFJewWL}EKR!0*btNR#$J##E%Q?CcH(3V8<Qq_`^8V52EUEHv5_<d<F(>CRi1^y zBFhd`6~CXn%>z4sMB?_aK|*FI8D`01OA=Bq@1xHZ4{8v8LF~v+FO5!zbmd}>Xo$u# zFK>h!tLwLtAa+m%8Ov+8U_>Csm+3=vm$f>>><>tE2qIPbsB1^U1>QAE86AOSDGqO3 z!B7T28+wXNBHQ~xHL7|(mFB<(Wn5vvOPLtZu>3X&Gy+j@7m@dDnt+Ue*Z^nnU9EE! z4FtYDJbHmR7U(P4IJDc*)uMi{&8jWeWJ#!j5Q=i3i&PhwcV_`}{L6+vRFj7P2_h<d z@#9=Dc$bCvNJoS;6KVg&?M>b);fYO4Qzo%3z;zNbWWLW#!sdes8gUn-?x&89NKnYn z2qdVnJz>Z^d4w|~+O|=@=(bQin6Hv(J_tPDi<4pNdGzZCy0kjG6c9u?{)5trFY*ma zSnuuC5p%_((H>E+2369%>QG^ai~b2B38b*kxD-zGeL+MV30b~bZ0ge^5BXK?*@Trx zc=`c5((%=5D)>xd6F^G0P0Py|5eRK8ttRE{UQBZI3GUt_dn>5_U(r`tQ(Q$)xVv^t zK#T1zBry4fueZd4X*b%D!p1^|zv*r!f4mi)WzqV<6C{Ex0yR;fgw}Sq*BBxtU~2jQ za<s}F?!OqN!-t@6RJ)QU#?EVqd06IY!4Ub0yXFXy`>ovvX0L4<lA|~>5beJtEALK} zh7INgma#4I+w)7{n*tGagpw~Xyzh$Da1z9e26Jz5aAK}<!O#!i7!2V=wsYNVu{f+Z z`GAFXXKJt%G`iwv-}Ulg8Q>3H6fvIuWc7x!O!MBOIsozC2P9h*RNYlk9&SO-&M$D4 z0-n`sRrje}^wg6x%v#wOT3c&DJ<`M)I`2t!v2Kc_|HmGWUW?>S!!ct{&8xk#6ZY%@ zC@7I|l@7Zugrt{3lYz@CWy$W&Y-Rrswj6KSDn*%!S0gr{nSlfiwp-evw9jfT`x$Mh z-c_k5fAi*(kjo?!heRLpi(?ITd9(;bWa^g59eq)SOBITw+En}|)7IbEX?MLlVsX); z{>@bq+Jww^Z^}126oW*c<Iy~U3h(ULAMkN%%dv#~EC(yRlyf*W>C-ib7mFWJjlEUA zQ8F(L?MrXFoSjCpD5l_KK*?qX@I9?gFgm}0Y-^^5Wo&YM7?JFMU0a={f^xJm;zo(L zLC5%R1XW#WqNGcyxYyiTIdHXLWckbi%4z~-RhP!5+-x`hfE|(Y!w9vz5Dsk(xe()~ zldo5Ic5;&lp9#Mne31%hOw(53X=?uySkqIGFIQ1L_oZI+G({wRS?BexiMBH3_`T1w zj&?i6_t}(I#M33@6f8HwIL?r3E#$*yRP44BIp}|yGHlLD0j4e_6$~A^J>*$0<)L!R zZx*;4UJMGMVhv*O<EWWV3lx=M{{fjoNaOz=#{zgM$KT~YJF5k3iKzro8^4tj|3!7G zD92aUW$@t&cSE#Q&-sx(5o;D17?B!K<+_$<nTw%=3?nH?S2aO@9!7X%DO@cvdJ!M| z7m6tgX?gUWoLc2?7QX#~-cD4{Lr?_#pa1%Xskjd_MUG(53j<`<9w$B;X!&`VIfR)8 z%ZSU!+Av9~sHbA*wj`%s7f`1>Ueztcs~rgla7&PEV4wmALZdU|w{C^54Uy7GphzJA z?9cW|Y@pu-F^<6-gys}9<K4zrFPXln(<PbdM|x}<A(3haGyN)R%XwwVzg2v=y-ax> zF!8a6?*X$eN%WsxDNc;Kq=N|%%qu9Dt1Xe5a`LsL)vSUxccx8yC(Cd3Y_tifBG@We zZ=2x#7=ME^w|smAGm7^k_ZRdgqv)G+oFV1>Ud3DB8Uvb=AlRtw?PEMwx`9$)KQq`- z3!7a0IpBh9pb-#(oZNyRIUkYLmA}onkC4x$k`k?g#!N{p+>?1)$x?yaJ2iHSP^wUT z!)O|1)~6%8h?X_&HEUtoi-;Tay7z<CZ`;B~9fp)~7buA*u*0`>6kJ80yQpEHKjXAQ z`i{v8Pto}(8k_+RwqN8qMR)iF;PV*_IbAETp9Q>LtKDWpp$1}9ZX#jpW*1C;nk<C- zE&Z@J19(8_D3JXF>KpfnVRlRpdKe3M-@wpjbF2=8ckOQ^DPNxIYeQZjp1y6gdvMhw z?k3oKBT4dNh(eSi-Mxq@L~wg&^MG$dQ?9XdJ;OHPkR1T~f+6yC?}`t`d90H6RHH&{ z9@l%+EG4yjwVfy>*_~!@?}Zdh?M(cxcm3y^Br8M4zN6pzc^{w0e>Q$w;sK>*D+7QU z17Th70u>_zqZJX$2P!{yVJ(gvm1abIN2*Z*8Cmo0%mKf|+yh*?P33=x8J8N>CL$DX zoC6993p)Zf3;bBb&d0V`;=^7{JNC$L-Te7x9(Y74$uYnItcqc!X<ObtQJ7J0RI1ln zy3<%UTr&rjaDt8!O)h4JIr&+b*F}sPfu2u~Dj;Usu)n4~xvwL(fB){RNxsXQM@}ny zMU?K1Q@ZAlW*j&vYjNot1N8E2cj8TjP&Wrra*%%8pDh}`qu@%<rQ_%g7+_z-j8&~O zS%KV?=`TUfq-Zb4#D~lx5_eWUde!uayvtV`Q(4yhsao3P$B5IXX0X)JxsX2wl~1hZ zUF1PG5v+a@te%oT+SOlvN9&me%JmRcQiW-64X{Er+4*69p@R1M(&Ubdti^woTUs>U zA(m71I2UZ6_&61l#slS|OCYUo?C)yFB2gdd<ckK`tI3bq8U8a*t>kp518QH2UZjt` z5LjDsOr7aRB_c@8Ud_ZO^jc52sDiJM=d@a~$qFn+OlRbW!?9vtn2U3Ac<i%+!D(vA zNgg-&F44+1f)Oe);Y?Jf1#>eIB%Y!dkMzS<)PJlNQ1{qqEKR!Tf-4><lY5n`SXIgF zHR8>2MTv>Nd(f}LXMHPpq49$-O`CuY;41(Q?bBc?>`BI_9w>q#m?2F{scVbQryWW! zEhL?vv5Oao2zGu|vz-SB3+{`}T}2G(E?OWW!1k6fOoc)|#ZmMH8$Lh2M45FDS&P=o zg;z-%eS&ZS^@wQKuU^L8gvNI}zGrK#>$_xiq7t-p>nB#J-&q3FpO}@GKkVs#-kBlz z@d+sXP14820KElV>;4TXn3lWkTdH>+cy<q&a)Td#2#IAXOfY<0xR@*v`W7ZdDMGbw z4wGx4BNebIyZEL~)BVB{T!$aY9GW;q_QIg>X*}XR-kd$7OL&PD<JOJEVr@;4(%!4h zwbqJ9?$p)nUGcPk$bAcpr;yWT)foOqUV%S*V1ta1CP?Zh+7Ln0!lx;m>ZBe(6rDK` zSQus(NdJ5%w;jq-o%Pdo<!wK}3k<mp#)iwaY*AOp@JUzuY!9r!9@|#7SI_x+0wQ;% zUb#O5Kad0#zJA_8Ws(p>`X8mvc*DrPe45$!`3QpJ)}a+`<l!#sI2e;#@A~0CMK5(d zwmq%ruS&rCw+aXP)=f+!7`rA9Jk3x7g<O#WFPNCZYPf#Eq}`gc122<4eQsV#CK1hm z97xfYsYkl4>*MSXm>CKF{P;on_`?qjckosr3I{Wat=%YQs7HG=tNIeYUFZFf^+HUn z!ki@SsxFd#3_@aSZJi*b$L67pAI4TBEQl{LuvI=zXk_Yvl0Hge64K?QKM*owp?|_w z5-x?Of3vK?{h;~W91C6-xxl(g8>rCoaIpkr1h}ST>y6F{`P8e5?cV-hX-zbep0f%K zi%cO>P|ZK8%`@^ifGs$$1V?DBJ*VQ(ig_tUu5y3YtzZ+#mWW#F@!vZy!Jy*^9>s;( z@^L?-fSVX&lql<y2u^+BQm0dV`oUg8E*|c$x%a{*<LpV~yyv0!N9fP+O*bn*Ps^~& zFs1IZV%1ap0yWq{@IVCXQ@*=4tCwFp7n@bIvg?r*muk?F3PWep*UxPsLgUIW9c2WP ziFB<Z2ox%mg74nII6!Hv=V1E>lAA}_HLvkLabp1PZ%8)rzBcOUSsy>8r)NlxZ1-79 zQ(>#Fb`*R#e%s5XjwyHnUzqypiXc9^wf!d{)C8fDh^Qa5i>dBy_L<rt;~21KrCm-@ z$bSDqS&T@YChK+3Zg$X1HFO$+Ad(tG$x0_{qi*#*9;HyBjq--^fPP1Hxnym`R~Nr! zuK|v6K<<EL>AI@LkBnxMN;_B-7Y>a#SyoI%rg)+=JoZ^B0fVDV9#l=~OD1cn)UNgi zZ>!?IQZy%o<%@(PXDyS(-UG+D%Jf;kWdqmMi+h-TN_X@0>l|)I8Y5u6w=&<tY&v*z zld4k(2_vb&q@46779O*VQQbcB0`CxQxOZ^m<MkG^qnen{4-2)TZ<wRx=Znk7<&9^9 z6JNUG=&5mgtH>4UJYH!#Al75QdMQ;<AsU>}b-cqPE(o)UO8=m^#+FdS$^Y%1wU?YS z4Mu{0K?4#$Iz1PSBwlp{uRrK#)=Jj9*<zfLUu;e>do2)ga#MyId_Hd~rf^NKF(8wW zAn~Qmi^?UKprz;YS%W@zUS1yaV=bg#1kQP1i!WwpoTlcVmj-s(@E_qHt!|%$&cJ!N zJ~a!6y~|b7e=jVqmomnYS?OH_x6e=d>iT?frO6fy!A*lB3D##;he}adw`835E-g?% zggn7X=7CBvcg>4Dn0|A!qbMb_HW%7}r!FT2^1!0&hLLm_BQ@Ru$xlil$R{a<!J>(q zsV{?$WQ$GDWU~NGp@4oo<r~Q>tEw+MF8;a-%$r}v{Y)NfQTSvHcsWIB$J&A;aU)tZ z@&b>K*LBuzLboW`m;omcU|6yQnW^<(nlGGiZea3;{hq48C`APE=i15dpX|Unh>{7g zJrnV^Nmo_%@(|2SG1@kQ_08o65Y2~%g_MSz_1y=qGW0h_p~U<|jQ0Vhxi_axfnbw| z^hL%dT=tI=^yI2sx?AF#)`0e7g`xD=!e;$fNG7?SMd~SC9gxICI_sq5Bg3c`JoYPP zvkYq!f)!5XQ;6Y3*9C}!J~2XsPxnS{rxAWe*`6w@)DL@P^I!&V*i3P2<v5-JJ`DkN zkxJo*<Z^Bp-pDUooN?B>HsJMhS^*$Z5j&$9W#68tGcy@zC96hQvN>4+2JN!q>Z!30 z7Sh#_ez^T`BtG%ccP~knS37(zlt-<j5v-wfA-}4>>^}+rHy0okE_wN1vC!mE;rZ7c zH&sGzKt$>s82BW_11@57b(;~B-QX()dOQCaZTrF`*e-YXQ$PvF^$Gq0SLN16R;~=; zC-}&wVK)s2)7%~s5O^TojD{8bKsPTi;b`K`z#}iqjlh51kp$ZydtpZr{~6DA6FFII zX5(9AO=Z}T!r*ICTZy}`ZMDIV)y74us-U9rfQN7z&~H6GlOtlj=#%)Smv8k$TZL4L zpT7SSrLlT+!If#CmDEK;KxaYEpQJ?1RO|H0h+c{&*)uV^f&xV1U1a<b=Ao>-lFNS1 zZz5Za;I4h3EI<%hfXVG$dzTwH7WnXklh|vyG*umzbydmO7FJD7RBDdRHN#i>)d!hZ zWXjOXCv4y}gfVV29G|x=XU$giD?@|Xz6y9x_Fr!NVhK^Gk?hyB1v89oe@}pwA&e7F z6%E<h85H~J>{7EeS$}k&Jx!Ldy(du3;-c9hiFKstCuS2qtcb5J5nI9eN?8MZ!7(5Q z6x9TvPOw4z&K3A;cZM#fnF>KLduN}VVMkNh@D-Jx1WZ#$Dk&Y-rE8UxwAgWfee~kl zz6O}A^#mAM>hdNSJ34sAepb#yrI0(isuRQha}R4qO*X|{D8oW!IC2p*4={^K^*nqU zKTpDoB>r$<GIt?n<gm-hU(6wbhWU7PHic<;M3asrK@GR{Sr37~oNq)J)3snctj&lE z+=qoE-@AXzwxH@^*i!03B;wBUwR4-ep(>ENVw73Mg8(}<NH^Zr`!Fi~EbkUv(~fn- z?)-Q-QikfKBo5D$gC>av&VgT0z?qqENt`M+e4|HrY7A&PRvsGFU+!VaXLLB7{8jJh zdwibcBh)51K$|hFmU@{46IblX{|UYn0=;5^7)ecZIwWsA_v?B%HQ=!`E|mAU{cKi= zs^r(CP4*-gR0gDc^e;#yQQ@su#5u&PHsC!5|Bo)B*rC0sL~>}6Ro8#_{e}T}_AW0Z zk8HyNd%M|CN!gr09eHx~<S3qd#HRMG_~Tz^Nr~~UK)`!W{I!|ME&1me!6e6AO^)n` zLrL-0*X~zt3(403Rolt#m#{i=M6q%Dfud5Vz?<*gCMq%BA|O?%X0$DHxA9Tg!{^g- zkET#fj_I}JueDAWo~>m7%6Oe+K7`SMIf@YyxbHntUT?#qAZIfr>jafC`fOI((HRk} zMT{W|C6c53CeMoUT)TQF957hKVR=2)?`dV5H<R6WV0vupk3oiStMvx|3a_=8^muJ0 zwEf{4h=(GExj$d^>ps$!)3hOQ(@#lL@fR0-x!;)PZui4jE-uS3jqOpq*)0EOMBt16 zKBp?-2K-moXn7HL+-SC#36pcarzqR&v!-s8R`tva`0S!&k;Vk1of3TwIbL|i?xJM= z*4_iY!R6pO2m!~|05K1AkMWh1UGqUP>F+{CFgI=5>d_9^UBQ5@eEC~mNjo32)N<d7 zw`$37hG9XhiSsNrugbA2$~$s`(!$rl_M`3=g0XlhG4FJd*6!B2(N?VOkCeM4ujG_H zxdiC`O#%7>mvkSfd|wYwwe`k+?4y|6wp<B|j;-T>d;~iF&Dj<&-=OrF4Zn`}#sJHP zgA%F*4D;P6TsTjyjCMEPKiiyPV^Xc1y`z_TOwU<HITFeOK8~l|JJ`>XuI0$r9~_!E zj&Fa_cMD_OTVjqn`mya7v0$_&#r;^<873pL_-<Y*I^#95XZzXb)!xrd?muD3syG2o zd+(6OJ+OVPEt{Eu<=GpnIHs9sSoldqN%eAVAUfu`VMyW(=Cvr}=KaU5Do4>G2w8r* z92UfDd_EqO%skhB2H-o#7+`y-`wxOlnGYOKt~|Z@U}6-jOKkXd<7X4UL2o{2w=roW z*LFDCz*gb(cIp4R{ZfL=L0fOq(1HDd$yPw)N$XG(c68{TN<_Tmv0oO*Y}<P@=VHK? zmj)<p3?6wH$+(j{xh4JF?>15J0;6~~CV*eXle9LQXU!Z9?nDq;vJFKMY7Wwa6L{t^ z{r?3JK+i(*?Nn$Dh#lhhu6tB}Pz8bJ1DHc*y{*vQn@fJ<9p_@3=UP0WJtw#0^7nqe zZM_t+;m5xWk^kwwdB6wer-jHI!hVUeSozULZf6&5#8#H;NAHNC$mz42??}16f>jtF z4xB$|yv-89H##;#Ud#VuV=$!$bMzjahSs#S!pX&9{t3cDfOVF51<S8xo6=&0=TTQ| zP;vasLjxqkcZe{YQBO2_;Nvk5J?4A}!F)Td&pV(s!Ap#H`Lt~OadXY(fR@jGJY-)8 zT^>?hwDsK6OApT2*ZU_Sg8Mjih|S{dwjb!hR2r{+^8oiUair2+%yx^ifUklmWr1nk zEp-O~fs}Uemt-&k$!5oj!<I(rBK_B;St$#_k8>0py#JA_3iwLqOhi53Zye-Iy_z+8 z;Vsz?e(Vdq_^^Cj7+6Gr8!17*FQ(52=1{(2m~<I2JGGiHzrH^H!x^*iY`n5)skh2m z`(r<~h%B=sVo>e+gc*^^^;wc3pRE*Ns0kpu+x=cXE+~TM@!jw`@l_y2>sfaEzN`Th zb}c|*1R^qp4n8azd^0bB1eGYll15YYqMJGKY49PpuVKZsxRR;wZ*Torv*%#`%<pDO z5L%4-%f?lll;ervddslyx+=8@x77lX&CL>~5S=7_Tb8VaWjWog&SgX91&&%}(0pKt zg9?T;BjP?+?OFbZ<>+v5L8P!BYUQ*AKpBE@YgshiKM3kPJHPN5ZR<_f-MSZ{diOgY zj>m7WD>nyB3*43+TKV4U8WD>y12J{5CKZWDMVIU1#cu_ueZL!SJ;=OTYdmSHBZD*M zP<kT*>*XLy)b@TaI_!pOs3#{*=^PqxhJ+kLjl3sqnIaaR<(?HLgw@UDIkxdcGC(#Y zWH{K;0$+EZX?s}4#<ErbGX8mtU{Ix)ib6O4Z^?j!d|U7FzHH^lO%NR<5&RhSfD9ye z^zGaQT<l!J2H+joqlkQj$Hui=tQ8Nkqp9)M(A{qgzSdDcN=^G-h6s;UqPV0OFFy~5 zUauwfo697aVft!VQ&q&7l)*=>^Q37rcf+3#5J6kpjs|bR3h1amd?y@5Te?v+)K4r< zN7MDAFU1IG@IQ8do3ns1ggcCkk>2iMSw$$p`)j2o#rNu<_kj~w4=0zMhA%9S^qBJ{ z09=UrA4cf4NE9MZWb;|;7M>X^_oeCdF1PDEH-NokfCM4b<Ze1y@93aOqXzmgGfjXX zAll|Mp4!TB4#2l$Tytwo89moOqY&?^kNg#Q$cAZ4X9YfwozsMYj0Y?s-cZ)>f;Fk? zax>>y<J8Z?s2@g<_$*<i8^RcgXal+ai~u6<+J>9o?C^r4Igz@bUi`EeDtnlPoOYHC zzg&1WbH;SEJUzjs?wNparv;1J#IPL_X0oKeU(P51L(C!G%B-ygOt(?~WG`^-Kc1O( zho~CCI(pP$iD2PB-)#}dH_u|&jEcKepg=>I+W*j-oJuRG5UtSG-x}Yck<<~{Y^S_< zq#&t7f9%oPsvZYaK9QvG%^%Jxz*;1bG(x~j%~c-8eFS5?)@W(#9lp;=ZSG*0RS!wH zs9VihO%E)Rc@Q*@C;8tKzD$PCaqqwoMF-Xq5QJ@dot8I+XXehv_`~f+f#N5Y_4f`~ zzk<I|HS_&(G2JNzU=KS(|CVT7JzDNCoh`$qk4y(Yj|)bvL@Eth1yp8Y-yxIrm91EK zHpaW8bg_b08y-iJoS=qkBLR&ePimGDd#(>4DXN|V^ht|Q4G?gF&Y{*85Up{lj!~8& z?8Hr3lC-|A{X@9Un4br0<pp<)eg)n=7)KH+C=M#@yt%po*}q%~IoC6Qef>ynJWji- z2_S@(>1ts23yz0+!BdO>F%MN&=)m{lf75|~H6PfrhX*tDah6m9#9^%ou5)ef^9Gd~ z#zN+z**vFYVMMFZuv2xuqnk0ACh5SxHMv;qZ8R=QF+1}WCUE3uH<9SVK!p%*6`LUL z{XeW02};oDMnr>kX9}P3bI4FSr}bG>_kY$9#P=#-2*s7$8ln4TqQ8^|Ehg_ate$=( z9iajjfJ0pYa+Eur*(o@fa{X=m2?ONZgH@g~>)8hOF98QO-2n$(o}JxLQknaTSEtPc z&DdPZKKZdOvj48{6mx8@=X7$DWCJf@J0$^$MX=BF_+wb>s~geG!c_=*cGVOZZ_>`s z&d(c(zv#zkcO(0f)lMh5Bd`yWeh&QpbhQ}p-8^-8+Zg%5(+Y&U`<Y_US?NZcc)nIc z$DH3ck7?oq!iHUy48~tVC1bgJn&S9xgTX@IC!1m1`~di69?RyqWS_nbv&v96`{p;W z**{kETQzI2N&4|{?}{ms&t58p_~)0G|LK?3S0=l@11<p(Q-2Wy+H$=u^!SmV4wj@F z7J6D9GJeY`o~%QBmux_={<S1>lu`3tml~mq{E=PcioJI^Jy;G?`uw6iU&7sFOcV;! z1W@5~FKqi6B(X9mD|LiyZJcZ=F9a}0WpKVva)xtE0A*ff4MHwCqx0D`EJ211|Jg~y zN$v$>UO@9DetX0y`@4{gCG5WQB9=N@zt#{6ydL}7IotTG7log~Ec<fn``1L7g@8Zu zc!ZXlj==ha!4kGs{GvgMuq+4F@u}AfpO)JHD@t&UEQc(g?eFXNr}tHW!60M;^<DWm zL`Tl^QxraGF(5Q7+6na`=>^;oQ^xi^4w2P^Z|6c+QIyz3x@v@M>W?D)?6urO$3k89 zUn;93X){LlAA9Wz#T>5k>b@+RI_zx7^tylO8%dcd=*^Zl6rTm`JiN!6^7_+X&jwrg z@Jb<*uL131oq0$i73ziM$RoTXhLFd;m`kSAK9B3M112lW$Ih+%_6G)`-vAf%g+`c; z`-|o0V=ShB9I1Ucr#O*9!79Zu*Fh+9Nr<<ONPhK7{v-6)o)0_klS1JF1(iOCoQ-nf z^_y<RxGK#g<H}<RDUmNK6#l---ZvhJ#B$g&jzdA~z6_FWe^)7XMJ@8?U92^yDbnh4 zAeB;eg<PlDNll^_kq9OUJ$@ntK)Rfo+frMB-Ieg5`?46z$$Xb;QXc!6I`c1hx{fKq zYjg0~IK|aYSl-jN1cZO`b0Xz_5T%3BP{!*#k7W?cVDURO20fVlyD^8@08GcTz{Bh9 zQ18__iNT->(X0R`z@)@S&S*8a%k7pX%JG90F`tNz|2&FDJ|DCL>_@u9%dA0e;`n9Q z*5}xRHrO~NLanN!imjZhSHws3mn;(elV1?AMZP>Wpn8qDEbJ6$=dcmZcfj^QD_{X# z%QP2N+I&?2B4=lZFdl@L!sukW%5kmCR6Dp#Ey8{(7HfZMZicmPd_Qc-#oww(hV5t1 ze-Hc3)7A3zkT&AcJ$;n(cz6xTIMw8@RTNci75QZbpdf=-?39K?pSQ;p1}+5KNpcIa z;H!UM43Lv;|Ad@I;KBt8E3rB`Z_=zDX^Nm<Zk?V}w6G_M4f0`1oKH0ojH}Nwiz(Q@ zsh&mg8<a=CzD@}soK4DdyG>Y$Rr05WsL8!8IN_m^c|Csp-UiixhXYNx2GD$=G=t13 zrC69S-^F3;$%8dUc|kiOY<D^!Yk<2UqroP=&~1QRn)I*ND-x9}UmwQKh5D<Mf7LY7 zf$Nz;>TptEr{?Y07n_2H3amt?d)9vTw>h~jq=F4v#973IkL23#*F~19c`_rPHH9sB zDiu^`{GhG_TY-$mXcY$;Lhi`pt4CS3+}QVJw+_AtT~>2X#Zskxqk}BE`wxmJ$PGS< zWQb(>Dm=y0z-~pYyqR<qB{%8PZ{ppt#}AMEd@%UR^_x~DDX;y3z&;dd30L-pF5Ttc zQ@y2UPSB%?9up(j5}Mq)PY+xuaQah&rN6@UvZ~hN#akOIU=yG5nzu5f#D9vsTzcCv zTwv6eAVsBYPqgYC{nzv^3OT*Wf1=V8qFNc{zVIPa1Y25xg`FCEN2fRr^x-$Lw&N?Z z=P#(x0MieK*pBU3HVz3%9U~I^+Sa>SW4T!ggeK&OJ(^?)tYUs`F1%dwlFJ2Qnz}Zr zeU^xM_~BV5M@uwzfaGUkDIcSD4nMh+Y2O^v$u-2MhXkzTzD6^)qs4!nvxWYivhCIH zpndKQr@dUav<Ci~+zP|?P%EcAW%+^_IRW!5(P~6i%jp51;lWP=gjj0!ngE*9+Z3Na z^kk<V!SfCXt|}iN5;+bg?TvERl#9tRKz=Ys{CF#t%0bx?`j>TmV}<wK8g4s(S>V)4 zW<_iC1?pE}%ox>IADmv)yT3#rjWojH{9{&f8@Ztj?xVhZ$HZ*Ex@m9pSW<tRm-;&% ziqRyx>pNX@!wTU1XMxh2vF=U!ULTh+`Ax<E>0(tcgJ)Yep?>3^`^VWaEk}-I{0A!* z1)n%nkY^Q1Q2d3!#}{9c-gc1NQH~>MJzPJ*N@9Uawc|AkYNq?kvh5l)xBlKwTEbfq zW?Nja>D|wLXS>N;(=IMV+0#@j)yF4PBI1@{+C-hA8J8r<qEAebOjav5>dQ=4ou(EB z?07#4N1p{+G`c;^|L?fNJ3+Ml?3@;kqNAVYvxNf2XI#1@i@wG726!u*WNOSlhu|i3 zyvQCgJs^7JJvw40%5HCMFf6e)i#l1@7ef;@ioRSK?h%E@(&as|g(75X$LE5JR+SD+ zhxb=3az=)d5K)P{)?!F<(1Dqp)iCACH6TJ$K9P#f-StxmDO;CG@zFw{Xk2{w+IFss zpltSLg$nWuJe>@rJ5dx{emu+1mSX5pRyvROG`gnuD#`aDjc*hHxU-U%KQxq#z`hfm z6jPnWbyvWvPt$NKui88r>-=b)3HoPO44(n}NDI6|<zxeT(Rscejzok<r}`tzRBqb! zD*9^A&WXQ{(dRwM2}pYO<&!9%{ih?xPoNhVCac8kO%7F<r1>QB>WQsW0A=W9#!Hrw z@oD;stWuHt``I5K!k9%ob<{jKyO7xO?&y0A9?%BxKGNk^m+F854i=WDsTd#>v8YpW z?1_Q%U%Jj@ZokY!(80<=H!C65K2$}Wdu&_AVMI8Oo<;B3XQgIPMRuu3d8AZy&D1o` z(PE$|Ap$|+R0JeTG?bnP*>Og!)O}Qj)p0QDDFyzxo1$m8``sW?jwc=M?fGl3PQgLH zKL!L6;_=HD|0GAa{?CKQWFSAHw{oWdpYL}+hN1O|s3hV|;6QV(I<3f3;DRIw*M(;q zLN68i$6e<&tj;pkU*cRsCedpxXph6x<yqPfTH^nFp$;k!{Q<a=vIR3PbqeDbPLEbu z=lWmH4iCRYWT~6S0UDCGI=~xsxAhVfcS{pF`pQg@)KTiDtE6RAhPkd3;-z=@kn@Hr zOlIf#rn`UN^RA<B#e#>kcRwNpCizLxHGO1kp?)UClZ`6_WRzrJCM%(3Cu$f{g_09S zNmKN0$R~USds;Y?AO0tnfa~F9ivDEl%+_~lyW?9Hnf9XZJ1dczv5|<m7aE<;;e{`` zfe*vldUN~$)BL2Wzm=9nmXZ)ZQPZz9yP|$q1Q60#iBiFk%F{PnF)k=E@MEA5jZSv( zkGt1QU^PYPgL)_sSmL*bO@6;4vW7BKLer7Xx*G(J`ujLUP|+o;!FA$aDV-dmSd(}} zp|Yj_k6q?CV!^!@JwnsTKdaJf{@!+P>*^8^QXj{~E1$^8OBM;oQGdX?rfBZZb`Mk4 z%aS-?pLGbzV~t2Bczu6mz{{}XJ{LS|;}UU$Wx}C={6N#$!uRRV69V^zBy**hx8sg; ze?;^;7>Qz*XX(YZqQ998H+olEDfsRAb_8!w{hj(KU&{qP%d4u3sdidTcDY*`N)O!k zs%H)w*k(~N0)tBub&~289NLaryDLUdB{PgEL7LizzXm9Cq?nQJVYrfrZA8rv>1`-t z`RePM@|WkPt|wM+3iYW#ywjOaYyF=Vq2FdDKHQ~fxc&N?k_i;1h93gZuY}fNz2vkY z9^gj@2c?N9Q>X<clL@rrw9HY!@v-N^r7%_e1VkS>+Xw%Y*AHo4y^|ctC()w-NgJNA zVIFv{^9EoI{NA0nebCMa0^P7hyYDaj5y9307B|1H;B0+rkjYdsfatH~V>4BTcC_hr z;_4aGtwcLJC`C3l1L(fk`gRmGt(=dUsZRQUFe4;l^IIkQHGAJ_vsrr!KODKdBAjc8 z-VFyTLWDgn;P+U=KJ$VDhxDjGssw_mp`+o<y1GxYrQO3AxTEi;rjrqRBy46%QqiWx zhMUoo|2@8HwD{#j1;;mYBp&0tWXB|*Uq*DEW%IKw(9%y!l0X4!0MpJn54l|EA?|r1 z#Vz4m1*c3nGHtbL9g5F0(c2L;k2NuNF+hek?+GIm!DoN(E4CRuWoIw@s_nw)0u9^+ zgPrhlAL$<Efgx?Zv@ez?$Kggq<4omcrdn9%5*;tji%k%R7WsY&y~AWUkefJDsUT?$ z`?3D}!dO62cl$T6MW<2DJ?06k(iTEd&D0aU+%qLZWUkL=U9Prj^4@=U{VVrVertOE zDYqDq2+E(sX~s+B;7&>&fj#!GCMt6mx3c@<AZ7cP?rk!6i}oSN!<#GDgbwr|gDdsg zC${rQ?%9{YmmPiB9Tjk`!|-#~6!aw=$8!N?zc$1O?(zoR{nWCsgDF93+MPYBPdDBt z-U#ijEIgyZ+7buwTK(eCUOPS*R&+EHI;qm|)U8ASOh&fkBD0Q#@n)O?x8{tdQ!Ts* z7Pw0}!H!(`QiZl#8(Lz;`!jicFh$IcCm_7R0&agTcNqt~*a^OE;U(Y4qXWKx1nvN5 zhy{}XJiS+$CJfkoj{7#dt(4A+S6^hcFDp+vIwP2byXyCsZYr4@m~VQg_8<$CCOX3i z-jrZt4O|HLvTN>?^{SK}Vzk3}i?uxKqAgI_!UtxyDMa!6a}Jd<{`0Vog@6SH3u74m zKA`y>&3W%yt2EoI%nR`VuYZJZv{>J)#y@NOYkB>9zNh>%9ck+$6D1|c;qDKGT?XBi z&uw0#AH?+-KviY#Dk-ctZM~!m0jFrXzpal}xR(oL+7d2M)0+qB&7zfZV6Af?TXSfl zIUG2)@T_=Wwvc3>{1$@kc5}_;hE)9RP1@1R2}T-0u)W3dZD6xc_4tG6x#_tFr!^FA zOek4`xqg4d1uA2{FKkHIaF`uak`MHKR{lWq5?c)?!*N{v32CNex^Quw;TI=K&&WVJ zC)risRgu5s@!|lb<{Kz4{G+<RRF42Z`!`BoN*&Nr7qWLBIlkH;%HH6`hRho>Sifxl zOMZ2PusM7}ckPWkQ#5<Dt5Wz`TI~v?XL@SLkJQ2brD&xB*!E|Ks;vp{D@0&e2CGe5 z@6Eof3^=g$?jD!T7ws)!7NS!~`C5pVhic?+k5|EG@k(*<Qna3At(;mS{(o(~J&OVU zutQg--Ef>1OL*YgkX2~|M&0!xciZEt@GFGy&*;=^dZxLHcLtG%r?0<Y`$CASD*js= zHYSLX3k+MTzPTh&CsvQ7QlkZpLVXIagg|zDaxjlB6*n7U=Dzo0(WW+D;;AsRIAHHf zgb8+#DAVVhE!W!Zg~6t@0Yi<G5y@VA*=JTS_J!WQt3?y_I)sq!lg9&>MfhH;yjmLy zb~{A<{gsJ9cvC`t4*0EwXzUmtCt6;QlzL0(2J;5&xVp|zl#(NuWF>>@6<o&R@hntF zs=M#G!6pp`L8F1%{g0%i3;3T&!#mon<TY>j3~v7eP=Qvl`0?;Kz+aA9txJoT4<6pL zgWr9Oc15A-f<Z@!$j7@Qy#Appp%;eB;(D;?KtR98$CvIb&4fE2tk1d}yB4pD!5B)8 z=m98Fd3U<4c&|`%TT7o2-AI=#HxOw+@kq8=uL73#@ndET&kxDRl3RgGZ~Wv5{7yo} zibQxhxg^ArjzU$6%n`sKMUC{B<!;7?<&bu<&9Lzfmob;E@vxhx_qx5ym%rZhNVKzd za*i3wKnw$iDqf@K#<mx=$B@&jBBY!B3D5$n=E+awT(p4@%sl>#>_pGB=-6NtZ<pg# zkHd|<@>4gyc44+O)OB!CHh9l54xlDi-oGzfZ&=!=rM(I#R>qNc(vl1F&ASRZ5gFG8 zjVObxFMSD2kZ<VKAMMLd)~~LK$7Fv$piNR1<th?fyzTDQHC?AP!Iy*?e4h05qZsG0 z@LHDMx!1_To0f>l_9w<8SuKC7MKcvNkNJHYl1+HQW1r!+_^Qku)rlA?eYNW`vM;*~ zi3P}*8V07>qP-7Qi%%X859lvlcY?=tL6ya^YP*s$7t4y5Gu|hg+8WPN?^o96Q!@az zc~yt$3ni4RD#gc~ML0c}S7}*W)rhXbQ*)=IsY=ai8;RXM%|MZ_bo66-+4n%?i0`3; zq2Sg({QI<v|Iw|)OKb{d#x5fNvtTF3K6a4Fucxy%>5ZYbqhy(_(d;l*{(gYZ?D3IM z5AmhvSjd12Aq;KWHpJfdIz6}?#g!k|2R+-1jsw^#9_d#Jx~TIT=@beQ-ovxLBpD<Z zg=J*eqisntHX$-XokR{!%q!IZ1~u^E=?x<%1<p%pcwTidUu#ri_l=1D%Y9b38V6#3 zG2lG&$<b+%X+ztI6}8XArwlT=s76$;GSgV9v-Gq7n5?8makw(-st!{%M^ivhpc2av zg>HD;*^_2sD{9<WoOEk3Vh+SCl~S5Y!C4`Ktl=b6pG{hOl`We>c`u$5U|Rxfbh*I* ze9R1ayp5yX7KsRp`oI)O^WCW>9MF9K%QR6Ga0(lVLG_Ogd(I@=Y{r-!%#u%cSDz`U zYIL>E*ZQKCs;`4<A)riXu-B|IX=wLO113ny4(noyk}k8{<!=}69I~&)k_YAUg5O`& zd-m~Be0#X7m-4eG3Lj(#Aimdv(}$&HXaV~oUx;!Rk;$6U;G`tj#;P4l%Gj=jQe-J~ zC2SFLAY7p&TzyK~M{#v9SptWC$dH-IN*zMz6BY#p#W>JLKhC1yj7b1X*M9C%tF|We zaf-<P{Ld3#Z4mckYANh1;$pDKzm_~lB(DQ^{r#`Wc2^Rd5dBEIoT>|`$p<W4`zc6} zR99D@C5Z5n1QbL;{1ChtU?1RJ_?I3tFz(~<=^vr3GLnT&_<G$3IfRF`>XYWj&y%G{ z%9BVu>>=2ZZ2Op@kWl<c$js+8HvZX^e%cN_vYtl4LldM4LN*A$Na_elt_yEHKpw2f zz`MIR6@w}ShtE^B>siZ4nw=6EQ!Xq%w(}2&?-g^H_r_4_esj_&UktE8?ySfJ#lG&) z)Bjq8>-cp^<jxS-ml?BDZEwCrUCJOC*GI`+ltT>1Ze4%xEd8}*r<q#@m<mjJ?^0KJ za22r@`}G;<i`}7AExyKN@6XbGSs~0TZ;6Sg3PyidA{?jM9bm+fX=mG%??~<v2F0Kp z7QGwC-#nE9q%8$!!+hFIYTGKKeuInTbto$lN}@ZIFaape`xJ$@^34Kw2}-eQzKB58 z{Ee$_u7;sTLl4CAa_z4*NCMCU?V8UTPNiBLN2-YfuWKQV(p(`5t{U}TCBeF19<oxd zq0~)!N0XbG4AhTQs*w6I=U{=yV_!Jxg$QRJ7yP{<V}8!RdCpx!*d#(1^f1jrwZP27 z97|E|CJg@{%J^62gtPP0X|Z}<ufd|ee`wuOB*<7=VR}f^#y6n)vI?Qc7oE<UsNeR< zcO$@`|A(fl4r}uL`ox4Gwb3B80a8kLNNsdC(v8w3pyZq|MmHket$=jHq#Kk_X#^2T z>3aA5UDx~H{&+s;K6Rh_Joj_XndrMHk3!&jm<sR(W8JrUq%Le&pyfgC)W#y5$)c=0 zkLT)hMOI%-we$f0tI{V|GIh$dO5iaNqel^b<=o(8ecvCz^c4g!Aw(x_Sh2lQtOWYb z_5hN=)|J&phvs5cVwZ*m%WQcKZ$1Am)v{~E(9@X`IHQZcc!c{Z_KaE4=2Z#T)lKOi zJ1(dvRshTHt@=zq1wxZFf@#*w$lcr9NBf9{*%HJ;tMB&oJMOg$9!n6sS{R3q-#I@( z=KFjv^%E5F`7K{X<?>25ICStrM*)XB`kC!82nPP+R*c%*ZMOIZvtT=^Y$U6-(KbOP zV2Q$ke5iYIKw_lnhV&D91ee{0LjNjF;Cg_JH{pE0u~ds<3+%?w@PNOx`EwL#HS(W? z-3pH-6<mTGc1SD}vBjRKF}LP^!ejw49AyvB+P(d<xhadh=zlhCA17PN7Yo(IUp@07 z_>}Rv?k#ad3dtDt*XuuQoO|&{c?i`cwyuwJ{<c(lijk_ps86boq+7BcK~zgBUx$Tk z0;plqq_`-?ko^XKZz@}QxjM-?&pJ$l7Lx@SDe9W7*V!HFqtBuk$XKUqD7MokfgXB9 zFxf+CssqAWG>AA<{s-v?7(?Z`cz8C74ff^%#P1)r<dC=+RT9~IP>pTT$=se>vx+6F zRjoW{^BntFWnKnqod6-r>joCK+T_uuy(GOAp~MSqd9VPlpQ!0x>fU$C7l#F}!|>_Z z@8geRLhz&t+iS4|PlrG<k-!h13*PclSseWK+;3M3m&{rxH#2*yJ=PV{_l4os<TsSt z?$)P&l_`+sJlSg*OR|v_=m0$X-C#H$ALYimN5=|`vb2-5+dEFyMGKI=4OlgsQv;av zOSe3y28dyDtP^ZBr>J79<q;@*)VBw?jMQ9L*&-c=6zX7(xBoH9#rODrZ?z491<(S- z0Do>fm4Ab@da0&yqd;Mx?V3sMDs?;VJ|U9*2xspH(S?dk3=VRCR?%9gLb!#bYkzM$ zD;STkoG>chD5U<k-Rr1uZ3*ss8d@T;MQ5g-BKy-$k_L~bvug^MSIiHi=(GAbcn9K> zP^>tDc5!MO<H@FIQ)hv|ub#MHw7zD2eEVl>k<$&%mFJo5(G^jq2f1JE_^A07Z&zlW zHUtKOne5ZyN!g)oAC>|$As2Z@9=xIXkZ>vOSz`&xjfYtVcgxnhLWTx7?<XXXHYiXC znE+N8tWA!@r1DLxXPSJjp2sG~r8wig4yzd5vC6`unubJy_@J=76yvNythN=vktn+( zS$8)6U22NAKBDF48F;%A2<}1yGDs%ttH0|EA@Q;<56e(1k*Y1B#Juai*Ee2&8KdF& zD{1%AQhHScY_n?81i(I9VL25)aji8f+gc&Ui)CfP$P3l){qJ4V9{&mRKJa`W(wJ8| zB_#CF&N-D8$7<U7{%dI<nyW?#36zP++qpOBoQDlN%T_mh)c#6$6s4*1mria$I>R&r zArPMspgILye{X7<v;gKMs%!jAL6dGwV=R0m0Z02*GV4Jgq)-@=RjZ_AzUY(&Bg}jh z+O246dO7i1a_+Ogb7|S%0I3u6W#rG>pO;<t*_Q#|zyc1d2AF{!5L3+xo>K9LZ~cEY zf*uaBKrW0Y4XP2X@>@dkN(wkksAtYP@q<U)c5mG`7L5MO0l&<aoQ!ORbU=Le$d$Cp z6;w0E>7WRlB^K8)|8*dB-M}%)^wIo|&~CKznjtv}ZE`m6UD<gv=FYC`k{uS6?;G$` zWoZvZUm>9g4~MuB_Ycy)jNTp;E}Pt{kfGH+>g>LuEpeJDS8S)pcX?BdD_*;SrRo@F zgF2h{JVFPKv2$5>zd8C5RH=OD$KTmqjjq5<eQmA@QSe&=`~H<shPPlp#RoctI<4e& z89X^JjeI2TA@C|rOrf@He%acXRB)#Wxg-A{VQi_zK0RKqt6x4a>MDEEsh5VCHl+5f z;~h)xIZH1=78q7=jzx^)70Oz6A}{hF@r*AL*qrziHVl!TK)}aoM<MeHkjZ-$V@nXU zv-@5k#br|9XF4bX68;L7C#+ADjW%`t?t(X|iGG;m$h=Jz0D4fcyb|_a=d*Kuyl^Bh z>P(_A3>sko?t%z(C=>1#+0EU(j5$A#KD)acwJVgxKc&Vbv3)mZ3>io!Dn@&4Xp2NR zJ5f__Qu<0NIdbPv?B8x>s$hvlYM{D5i+u}+;8#G&S|WRny_Fv~BtJZkXbn!y7MP%6 ze;p<{LHU=ye1l@0pY!=PDn-D7)d-WG>WP||_jC-$H%#>%Q`*OYe7zo3x(5BEFaI68 z*3ju#H}Jy&uL{}&GJTbn3uWX{U%sE^>x$qSI=wsA6#g6HVADKKr;osSzKNnY#;bu+ zG{tcpA~6Qp^{`zc(zTh+ZkdK1(?aQ#fA0l%iSS(D)q-z)pb^Gk??>YhC|i2qaY_#_ zM6Ueb-SJq#Eo%Rt>)+T26_7v4w2;bJBV{0U-H5Rq40+FxlM#qMsZ3V>*HkZIX}EX0 zH9Q*u3%L5-hAfBf?f7oq0j%KFjkq!_&Mnbp0JCn&GQrDHlesrxL8S&#u{+w`wPuQ3 zV|2K6UQw~PDC#=gy>OoY7$xAF0hI$Ye&5#Q2y=0?&FZXhV64d|$b5BLS?Zuord<%- zInT@JG7{<sn-Biw&s>k<-w(hTZvW*9;;A5=P<PkoY{nR|QRSwpvFbAY>>A1!oAt6~ z4{fm$#g_GO6-wU5vMR<tv*$gtH^wxjTH>nRrfYEPJXl2xSIuqrwdNXp(S02jgy=^{ ziwwN%MB?<tiaIS^>+#xkzugBNjfaFByK(ad1nuHyGpfs)Od!8qf4=}JaDvBwe;?OC z{lr56y&V_j(iCe@MiG$2IZxc*$1Bb+b<R`kX)z3Ewata+8P0z3ZBL+CvnJQ{{oAmH z_{?^hcjn+njE~G$-(TuC6Wq)#=04o6Uu4eXJ+xn3e%88do%?QXBX>VUI;04KvV2Md z;99q-9)_@CVXlmXmaFf%KN)-9);JvSPf|Qzi{2;SI{!OEuVf7BDOeo0h*T}{;$3&P z0X{i3=gTv5z4&L`_8%8t1b*28et%Cx=n4WP3Zo~b42GzWW?vot<q<wNBW}4h?mBNy z=bq#0GXY%D7il4I$SRfd*bKNg(F<3DAeY@{8<U{ycUniE^$$FsZdB+YC-Yqha@L6P zBmxCWB^+!_Fq5ep+<3FDepc)BkGF&?JG&b-%ap-kJ5BUTW*{3N2iB<!cBqdAL4mu? zHyB~|+Mk`p%h%?k{}@jNSG>W6qns<9ED#yHhe`ym;TYaX`BMg_jKE3*J}bBS^Nc}r zBZAD2z0qch7YJP+nA^K|*5E2eYcL9%Go5YnhXnR#t!mL>7qyVq2^o9fmVtT-c`jxT zP=)6SyAgLozZsH5{1I*IUI}#K&*}1w8`kRzy?&{R1*BHp*`Jhf+N6V6Q7_pGC-2g+ zr2oi+=<p~1y?zdFitFswkKEDuJUB)tBe?cx-WWi2G<!xGQHjkwlqD<Qgh|#${=EL4 zi5DP2uld=z7z~J7#a=Em^5UVrNF#&}HO*6t@UiPqRf*^QY6T{gI1Q{4#3@DDK{Fsv zun(C!N3aNlW+yGSCir~rKF<)XJQ7ZF7S&+mM=j}eeNDjXF8|!j-eU%hga#|wyon<t z1AABEok-MR-BH?^JE~$DCq0Y)EhSQeKc4P|S&}ZSueYh{rU)f#`0mfz>D@Q|E|s9G z4_(i60Z@fKMJP%sDt_YbJ`qd`$1oATSbe{&n$}Xdbj>R-s51^Jtc4cJ*dyDK<-bhr zRmS|QU9kOB*zJJr+wDZC7cF1&V&D%mwP3Pvm<gyxLJ`22eVYeiV?ZC0MjVJr=>jqW zn?9eLpcE66GhGcD(LI9ps}x9$5EYE^bFdyp`Fln9zNnN0Ou~@vba}<vBxX$I$?z(@ z5=oB`Mg=uNgeSpJg{`)Qq8ETZJq&@Rtr&y0^X90tk%s0jmed50U*@9G>nBRs_{|3i zRKTfwEndr3h!RcTKpcs-X=drjl)MR(1OB^1&z6_Ff&>H6K>LB6LJk-!N(#IA`p{_T zH&eCG?IW5}sxoQ(I@AwdhrwHCZR+ZR1O)@12y39OB605GM4j0G4sk)jba>XcG<55O zY5MU#$oXqt_?p!Q9`jG@L2^?kr`R^cDwAM)x4VimIGTM~2_}_uH$m5~KVm@rtn%NT z7$1IVTuaY_m)#*1_^HBPIu&v~77V(Uc37C!!&Ev@D6F;?B|+l}jm(@Uw^I3Zj*<qP z)DNgLMS{&GtO0KE;4bJ`?6f?nr*4%_aHgrK2-O$?0e$mqjY4^jaZDjMU#!n`V=ry? zWnX6$wVD(F5g#AssglTBv!S{V5jb|sajw#e=!Hzp$6&(T-~Eopary@b)LCz?4yR)V zt2||26XbG5@&NODEF#Gu1iCN>r&zHkkU%9I0oO504;3|Vd^jYWEF?QgJIs+$AG^@U zpTW0Ry(eG!nyr?7OOALP6mY5$`<5YHP$(4TC3i_f4e$b-rGoV_G}6&;Oy2Ve#9wuG zd-6|N>(qb6{t%ybKG<J*{@45dOMBU}Dl0##k0a>^Mw0<IWIT4-6odnWs-qX|9z@EU zc5iX73mcT`R<`1MM-KZu=EdHY7)0YzMQi~h+=<q_U9y>;!Z9hi&`({;gw-u39*PuU z)?X&mCup{KgC@|Y?}S<GaNw&TKo<vcbQ!GunF_1}#THdI>t?l&JFIagK@HZ%Bo~_| zA7JFK>}X+SP9!#n4VVV_5uVa&^@=(^*@OB3)9<aJqAGC7FTtS=0e1!NskV%A+n}eZ z`c(#1uDAV){w}skRBk0CfP1il_XGcj2+f<SRX~t6U(JM;u<uf#G8I+L-%7quX38X+ z$s*lL|K_jooBf-rYRnhYTYn$YQUdZc_&R^N^R>2Hq?~syZ+H8D`YCv}7LvQ&xqU6* z-0%I0QV|Xsd}$fWy`Im?i<r51Du8i*U(|uph?dw_Svp!Hne-B4G&y4fq-midB4owe z=?pOKFO;U*<H#hC?CNilC7v-WwV^gh&dMw(sOIUmUMk{56%5&Ef@j_Vmk4wBpB1l? zuAXwr?2CI1OZQV|lMXJ55^w`oRcm5I;arjVBB7(CeiWQn-r$dHlo92V3uo;lf)kM& zVwJ4Nb95?LQd;1HzBKM#!d%Iwr2z`cKv}wn2^UF%(7*={{JD604ES`W<7)&NF_Q3} z|2-yflnQy`&HO=qXE2FQC{_nU&-{t@hx!y3DmnC6i(!3eobQb4$anxWH{hw(lKAj0 zsRt<;6*an{?ydI^vaTnss<eI@XJTJYjJNQlWx)Wjmss%G<b4ScChxMYuU8gr-5*`f zLLJA#y9b;16|-oU-5KK%0uWH+Is*v^a`#s@%0bIXtI<TAoKd>0ba4>u{o@0<wupwc zrQel;BjP&%8EZMHKsidAvoQ+z1r(0jP^Gl;6bgij7^;#Q7z}bA0)*1^st@yX2Ga_2 z9xJ47q<B7<v_$meL(ho0%NLY0bim!i(bQ6?pw8tjm;gOJV1R8@A=>-GR&!zwC-MEo zmue||$V8-T^9RrinZcJ2XsErtzJRC(J=Ja^A|!<3ytDgaG}&WK)*6Jb^*?{t0um)S zPmbfopd~JH4Qy;zf8XJj(ZQY8zHk=~$v^|C1E8zM6Qw!H3`HGUz)4#!p-e*z8sZYa zf@j>cL=ZB>z9~okUYXH($Zfq@g`wxKA+T^vG;&+Drl%D8%u9czN|Sf}O*TZv?WGY& z*-0=qS_$AK?%Xi%p(vjO0lM+5>kfbn2R&P(Ynt8;QSsSEpOn%oVyYI_l_>bV#NLdC zA-S~T3iwb(q}s~VN9mMQy?ul99_OGs`58drl?ubp=cEw<MO0;b9ZVh{O8^Duz%|^i zT7RKR)IksEABoJ@_2$iJXEB8nKE;7V<@DD{PD&kF!^d^BTfT3^1LM{f_x!NWYAb|# zjchctPuRG1u}Au8%0|(KsJ26zidiPQ=pc3TGYBq2dsR3=6?%a@(#8R+C1U(i4QMP! zGQ*7k-Ph?T9Oc9sARKI5A^4A6Dnd3v!psPxKr18uh{c(UQU@W(&lAa>4y1Z%2FM`3 z71iql$w6Fs3+uECl6WySb0kt4Q%F`)?~K{uO6W47(v;kxU)iM(unNR*O8q8?2RV0? z5dUIm0+mAk?nxO6-PX3dyIWaV`B#GWdsXEtBa?%nzcE_IHG&U`*G+3uEa4sRgWQ&| z@{nzfPSC|XYJx2%ManUgycn4PCP$hEF%t(58Z{Tau0qoqI!vFLTEd&4Vw>)+twS~@ zU@u2>PDEL|`2B$ECnO8~noRm()edgnd~(hjo6>#c&4_3HjcDLcdv~L>K+@S+Y|~y% zpxx|%n_^(RVp4B4wCjbB)Z%ZWHfd1@XCxcJH@GFX74<NP5Wlc=FZPFyD3y}Xm{m`0 zdDU{-$*O>n?`sy_q9ggb#uC92)Uh~3-O3&(Peg*=80%&`2`kAvQ|URlSyMq-biEWC zs!kG8T+Wv{Eg<2;HJTSdn(lwqo{Mdy8$Uube{dSSZLF`UiU0l?_X9}R;ZUX@?LxB| zRVo*%vF<LrNN>kValYFw<4>ebcGO!ZkbZfDFrFrfBBrww9;7%;_*;nyy2%%K*J#i> z+P;<r@}fsLn}L%dbPR!QmRAkJ7<n&&eWq{59E^hx3pAv5(-btJ6j2EuB#d3$7N18q zUg9nw{2`BTA5{>o9R=%+HdLdW=mLa94*kL`G+z;5!U>^oUldv$<d4za(50Z{hW*h6 zYyinod6Jr#a?kIPjn}vr5B_k798W91Pz;o5K9m_n>yq@!-=t%tONJ&gB`Soa`^8c7 zUPA7`0p(ojCS#g2e4=j&nKv1ik70KVO58%3#u)KtM>)Z*?>nnYh6k^moskq*QIKhn zl@b6f04lN_BA2Io1o_>dAD3kQKLO3bHo#JB<eKmwn~(?-a^aG;mq_dvDFY2VjhtMm z$|atR%Z%B(=~B1>74JD$==axyny9B!gKQqJH?v=T<B;CC7)Gw`k%S4D)Z{3M^7~ND zE;RkjX9<7rT&I8`j{Qc*kOk^#CY9MhyR7G=p7W@|WkOQkFVe5i%~6RZ=O~ClR4G~w zk4aRXJ~bM+#mQD&5GbH~``>osf)ijt+H#`8-*>{dYugpVUMUM@YV}p`k${jWwYg(i zJbrJ#nFZsDNpfN5$+7CYEygx-+TIHjk$E}-!kivGE+IM-SVPK{65s`DTQZ)FQLlE7 zL?5)t{?>s?u6J71GHv3de=&v#gjDR~JQx(vT<|Oiuc3bSV$}?bOMLB8*)&b9k1=Sr z4jU}>DApao%BhP9N&q<sn8X$(r2mz=6=)X8<3Rap0H2~-BrsgkqRI*iT;jF=;dxKS zVlke$<F~iBvhvnCVPPgFp5jA=7K`-DtP^UOvbLFr(_+)*<;%6R<j<^|Se<C)Lq}Tz z*J;|^pVHFr@ve*II=ip_4&^EWu7+z`v+VShKT#xT5MIa4PN5V$J!ogn-(#yqzf7Y+ zR<eMyMrklm#jc+VRLL|i6;&0aWUkSsf&ktl^PZ?%SL*2kuT^;+oiI=SncmDKtQ;_m zBv@z59CpfOWKuQ<(3`E?CG@j>UBe1<qfq-ZM~B}m5qL&DjXA0Q$3qS8NY(*|v*GU) zek`jRm46j^gb!dwZ<7C7Qk!i877`Zpl6PSy&9H^CQUHa_!E!hNdk@1zql<I{rZ-LZ z{`|vNgT}cKAtOwMvJ$7%3wJ2!8G*X+i0f|u?PXVvK1Hed%gIPfzvn<nUaU1h+<5eO zMIgnaATr|rUbTBP-HUwNKlpJYZkp9{p0>5M#b84r^3yAY#K^$JtjQJz-vmkDY*r^n zx!AAO?W{P{lcAJ&9v0G!G`au_)avg?fuYY;!K7cf@IVfM%ns8lD>V+zPad7&a|2Ix z<8o5>x@*e^B;P;2_4>80zeFA=r2!H?IouFeTnO$Po2Sd|Cne+MQWOM8ADPShn$)3L z%i8&J6Zkp%Y>ymz_hLccc!5H%22}>Fq)G-@%k_@%b3Mjs03Ab$I3J0N3?D+n;uL7; zjdigu6>3%5G>zN%m<K_%{sw{bPE|)IU}BFXo6s9(E-;^iicAxtZvVtk=6hL?ion6h zReTgYCR*pBqEFXF^4_+%xfi{ScAjtJLIkgfKcGF>!A|BdPT$yuR7+-#CUBXCcNS4p z3AKMCFJ@ikajfNEDRZ@Qb^j^8DD^Jp@vvb}I_C#_QZiRAJU-Yb;X^;(4t)xEb?C<Z zuV0kgeAc-=JUc|c+9=h2jHjib#j>MvVw|0^$-z^|YMi4U9CYWXPo|z{M?pJj;`fy< z>jS8VIjWxlok-fdASTs(JzG1&jYzfIZgd?r0h;X)|62Or8SYpd`e;Ih4j0Wb_dsDp zp@4vPoJ$MArE8xEkAzcAr=EsI{&(C|JVCQs2i-Tiny{N82~HpQ{#EAI{_5*k^njN2 zaG<ZRFT~#NmvCH&Ypp(CS*ZQ<B>Y|yzAR9q7BKC<$oHfWl?tJ|k?8;174;C##ee&g zkOioH*AwCKwAbQep$M^bi%%W3tCA(CJxZSJa(*;(D()hsPN!@3$GVp(3_PxVX|{-7 zQP+~;Hb5RP<wTLS0$N^9*D#DVeo1g>5z?!x;Zk_=^zpsRPm@ex>|~{aqJb$6AeT^e zAy5E7**U_9cF8vZkGLL$ZReZ=IEN6bUt7r2%)qB<kq>Z(8scZTplWYf{X`()YzY!r z&~HV+2Yj9LcFQMRia`g>AFVo%zqc+f5(Q-yXn?mw0Y4hW#-8wHvCz9>6Q$|3Pq9!y zZX7m{0aqadR0SOuD<&U%zf^0|_ocn1sm`gG%?c3^esf0B$}1NhQ@W=Dv=k-y(X#(3 z9%}PLZLz0eRZPlpeYhH$3JY(RmiByev-RenGig*Kh*YqmCSO9lyCo0|P_P;39Gx_y z!1Uan`<W_(4@M(xJRdPlIXr-K9o%is>d09MnSHilqLNX%5t5c`4xge}iyC{p_lX{D zQ%bZ(zwl$)mI;0{x%|F^hx#3^#Sr`h3i^@_W}_DadTtoQ1uSIcDs;(rF@oL8#~dUQ zOam2+Vbd~TQhh@!j~{=sUn~`mzvypp@t~cnKiK_R#rgCgfBB7e_t!Vi)+}l7E=ql$ zxdliyt<fLXmjM6ve@K3J8p(CjICXe;Vh+yVC(N=l21I0TBmdI6iIOS-==8DEo#M^A zjs6DKR|5Y6Zi-7ZGR8hz(QK%g=A%a1j~gK6kl-4>j`!+`(LwPN?1MeguAbo#xJ~=4 zS>s4Ls~waOJ@u?BtzKR!Qy+E|)bx+$B}$R+dRIY3E}V*khLy!jIo|s+ixXs{3T_aq zlJLBxsXuvQf(3Oyj_K0Wge-nbc98WODqc^byF6Uy4;|t_alptH3Z~VF3PX~6lc8R$ zeYOg)28g0?pb;z%B=llSJp(tLGDmg9dvZI|q{9PAcI1L!4Lf<884s#bMT{o|&qqP< zo^Tg>09BTiqnIWe?y14gg?mf#f?)hXQ=pk)l;%+PZ6YU4a-@9|zap0!fjL_Ddro{u z99cJ{>mCh!Dwyo5$!yCyp+n62@OR)d<;1Y-HM3mP7(kSp6|%=YXBo#F+XN)hx)wBZ zZ4G{8sZ6IfHkEe~@fg>uZ({b_%jDj4=%=4A-bQXe>sP>3H+1mgrI4_VHO!I!8&Cht z^6*`sl3lJu5BGQG%UYjdO6&`v3s7aAN6Bg#+g{UB=P`f^y(~B^z57P!`LOhaM8akR zirE^K@E*^E(s&qquU|c7|9tXf8=d)EXn@NOp*SC!uXWs$%HD-%w*m!t{Q1FbqXEu> zFt&zj5J8;|Yfx$i7OHt~QRL)p|N8sI-Ip&T=~QqwGc9948J>Yn4W+1e5{ADwBb~YY zB!(KH-$(*Z<xqjM!{mu0DKrXh@M=F8fGU1b1Z)03e`C4r%zuKkJLAZx1>;@ePrT`! z7z9W9wL?)OscKS31-l8f<}5NI6tEtmHxJhtB0<&McB4DvJQEh(?;<6BZ<&Hqj6axG z+C#^8R-^VO<K(!?iBuzYpbw3nEqW|51o`#0=Safc3`o}&zVGOQ8xS`<cH_urmVrMn z+pP<3<-0YcC-o+1?lpA^VdV8dqY+LwG50tbhK3O7H|WDlap_L1`yW>U8Ouc?qV2X$ z+F0s8a?5=dht<gdj7Gi~E2PS^zgATsU5onZ6RVQxAW#nc3TkSP(@ucCB4{<jDEmhB zM0+1A9UQUGJCBevk?PUl$`5_nph(@hVSl+3JjDG*REl5jwO@aGjspB*H{ti(sku5x z@U014S54WPlh+=~`ZTm>tsFbB%>5vZ3m2%@`aCaq_Fv`QNuFR`0nl7jibeLJeR6L; zH2A|2cTR|e((gc@N>qu{#}qRK%|un=<iv05QCElkZ^aZ~G{vePaHD;`^1p5gAQpks z(IQVJl`oQ=rDM>hE%bRaG*QB10Pu!{5kOS;#WWG$iLc>1xurt5AY1dJ{E-nDXMnr` zG7b$|J2X6yq@k|!%2?5B963|KsEKEwdm_}YxeB+w-IRrC!aAZjS*%=6qb5&TRw#0U z@!}}YxD}Bf<0J#()!=(a*EuVPn0U?vw5jl*BMba<tX;QG3d2&&0Z-gr`OLga_1Pon zRUnZTW4#s_8r<uP1-U)?8(<8Yo&cdTRN$-fB_UMPLhw`O_2L0zhDN#9%}4DveITy^ zK1t=kx95x>&~#-og*oKTer4F2<U1<C7e%AWXcsY931D5LGEZS#=Z6Q3wj`BEiLo(; zwYF6!&mQ{)>55G%@YWdkekc$6Y5oNpT;x&V6GUeI^W3@Mw6il4kf;Yx!Y&31qm!mD zqc=j@0SOp#7mhZy!J|*UH#GcMQr|j(;6O^dei-JJ&)rV>^uxSWX9BkUc?b7&k-(_3 zyl3O((BOsr&l>%{dGA^62m;Wi2lcU97#aSoQbu{SOX$=Po<p+W?t=4?=%#;wv}pVi zWDsb5mhEH&mH@4dJ?CJ8G5YzI0yTxI?H>zj6$BXyE#K9)`S6vVN{A>GtIhwQ316QI z_l^D3M|$wW_&*a}1;<-;Q!555bWug%r_t1-GJk2!;y5ZE;djZOXx<gQ)SGrhT{nmS zx{$|)I}*WPH!qy1>qZZzSP^2dq5cf0Q(Lv8m|zqQogph*ilUx5&Yk8-?-UDoP#Qer zbN)^p>=IA%Zz#ZYt#VtY9qR_bc5xZ<fHKt2o)oB8{*2wPEIwl~1n_s-uc-4$9QEhg zNd;7TLBxsQMD^8t|BZylfJQK<L9eI<+wKA<th!4Ck~se}@YgXGS0nCqqXw2lO*k>V zKCHx0@1F0M7CZe=L4=r8(y0AJp0f#fVc(&6jb4g`k=rg}^mQahjUM1{ltcAza?b+_ zYH;O_no#{zB>r1>Kc0TjHoY<ID@c=Zq9>kH4sF^o{T2Tky17Odi%9?V#a9)ShQz_4 zI1_J!)8sQb6MV2=#hhA2Qb*u}$L4s*+gkMiXHsu^I)K6TFKOr7D_M#y5M7?T{*iPX z+=|j<q=ukOhrOSppP&qpURPI7AFyD@T2>M8{LWbw>IsN><=shPB&W+Mr;*4x5a>g^ z<YQw42Z|0ajMhWF94aF><~_Ls1NAVkO?2qgfZ+~LzLr~iebfv7>TzyNq>lj|Gwt+L z^io3-f4?~wszHs1zIvI4bv`Aq@niab<7s$3!Umy%^KQ|!s#TKeBP~c=4^H?F^3Yyh zlE7!NGs7Kj_&rBVBHLCAB$z}FH<i*=`*@;>-@{ax@K`PtHstYF^XMsx$S!=4!tXI# zHxr6A@zH*#8*ia4aPZ@bz66N{SKpEqXXk`4*^oxgc9V!4j%r_3d}JWM;$z!JR*+&S zSw1krCMWp?+yW!*K=(2*(&;cC^@WDqa7Mb9E&n@sJhbP^L34K%we5+kFEp*SPUGhU z1kGvJlf!{h$d=v3(H1C`(x=%&jJdm{E=H<~@ITcN$^YA8n9Os|`+A!XqHb{4k3Cny zS=XpyL$&9FAUmkLUvxu;=T-yMYbR9#pF%Zk89#kQIr5Db1R&n-qV}&Dyz^-`VYWLV zN0am#f_&yVcCZJ!C`$JEX10}G0nB1U?0+KCe8Va~(*(0_#P<h_bj}~Bxh~e&Yyl-C zI(4bzOpZ*trfPh|O-gQi;GM8<Hw9x+l10lepZy+cmLl;^_lI-ggP(}XC^f{_*P{G8 zcl@z36BA)i9E^aE1s<s=;q-8d^kD$z#@z^Vxg>Wxea*J=Jo$gX$WLnhzGb2c7}z(g zr=@i|AYUIjfEj>7g4)nr3Rrk-B6{2YYLHc+|AJ5-A@i0YkD20KSqQfxYl6VAjTN}) zHon^E6&du5#nA#R%TQh%fGC2BVA$z_(b7}&<JLNl<Wd+~pKa0&ah#i-QAb4DRISlx z^)^9i&UuFO0}!+*pq6TaT6b%h2_h_o4My_<KGgkM+`UFnmd?Q%ym2!@0+o8SzNuGA zU0|O-^H2Mb@v|4N!qrgc-qRgC!GzYKr}@+h$+DAz0>eO`e3W-%RKvZy-%nmU##!`~ zpze#%VQrGc2pGkAx8D|_zxW_iK6OO)?+VsL;(b;^^x@aMxTI6_hs=@Gb%Bjl8B=Ih zyeZY;H^uYp0^K{w>>&}3+QbpBO_A~*cFDaF(BQl9yt+tdmo?m1)CIozGaXQhsYpZq zlQ_>yD!;fy)*+1%dLEtEX2_PHai&cLFtIs_Z2~rVYQQQOiwO7+p-Q#{S^gc(u`k=n zrf`lL7Q2@Dz`SR2;1m17;HLsJRh^e_&tl`qD+SCQel<^Dk9<iGd<ZH}r~&h!B#z!P zpxA*@yV?VjTyFN>v!+UM(4y%q!-R~#c|zu$KUD(R@Y8-#D?q2-n+H%DyQ~k7w&1)@ zg1QGF%A%F)Rp6)(CbA08s0tVmCEs@rjlbi^^7LWj-DS?I_XThBKI|Dq(NL1Za~yos zFqw4k32eigytGCSq@LXS4*DvV7?3@4DD$HeIv`jH?8mNzC5wBR!C9!|7rlPwi5)fa zFOn;M!+s@MwbnA(O>Hq|6P^@Uj||jJPvzWN3)rN9kQX~xQ-bR)O3f|Kt>(K9-lQky zz|wU?H<%;s$$XszosYQr)l2WLB)Ts{wu2|B5}2AHT<>nPwGW?MzL#(!VHCm{)5VZ0 z?HF{wbKr5U07l3=lH{y%91cEf_gn4Rx+#)Ft!W+&Nw#qlO{~c@Q+CLQI_Yc*6OhrT z^@8AjKFk{J?>IeV{0))v`9ks9hqqA*sUc#c5H8=gjwKQfV5p^a?#);{iZ(}81xJZ0 zA_C?2ygfm<$@%%8a$u6YqfE|f6hx>t(Gv**bW>*kC0+(yaRBUrj6#}v7<m!oI<bz9 z(wKR59u$d}=IxK5W#w^++J*q-n-cHsIRn!#?Ly2_`5}olX}wf%8&<@#H04us)zYX? zG>FJ>V1X!9h<Ms-Nb42x+OpA?0|37O{>%u_T9Nf9Zmb?`5!CimxmmU$xbqd7&W=OA zM2va*@uR|s;0*{+U+QvR`!HnBF(ZcU4N=5iE%`65LKBARgN@j~?^8X{J@SqeA|qm} zr+bl{I*hg9KvTN?B1)OTX96&BdmB2uF+Eu4v9Ox&BTI)Ul&0f2m0)Q}8j#vn<cQT8 z0hBOirb3c|x-5)XNuq3D7$RC0n0grD#a(jA##sKHR$q5wbv5Epjw28qnA89f;iyX+ zn0|YG>yP-3bY!WHUgrSRH$0;&26BQ5w)=!<4f)}*4HWn`*7y`D>@GMyd2(`_*oPaw zlQ2v^XS(A6FbR?9Bkh5*w7Ktt{`l*|4qh4i7(Sp=Q<X{1U*k6KA%7;-+W`Lv|GP9L zOhBrHr9224=9dv4^g-<uq(B99-?NJ1<jbz<$z+qV<VrmVgPKbv((Z0Z$;<pAF=ep= zX|FG_!;4GHm_$0__)^Te^x5G5nkh;sShau|w}kPEHq4Y!&OKW@(b=Z9EUv(Fe&9I; z;&E-9K6d1+pJkwv{uNi@ITx=P51v0D@20n?pwA#EmsxwCb|2tOx~46pp;TP+JJB{B zDgc5k_W4@<Pp$6%X$e_DfRc9gdaC!3Iadm=+gA9sjW)<GvISrWR&XZCY7x@_cVhcP zsw}qMQ4zfUdYGwQ<8>T=82&M>4=!J9n<is@HzP9{%h-bh0B461pqfm^ehSlQhE^@< zE?9H-i-%^tEJQ7{Js~Q8;7I1r#uu!GvXl;5-A)^L*6zhhTT7Y!sRj3d5(22Jc}7=? zGJGDRgq=^^V6t0)6103uv@=7<wJ6s0F;m+1fRacj=aU9k6i}*HmL=-t)*RUpeLq&+ zrNEa!e~%(c0~on16{J7W*3A7{n`nW^YK-X#W7FR!UUlx*b9buJy`5YN^w&xt1uTQM zXfKk2gFZ^}+~-_&*&KuhQpf|qWSXQY*Q#xS=98RND;fNe5gcIxEsGDOKnB#*iI|ij zOqL0&Gj%l=s|KIGoYjQjSBN*slWk}a&3`UpfU%edF?{nZfTU|q$%E}n=!CdPsec2` z?YghaOdd|siICW4LB&qh{~XoM_^w5hSX6aEugrz?IO?<yRmdM;rIIA6g#sQZP|pA2 z?h`~(u{dMu4`DpKA5j?mv?oSsI{;@}S5=dkaXJxi_gv_13AfZYPshX{S0fYHafEvM z%li(>o1{Qa2|2KW85<8a5R4fheMeJY5>IZ~p<PPapqmT+cj`))(!KXDfeTQ9|4bEu zH5GU7ZgX+>f38<&><%9SmpfGGUb6Tj@`$F0_Gdv=005RJ0v-f|;`i@6iLYPsD#pui zd43KAg>haK{iu-7S$#%|I~t4?(c8Tcy3=N`@_i>eu51RK=shI=d@jC`@<#P;@hCrM zHF$y3ipK}pBY>S9-!n9q{rNGmPA<f2qoN+Vx#WMB?-Z3O)qB3}AwxP(G!YfDY=nnc z_|DV=gFo35<$OBYC!C9A;OZf7;kk{5cI20@M4@aBoDzAnVghc5;`UQeCSmI7Ki3;B z*rIU(&-ZO;I`(a>Kit~=0mxCdvR+V!N{0|3$mpGT3au_*h;+q~y(p=LvNH)isd9I( zknW`q#C9?TkJAk;U*S0YCvV&L9jGj<+O)J1T1;%%HkZ^!nH@1t+4ZuSMS-=Q_iX`M zb^LwNUHeEkGjzA25~bj?FZDZf&BrInB4_n3lEmR{0{z>^wWLNc9$Dx3Gh&?|fjpH! zwSXKlQP-3_NpGD}Z7+gQoeR4#L=U9j5MDeZ#R$D~13Ih<iBbzLCug*|jd~R4tWF~Q z5DgbQL@WI{f|;1c3iO2&@buXP$QTyFK@EA4pDYQ}^*NLn(LS`;!AE|~JtUP;_!RAG zkJSdYFD&E{Q@zc5G&(VGNPf5eUIV`)v5+!*y^1rK!+F34>ux-Fq_%zkg+e%7wf)bN z+4llCd(z3loDyG5bR>Q{1p*O7N|{waivabu!`SN37!R4R1{irW{Ipa)s~S-g%D;fJ zivuF-&tKpCKe>YGcJ*a<KUMovDu{djvGll%T!=)j4QETB%6u=m6#}~|0WVaPFlluo zwuLsg<;YMQjQ?b@;_vP4X53-Us)&rgehZ~V2j|VIwNva12?sjtloQ4p>?9>6)*NHr zQdGn-0Z;cV;?oopCazqncM|1~ApAqLa>A*k>Dvb);Kh#eIcXoEXW7>9dc>q}EeKa- zEdQrztdBm7+aU5o5bhI&-4iNk_G21{u?G@T$**}jq&4Vdq0au?x+8Gy==SNI$Ntvn zJc~2ur^B%)rZwr%&1HZ6$dnb_T@;%wHYLH!SzUYG$A97+`+^BK))@1zd?1!z+>@!@ zT1G4Q_~y%P2FuDV_K2S`d<q|8EO+QywUc=0NR~siNECWFpQe1aO9~Cw)wv+dW%U{o zFb8)+>4dw2gEM)PZ7kj#UfJ)RnmZa-8>@jSsp*6y(AXjx^)pi{?4keSbDOFubdQKX zXp(MLz}vkE$}mY3fVw!|e7{Y;|Kn1+!D(FT&2rxFY09JSsvlpUbHKR-9RhjRe=HJf zPI^pfAAS!Y&*ou56uqkO64J4N{vn#A`DxJ<#hSQ}<HTFIoV^-_dMC~z^vdhjD}-iC z=4kXb<G{``aDBi4f=!G6Z1N)Vp@7$YU!;2b&yfwi#3Q8qN;d0DUhp~J`{|gko*}P~ zSB2KDd$*o|SI6Ea9Cc#3c+n_?-eC{QKlXe}F#PxjW$3TDXZJ*&BT*}WH6(<@1lB33 zKn51}WR_M6zU*D=Se-oEYR_CY+OPZ=3oRx$Jf>EVERffc@X5S^9m%o*IlbLmpf0S6 zE^C*+jgv0U@sA@>`oT=25WC)&MpOk8iE9F}fr&cRoHt{$N~<4Ix?`m`kH6=UMnt>L zQYWc<QU#k_ZY|IY%CXX-Bqpu9V(viGhG4j#zWw~=+X@V^`B|=)(}X<P`bWyV?*D?u z0M@4<0#2ywh+~;4v!k!=E-Oa+&+8OO;pL$6n|HaGnDy=hqA+7Tg@@f_DuCAGUNB6O z#I^(a+MP-Qc6b{uajo?%Fq4=onrkrDvgWHUQmB1`h2(K=_C=ZOTI<F&o^`N_q+N#2 zr>Ig$X3O12pa^nB2~%becHIBYENrvSh$3<OBbyBD$znbd`q+GTIx4VE?mFRh-^rv5 z-iLL5UbRpbZr^kB^{Z~^uKT|?sFwG{*s)K$?i87>ENs|r5XS=x#Ya_r63i<X3<9z7 z1Dq0tT>b^+G3)r^d}uz4htS=Lnn$uM#vIR&n5dcd6pQ0yp-=>pxOI5E=*4X5c(Lu$ zW38Fe-~9vc<J+jjmo*c#_J?n0e0@9~p9^-si>Zqx7Vf^x^X&=_>!zeyON~%x(?M`g z-<>1*g=E+4xOhJMX5l#Q3$*3~2S9<Si{@m?e@b;8Lm)>XHcHm}&pWlp^ZjJH7!BIX z>>&g3aQcwHM0q;wv$jwbMv}2aE?qlXsseHtYox&4e0oD+D3>!ycUP-+sZpZsxrT-! zW+>py1lh1GWp3dko;g*Pd&uq1p}obb28e~JRy&GM^7hFM_>Q@-V{UZat26!(*j7_; zD@{Ej;#I^vZ8E`t2}unZTGbTnkg7U?kBhb@R_#n<`LqQ2IqG5W3BgL$s&6d7D$AP^ zP9i>qp{%j2Fa4fbCto%kL)K#s_G(dOEa#GGF~cW4GtIe&E7<9yy{gHHRC`6*ub+e4 zAY>%qxa@KkIt9ZIhy}gghZ7?V>+|h<wZJlFp)3p75N~+mW8?c8ooWaP30SeT0Nyu? zrpuM!g8gqXY2se7T}Q<s5;mZkO7LPkwCieM87;B?DMFn<5jv*V?;*Q3<VsKlaADiG zh3hJ<LhLx5_oGtES%!=Ylt7~lrh{g!pta^^D5JRUu$Lrc5?9%nK2S%Z?J<IhPDh?t zblnVoku-V5<rsvoITUcD^t5fD!&eBA=zQn-zJ0`+rKu5W<@L+lIfj=Aa*h{C)AX71 zhTINU(8hGHqc=9l2z(b^I~d!n6G8vIg{7%97E#WHM2ddOy!^<4wT$>HV<aUZPn?CK zpU1DA#WZ{HhoNL~08*<~fLez~k>!2X&MOl$z24Y&tV*|Ga^=K12-7&kDb>3_&Ck&- zUk1=#zvMZti@n3yKBxMJox0u%V7MCsrdl|u*Y3N_QKkdSlElh#4#pKM3;bSdh{SET zNvu^o_o{$VWLTpjvNx>cf4l0mbU;d}Qcl77L3&`7fyalSqazxdY>`uZUevh1B2;gO zayL*Xa;kN-3+;7m?)(TTwNB8pWB(t{=KWq>O4KVTcHCr=cpbS^9Ei@JQcivu1~Siy zVLyJ?*x2ZvxXu3(s0R-|jkBa)`w^iYSvAa@LXNA*@}_T#v<^w4G4@&q+)sHSXiwZG zU|vg5^+o*y3#(^G?+=>?1*5hFQ%k}IcrSjvApwWB^Yr!NrztZB$LglG<HEZ*Wk#kc zC-gq_7V@B=cDV+aL%{2?ivZd-?=0QYZIOJ0yi-jD<2M8eG3)puGqa38b5jM>!e{94 zazAL@D1M1#^mU&OW$NS*E{tNTcaB}mhyFARibv^!;;Nd;nH}eM_}V1W15j|Tp{4mm zYUrO>x?q5!yZn-l_PUTSi7+|X_&A<;Ks1ly{&R7#^wV1S!mw$O3M_%0vN+3%Lk+lg zj4fr<CTPbw4(4?O=JYoiB4-JCNPoQgTB0~wTXR69<NGu>>jCAfrLdk^^kH6)BIdfC z6`=<@6{<)Rf8MYtVU+mGjT{}gO)JgQcZ8otB7XeUY3K~epLK?GPD#0$2R-49Ot$66 zZsLe{s4!neguqC?RW+d`H$o)r6a?M>Tm;RQ!}>P8vwNh2PGI$|8XHCYU}65a76o4) zgA<B|h#ZTJWKg80giW%`XBA#pZe8%786a1*qDV%zel*e_%dR!785)pDX-NNwRO{=F z)a_|UA12ri#(oZm9G9?)f3?fG{1vFQ3~pbC^m)p`Wwz>-1|(}&YsV9Jh{z`^K209f zQLR1BUKIeSHJlwMLYHMCkSHNw4=v>37xkdl4zNR=oq)LjY=>k~8(bUoN5qFM%snz# z=i(+=-ksoXD0#2^;AF38Ti`Z!lzx4cMDIuS997|`(EJt9ynGf5ObIh2_<_4GtaCEQ z!tZxb<@k^z+*MeAh|`mx=AejOwg}Zj#11P~2!CfoyZdG2m}mOMWkk*d*;^S;B6`K` zPvcZa)#d~|(ycA`jr7jxNLS%QPg1RSNABsVpG~L~NPCgM=@05d_S2Br^4pJV>R*F> za}>pGM~|EGq05p^{P2!dU%`xjbz=5B898gs_>-(A9y6c2CR408j_vafji@)N^nRAl zeH&(;t!TB&7*9{4GnHQ_?FD(4)ljVkOv{(ho1B%1u_e72R>+h%qqo-kbk#*eln_k+ zLoR|*czNN0AbNmz_;$x@Kj?Kk@>)U7xtD^X$SxplV{oq+<!pKJ?Zqb9-BPTno8EP& zhRYB~>9Ik5nC2`4>mDnyr)-P9GD{^Up1w>TR8pW(xGZDBb?y7{sC-1^?M7%~nIc9J zxuT8s`YvBWL~HpX$bEobO<^^PBwgYYOr`)B!iq-(CIxYLT*y6|e7qz2>v_g_a|NBL zDmr+A1?m^+Rt%gcHH12oj+!ETP5FUCtfwI)yvwGZ(lm;Sm~I{*C2?L#znjoQ9E(uh zUVW<LXjK%Qq|+mKi+BD^s)n21hXRbZLb<I2?E{SkN;DmrEyRwNoi3bZ2YPbeLR#Rv zAcpqZk}r{*x0X6widEI8ULIf~Oy@d6K>d_hA%)YNa&u3Q@Iwdx6@>q14qAANUp~d7 zFHI=s!3=$~9<yxNpKH@TD_s~XhFRF}%u0B!>jKg+a;0!TJhN;*I6U*5c@qT^;sHU) z7GdC8rg9i7O~L75Mv#LsKBIqRL)XW~)Y`R|aDd{DV3AMm%_p0i;p9~IdSv3p&HqeL zfO6(P35ppdiUtu6$){u+;o_A}4X|)T>^D?%@fjoUVZhd^)Q4bV4sE&33KkMKe16sA z4RSE<8?L^u_-Q*;1FqZMV)jxMisE+Qq>=^hdKLGdo!I{o{3HddE(k6EVi+u$*1@&| z8Yi?kHJ7p`T~q+y(T3#Of9(1w*GS*OA_AfUcmS_ff1kAj?@fV3U@4w|>dZ&||FVzH z9Vh?|MAF@sJZY~Vf;XQog9ML1kW#JPMW}alP1Y}w1j+S`6r0vXmV(w_!(-nag7nhK zz||}w9jgf?sbvjIK|Q_a|L*%8fjLp``9N|K!&emtDYfIK>*eFR`huk4Yzink#h=3H zc53fbL4eTfr(<ek<QGoSeaa<v#}AG!0Vn|-Ikykl=!>=HUf#Y(_-U5A*H0?OhbU5i zFLE&sRRBFDX|FlF#Cg|?iXfYrke~^i(|ft|SQY^#DlrqalvAx&p^8NKWXo-)zevDQ z1huGeT5@{LkUZ)}`j|}Sdd9qs%VMbMipra-AGIj?a_qSmxg4EQHGV26IzYZ)%U&e1 zQfY9Ms>tJZr;5NS13IQ|^cpzd3@Y&UDdDI6+`Zm(6*inv$ehL>SdYC}4weG1`CyH$ zib`ZbqbSrz$%8^9DnGlo<y8J)jQpA<monLWU#ZRW5HDNrIri`Kz!6wN2LBUlKciw? zHS?PjEFz}DAjxX?dewgh^Amj-l?YOhB&DcE5=GDb5_#nwI=tBiRZ2CTB_HF9W+3t% zb7rfFDYA&Xc<J&94GeV4PcG`3EL<Y7q7arj4Y_D+siIyZw}l|;0TlV23^|j94;-JD zgbD~0;Svrp&xp5B2_A3AQ?Io~sK;MD`@KYh>*L#j#mOUMu!+F4x#H15DZ>Ad&j+<& zk(<v$%bkT3M#zB4>`$Nl^#G3a;~QZ{b2}q@cCXoA3M2J|Z?IsM@h@l>dOY{b2Sigx z<>QMJzRF*{<jhF)^?Pt1pEb8F8|g1!dWU1#^~%?;>>~d-4&pqIG7MpPBh~sZOZs0} zn<iN0$Ud?IdU&-me#`%ZkbAFvTQgCgRD%u?+)D{1Bh>{b1fJCx)OYFRVi?0hSXP#9 zH2yih5x3O(QH%{c>pdXBmz|J@akn+H#MLb6=gJO~du1^`&-*}>G7!6)!CvmZ5BO>I zry<AL!|HSA1&^Nce<y~ETmEInnvXCFHEmf%nxyyKy6mUe$=h$QKALeXE|V%eTPFfX zV4+p`X%wd+$wTw!Ypy3B-6RhNl=lCN5n!mYxO{H#pOJz#^~Nz7Z^QfZw`;_k?@m!j z$Pg!E<VBE+E*dDa(H^e!=lYc@AtVcU{&X!EyCjw=KjKa9#!mCZp@$;2Z6Z8=9wP!C zW~LlwYx5H{Sf<y9bnx|&0fan+`YRczaV*nE)#G8QKSzVMN+yf}PC%x9PYZ$<G)pQR z&PKb0Y;lW#kPQJkDCrN)r<9R8?F<MZ5mxw*o%Wu~ih~c>mjJ8;Kd{_){w(V^?d!z` z&w@dc4bP5;Gu4fxy2?qlISeNaQm016HyQJL8$b7XxDW0`K=8oV`7_8rGq>djqa1en zry&T}lLKLCB}?BZVd|r!!{`l}K@uinr!qvU)O+Laugqw~c%QPsq$b!OJ<JK(vQvXR z@jOAI`tkLU&4k!vbN9KbnfVIg@oC69mX{MfIt`0`d3Z<IKZ$uu#}vuY33}44^KZ4^ ze_g|i(&+le$mcVNd`4C~pGiGic7Md=cvGgp)pvru&6~$9f5AC-?r;4e?5}K>_Ks5d zES2{^31`%lS@YhP2Gb=S;8tl2+eyww`pXip{T0eW3=ZzkYqn5I?@=5oZ`=I(w}hEr zpNQ2QNPD^8)jVSwqln5bM)i0vg9<1|O#P^PFKor8#UzYkaNi}K`foN8u@xn1E6QMP zx^u(3p2QnWD{#zej_*2p^(?pX^1d#>qrltHmeE>NZ@r|@)ZKnAoCsY=j;5u8eHl%8 z2IK)gLwO=t1K%D9Q-2|b6I;TlSg1tu?H=$;Fy$=f<aCbhJMn+3Z;z|{=IZ7KrHKJv zt-GCNjxI_zY>!C_zaP4>QlCBJ{%+mn+jS6ppJG5|$mZ9;g%AQXpmvU$3Si*REZ;== zoq84f3hNj_9&GPicGt<6;n^p8n`k;6GWwA0DA|`%|4{b9)-lfRcl2)w)KntH-a;*u zG*3iih;%49M`16fLi4e&qAW9B)V%f(bUH}nD^ph_s|8IfG&w?@Tw4iI62aj0KFI$l z<Y`yqrAcu|{vgMCEo%Dbk3o_`U<h{$%Qv~hy>En#cNuvr@&s?E=XxmrvR;VF5KV?r z)j=O8=y3e=#{DUfSQcmbe{FqrKvd87HYMc(ySQ{OAfa?O3oI?Mbe9MyNC-+w<Fa%k zozf}Yv49{gA|<i3AkAk%5Z?9s`}e*7&fGI+=04|~J9B29k*-AFxE?iM)4YjfN*FN* z{W#9s>8|2P?0A+F^t{hM&0WSi7#?~o1f0Y>|5J`rN6rtKM4Ey|6-&8V(e%}L+y%A5 zJ$0P*;`j65hyIq379=b7(pU)bH=KvKxAp#gnVh}JvTMgZ*6#5({enWoRQU(&OnD5> z2nl(l@jo#VnE%{m<Madbds#zXUgc(>hk_)Sbzxm{eC8_JOn4zuU<aSOjb(Rl52pU- z_qfw;0L78xKjkDRhl;RRSS4cgc|mgjep{Zfq9yu<uy&%1C`4D+Q)!qJU51Eu*?<(~ z;oGSN{>^83|65$}5+dV%z{M2H8s+?|r__dNosLC0G2ST^+4f}L$BP&JguxOGT>DXr zJZ=J8wKuzIb%BLGzMu`WQaKbpXZ?i;di7VY(ryPh2>QH+@BB#<nmAa9TmL2zGI`6P z1mnO@>q0JIw%;dmg<DZ`pl@>0LC@LK7M$hiG7t$6)o*wsfC1=uNIiQ`?%!($G|{1O zaX_xv`?n7Ycc~uYVL6Yh?k9x_Hqwcyk^qKhCLm3){s@0e=;qBK?+G-cZ$R$JEOFci ztRR;ShhhFl&&DzsQ$w!oE+4K<`A?XpYlBOT7;`=qgvOkgy&Ft9x#ddYK@V&tvUyD_ zT75rB9g06rvC7t+PO=X`vc8UvGePY&`GSOefS^#1KB!K0vT@}M#n$_)%iYDJ(9My> ze2VwFaUEx9LqL32nz;9<Wd0v2kQ3|^0z2f&ph^efhL}gJJ$3p#cnX{)z?T*?anpNL zPzI$gM5eG+)sGNc^{0(Z<<7ov2Vy(pOI#hB>@TT94)RgH+bQK>PbJ>|jDW-eJkoAJ zQPa@o&6lM>wmpi|MXpc8l<5n!ur_=U3aA7_)%`puCakVsx3wJ~nq}+8AX-yf^)q7$ z@uz<`SM*Y$))tYpG3r<-Z7b#}(vvyI{tELB%)eR+Jbg3lW&x*6RNW&q0(E}75DG6t zIJ2QMZfvQF!x@x5*AB9dJ$~!?W$l(^<~G$4L4i>zMsy}C4=dKf=$8ZB-Xl(fKF~Y_ zhA_e{33zzEbe@vwp(dlf&AT?FzRNkS3p}ody>gfneF6)0*a#KNSW=@yF38Zw{j*Z? zBb5A$g;(d_M=)Z-M)+ZNY(t!^R$|Gj&U07;0lNC?F!ecd_ZvTp1&Vy#{P3MjG`IPU zJ9#l;AwUa^R43%UZo_ra&VA*ET}sXGHWg$Q<SY?dp94deC>=n&156(^iQ21jq`zcl z2{Z}OS9cLwrq=XOWJK}z2GD?Z52F=9WgOi)@~?;Gg*WOZ6uKmfL%|)<U=v*KuH02= zVd^w2h@ePf49k3?*nzdH-5z6}0BJr+E~MH3C3FlyR}+K(LD1`FaY2nA)qJh<+4#F+ z9^<SffnXkdbpBKwER;oVs-nHPP`pz*?f+<SaNp2pz0{SRvhpJG`-pXvucPkOzkak! zZ9opWQk$=-&9dtV@NWchbTl-SC6oq@34|hi2fvkS_!M%-YT_#w%v8|ekIAlKw%5AB zLew4qMiIB=fY~u(dg&JV-SW&P`&d-HOofTAq#f51PLP%^$!Q3)?9C1uKjDK|0!h-z zS7|TBD^WQX++o11A>zw%`;21ds4EZMg@1|+o&7md%hH?QVIhp%nhwZQzNR1&<_E<? z5Nu&$vR36RQ>%NCLxbfwBV*LwKvcPqLvX+^$@yTD_IIML|4dtnk>~`tS&_g??MbKi zAu33gz-k~XjX{26zymClMBQHXoEJ|keD!+^6JR`9u8uC0IxD-0Z7&7w8VqW{q<c^a zVMmk{&o(EHc}tN>WNs<Y#XCh`MbMlcNnc1)X@XLbKCvp*VlWZ>%ILV76dS1|n$_%A zQ~sFJakA6tr;SR^XD&lnPBNpgiWF#F-cl`ae&@5(UzacI_bC;EQtzK@*FYYBQ-Trv zopJ+v5I~P3`NgM+zIv7GL^rW|#Gm%<eD8svf9d`6El3eOoD5bN?W^;I5J;nXgY;GK z&(R8x#Xsv2l(6Y2pa8^jM}HjD^t5B#<c_`0M`%89Bx{Ty)W}%vm)sJtE|9tiGMpSb zhLeNCzYB@`(|r9=5CA2+Tl4hf_10lBDM8G5uL*^JMz98kGsY*<zIHFCjCd2`w{{Du zK{NpWsX*xJawR#yTYjVK7U_f_2TJCLe@)O*=MAHU>{)Sc0E@{uDM(0-*oj@KLx)k! zy}ye9ryEWiJoX&zQN$>X`f<*2F`T5j3Edd_fjM>~0+$F)Ui6=R@AXd2f%wN(m$=ZG zciJfj_CRUr03pwSmw&CcS7%PZo-cXXXcZYf#SJL+YbWlZG_`Cw5S1EYdlC{XQAT20 z6~|oCfVd)wv`=`SmQ+m8)2rr^Mi1|9$ZJw9tdDW@&<SN8s?vi~p1nGM*m1>!U&UHT zyr}A|2h$GG20^cIUB>D3lpb{&hA<f0wwYbz20jV#@vdQ;yq4vF<zzAn<GtA?Q=Ar1 zqJFE9!IDu+L3>=0(Z|vqg=IY9T}r2JPyu2kVe{6$tzkh6j(Uo!SAW#MW_48rIa)$L z0GO?c5mm}?6=6NPoEY~&u<<lM#*7J0?&J3Um4|nf9ee1a$66kedX_jBY=q))MA*Vm zlQe(})>q}E(x!MifMd7CPvWdA1tf`TvK{saj64Ml<bVysfCOt&0=T+h!`XKI{LYk- z9c$LUJMl*e`m=N(9K}(eK!=BwR8|;oz2yzte4W#9)S`FdPHs{zux2S8OsRbMlM)Hh zq0`}1_qZEXt|xRL58~6;>lI(K7Hk`CBVw@UZnk5@^hvftRB<^N@;El!iT0kM^ms{x zMOjuT7Ul>Q>V^D!eR}qI&~a0Pde{!~Oo*GR&8XT4WBJvV0%P38daO4E31Um0C$^$~ zTOw(ng<n{4j*<{^L&CENg-vC(a7F(U4{{3K41GqZe!gW|hoB{cf*dsJ4D##>1kDlQ zqJ_`hdaMMHZcMa-MD-i_Q5zqQ>B|uZ-wL*By~?!cZ3)dxMWPN*44E@&ORJ-`d8McD z8Qe;(uY=4$5)~#`l`0}0g${p_@BlWxI^X?rtt7gZA$L_bvSB<HBym)QI4FbagEkto zZ=GHw@$AC~4c=5A8LTR?NE4bYcXThwx&$_?oEp|*!zY}K%TLXTl?eC8j=*=)zPJ~~ zH1UMs_TLqz>*7j_$jkx&s0-oYhl^leSP<D>A2eW^`qfVzP3Xjc*DPl9r+E3Fn9M-d zJ0e#HRVNSD4)NX@5Mr|6?BFlaea)4ncJ9S`Nd|;l88PfW6Xjt+j{^0WE+*p^U6kl_ zlTA2ktGtHRlR-H#?UJuY&UuEA3s|?3>bU|m@s5(6$a3~p?N`#>@h;$LFU%9NKL}1Q z02G#Qw2zY)F!j%)VyV~La~izbwGUZYiQjgn=o}q*c^^{`{d2QMUV&FLbjFE$0QPW# zy#8Q`IT34DrZTV)NkRFBhOOniVEt#QXEaMxpXipf;{(i$s=i3c>vCb*83Q(Xn*T}o z?xj>dLl-mmHgHsgP`sPyULeeH#bbms?-QW#uy)pw21qWsQpwNsr)REP4o_iL759PA zIUzA^k@A#APK6;z*|V+VE?9_C)m!yjt-6ZSmiEP`0rmMd0`yh8<k3c_v+@@S*=V*N zDPn?63Gm2d+m@Hew&*2uRaB8FXRg5aZ!+yurRDH{#!eiI&!Xyn&9+Px|Gg<UXwP2j zSRPD!JH<-Rpg`Ntn6cL)6U{b91-d`XkEEhz1jO#o`}rStT8u4oYa2hZl+?ub>$X%6 zblTo}r{KAQINOUY1rmh%?X2G$dCCWqyLUdGkbFTP0Lfbn1{e!Tx3MwS9ta{DqX|^K zSkO7bFu>3S*U*j=K`K<f`qzUQ{zk%kjdG`0DVJqxdql{oMHAcG-_lor|N9T&9l{Yh z@=Fj`(@Mhz&#@CpTtoc~#N_lFZD^*^f9nb)m$4ESYEjSW^O_)*tC2p2*q@INGXu7k z%Js=N#K?aEKWcQItpFh_J@_U2OW)`X`P~{0&gm-`UsHSS8LMq!14Vsj2T@Zs(e)zj z)G6&F_P`>Qf8*2!$R~c;=ePq+R3iJ>6i9R*ogl6pxA>I-dy3#cb<9nBWTC^4n4;^d z!!7xN8`A%^H1$?2G6LqmhD{fzx3MDF)XSZocIM@!OX_!Er&4F+dunwOLbBu4eVg@- z6QTq`&rUyeL6|kj=|F<A<fNG+{Lf$y2$qKW1tugG)vW~FEjtjt*V{i=dul`l3C7z$ z7HLPw$h1>f)M-6IHyA2$tdK?Q6^V@bJsRRAUIOmokzB5*0Ni`^(6!05=<QrA?P+<d zJ|`JeE>VmnXEbQRUkeU$;&chr_hbe_kdxfId|)a9H^gTXWjP6|qfXC;5xOQe%ePlG zR5r4)q#o6pP$Fn7J*98*jR)=)_|zBcq3O!fpn05!MWcTTlXkV{+bGt1^`<6<HI^T9 z#M4<>(<aI%y2;RHat2t7D9WeD<gTc3_c7DKVoNBC&|!71|7H;{2!nk2E{d(TEnWQZ zf%cb-mfjORS3==}6GFrR91exRxEF5LPsX%+Js)F0-dC(`kV#EKUiPh60n8JIJZUsd zZW7dn@$Na9!P*Ma^t}XgJ=U%4Z})~i?8fwb3YGVM>a-@WQmr9h57ItTW>|0TL;A|@ z`>2j~r$UsFlZwHvq0h<6h_kIh-S0N55VYvhj4?-`@{bP2(Yp~)jn>rYmL4l7@y3>X zHD1>vL(nF6BeD&4-(o}*H=xgf!tC?cWE!Q$^RPx$5HljVhGUVX{Ju2;q5(%XI%=Vz z_Vhp7hZlal8ig~b5^1&2P{6^PceEpnB2JU-o8vw`T&L@}`BJ;LyMZ5Px0t1Y1AdkA zxgh0z<D$ml-UNT%R@7Ezw`M#fUbdHX?BvT4Wf)KZV~1-StO?5u9gB528=)R3{lWW< zuHk)KdVO`F+FXjBue;?fyZzUtumCzk=+ue^u~O5Dn$|2+1$buQsc>TicmtLyW>_ME z_Pdt#G*iwmFH_oEy~?sd(L-#-R_wEA&P4cHP&`r2?FsK&MUjH|X^TooMVZQKw%kbs z%dM{U{Pfdb6`B4Dd=1h`5OJby`Cc4NkIZcgcx|4R%~1dj?#K|89U<@MDDcJ`Z1!{? zAP;vW`&*P(`BAXdN=ca@aE;}ydkE!ghdwExI?Mk!$AT_`XLa;p08#w3kIKkChTFy{ zUupK915VTVY0+O5S?O<<IgL;oXGb-`RA4>uYF*P*JAAw6N7FuMIkAY|wR@&g3gKd^ z*lKqY&#U@_<CizOrNf6GqX7*^+-@RpO3Qc)`zccD$pD_OL5CKtbwgl%t9!OB1Qo4b zb)eZ`@VRDVwI@@7`475SPE+N2r0LB8H@XY|B{PAhZ}?h<0LF@-u@?a$%nBU~9<sfi zyhf&ry&g*!TPSy$tPP7{ju;NalJDbh5p&v5UQ`!^u#}7ixrZzW6#P@}I5WhN|CRMT zqV17kx7Ii57{b33zytAn>!VJWC$xFUMTHiDtCwRzc_~UDtxnUZ;9<R|Z}iQ~id7uR z0t;TA<QL_A?=l;m4L)S;fwbJNsIqo3JXqYQc%`Va({E+8-P4+(XbJi*c%SSL<v@pN zJZl_Q_Jf`r6MV1z!<KdDkgY}sVJ%uQ={i&=)0JEQIn}+svDW6r=hB|=*ZgOi9teK| zvbiSwiR!$pEKK5x$q!l|6I;0!eU+OolC8#rHt=qr>h%=+dtxYbcF;f4yJ?-khw)6I zze(o7!s$+|xdTGE3#F3a_)34Ouluuc9l*<X`hl%y<-YF;#&4IO2L4)A@SXA$jaBSs zl5_hp32;=cY+P|$p{(Z3gQT{79N<!_SkW=T{?d6hf!NN(trh5?eOrDOMJ6+JVz{w; zDbpDK$BTt7OfpFkgr8v%6+FS=8>w*WdKF3SNVZeaQOT5z{EYGyM!j2gNAh8=cPE!k zBBRO<!-6&gXACTlHW|>0VXrJ-6u|N6FYtUv?ov{B3sua*SuStaS|9@e3sHGNf(9Y8 z_Ngapb)mg>%L$*kF%3~6-KWSsmlihB0(?C1z*n<l2={uHiW$57ulDYx1X-zcTA5&` z2BW5xU7B9cJ1+0hVO@heDqhTK3jGKgEaVa4c!lv+X~Jc8$SfjLNp~bmDt9_!$w`(Z z=G0=m<J#_-J)hf_H^va5nMYaL7KEJ@Q_jqGeNvMm0?m}4mJ7+TjINOi*}XXMIcRa4 z>{@wO>TA`tGNStu_27m&3l>G~ts?y^Fbv7*X;DzkX1e0{+Yoc*1JQDo$#iT#Mh0tD zuh8VrNuIxhjW+_!1v%?+RiyC#ekE5wL41NZe5FV0Pu2Qq-WenP9E6`><T^O@k2X2Q z$ZP&&Lsb830GLn5B@^QQV9%O1!QHZEYXe%$0~7q=HSS55`36rUwtEow+;4OKW<wrh zb>WX4z+qLh>{mG9w@jnYJG*v_o7117lhVF3j`)-I6mzX>{Was}N+T|)>aGakm=2}! zx5Y8T;ZFk=KlRl2GgE&2t3Bks=-i*RikW)K?VgMT{+x`bn4w;y)&MFS5c`B;sk~@k zh>gDPF1@OOw*LHylq1<_xl<M1U*E>dHRX6c_|cQJlOwAaF5FLu@2+lndLZeyQ#aE~ zGZej^5|)Rbt+rTg=Jg(3=IAqV^26et%eq!D>YBjQd|=b~I`06$@Fh$HzzP#|?}uS4 zCMNi|5EUbAV!iT1d7T0GElsRwEaFX0!9ueD!(XsRfVL0hDQGmr>|a%Kk66h2nF>(H z2~kJtO`ArXoRc4N&yA2*thgGXP@E@!rf)sp#s|Cvh(jtr?egd*e7QDyHWt)0jIey+ z0miz@X8h>#awqXr>lhU|3oE><;5vU*7PIBPB3S#eK5iQ$nj5R(FCxmEa;MT$8xmEy zQK0RoC;0TD@EbVo#JJ5PJ`9NhqFVtD!lGO)aa~o;QIF;Fkle2F&SzOND{{x=VL^x* z*2!=X%gYq~uSJ4r0)5?P<$Io@ylOUj^Lr^W`beA7?!ss7$Jhnr4A}o#)Of_v7~}6R zVE~hJVS%86^NpQVgT3^2!kD*vGBM?^75g-dLC>&X=Tjm?80nvmY@LP44Mts2z%5B! zA{^gyirx9O3&>a4DfhXk{?pCEm{otHyzFK#=lR?loWI>-1|S96Fvp{x08TNEuR*W| zuY6y4xR-p!dy@#d^0^ER+QjhkT|6Sh{YhNlkrvDL>GkqP31Qkzi8%Y~QeF(zBMz}z z$&1H&rvFo8SnsSAqOS`$`MyH(i&XLUU19Em&v0(1He9)5Wj7|{MsqZoK4Jd@jq<;+ zW^UlXO*vMa$GOC0!iq5dhC8lCpsdDW-S&UgJ;#D(HR9Aj??lHHOLx50l>FvW9v^Kz z2*oGGiI6(I)et%QvF0P;*q#toCb=ztT@rP0TY>OQhr0jzPf5vV?mP`y&ei!vFeOc4 zzfUT*s3hvlD?bpvImT&AH8Y1roGydr-AjhcLq}!eH{nlyb<sb}DT#7z*S+&6^UQ0% z*+`{oKs^<{D5Jg@z_BXMvtp0?e$|`b-BR6SS2w~THkWZzE8FojYpxAw{V<3efW^_d z;<L>7h+pMpTr8K2wu8Fk!kg~|V<)s;s;Ax<J{DUrUu#dOGX5Ak7N|!QLN0a@<KL2n z+iUka^bOU96dk9S^(U>D^>Vt$zbiR+sztbPB)bI_JIkE{h>G@I>1bi=kz-t9E9jag z4YGOQ0~9u84iT7p&^AYke}VMpuMbsL{tN<5E3dbi%U5_>^H-d1C4i!N(%HDY32~O< zr@67GrU;pv<JcA+#o=IHVi|H;wcKBia*3HL_dQc_wWT}j$Lv|W<KP8ec5yOa9)4$P zo2zxA!!w2)5MOG<Wh<(a;MIMuC4JwR@A!p?FLaj}=|4uU+YO5KPiBo}BjDdpW`}c% z<=NZ}w~j;$sBnmB;+%!Gok^Y{ia%oC5OY~;r=qMZ&dXDuOChiWfTl#ZWR#e7&EUn} zB&I9tyV<5uIXI5|WQvgUa;MwJr9!Cux&6q9KC!OD6#9f51fyo3^Vh^jJIZuUSl|@p zQ9LNl`|Th;OrO;nt>g`hx?*<vPa_Vo5U<?(n(hqxp%6#q-HV1Kx4F+hJcuMSzfE#K zb|83<+x9$v&O^X6iVuIB#!h~(rBL9uL=YT3juqyHDDH72{?;z1y1t6zx0OPF%lN~H z!`wW|a@v76qTVGm+imVmpN#_Rc=(T*IbK_iJEO!8I@K(i9AYcZ2RG`OLVt3mCtd>V zUQEAa|6J}=SNmYyPMr5%Nox-|gWXtYZJa|~B#3k19gA`%=r15ZS$!q$1~%@)QbW4E z*<ORaH)e@W&hkqI3IwpJpU1fixgSqE&PXAURRMu@O&J%l$GJK_Wp7;M*1TvN>F+8k z4Jvwb%}R$`3DW5O<MNnDr2-S=CCE1fW~*lhOg~XDsa@0BkJFdH9zE@bZzd^nKzk+c z|32EeZg^Nc&u1Rd@j||!FFTeyME4P2Gu+{vy=avBue}4&*BHuS1pS0raYaGJcy_Eh z)jH&v=&RBwtx&@>`en+Qt!ejwm9$irP9RkLKvclz+o1QYGztvE@zj^MC{@kI`@NM$ zuWNMc6vd$cNM$F4R8$*4Vu_*&khi@rDDl4u+mm3+37RO2CPgYOc^vSXSIgnXmGOKW zlsvZ>A37m&Wp;LIlL`=Fgtd}h${P1%+Rq2tI}%PY(Sl5}kthK3bChYgwd(k1xUvH4 zbfF#6!tC5p0A1m;tda4^sO-hVNYVH+X7+g<z)Io%j!gyBfcM>!Xf<I$efTyabic$o zOD9(pdmlpmk4P1BnEXT*VWCrY$^Pk+ODi_jF-@|i6=ULk4gwM!*~{?^N^cD|*X6hG zi}Za)Jhhj})jY*w(IH=EK@4n{aTkte6`+?t(-53$lxWg-Oj~*+iw)fCtTm8)xX!!7 zIg;D|JX4Y%J`t0b4=8<&A7W^++F1`*&Yom1^SXHSz(itq#`^guXb_I?10jh39i|bM zUp#z6#YB|fT$}gY#V&G}`_Hy*ap_#(`AVc}Qmmr6^3ir^627HDH<oXg&MIQsX~X2{ zBAsJjq6*|gOGrQXB9r>`Z?|8^P&=PDP>?k~E9SRdpCwP1A#8*OJ>xJ~{fc3wG#ey7 zCgB?%bi?Jd;E#J*kRYJC_3cQwab2%Z1uv5FSdg;HHuXMp=V(TTB}-a#IGGW2{kAP+ z@gvxypyta%gebXvPT&b*$LY;xZA5Jie_X|%b;OtUT*4PS`URwNX@h}yNwc^r`pXn9 z<l>#9FZ*S{c1$=JiQS*IaNN)Ob6i4J+du`Pq&#-eGdW)+GcT48rv=a2!2Mfw*Y^x> z=^u=vW*J;kDc;DBbJR*H``gIt5sLqG(}l44<QL5e_L-F3;ReKHLc{S+w*%BLXLt6- zDsreK>zmQXrT;bG=x}pz$<*wV^iSz*uhuG4m3%J=iPIP(oDyg(_`X$v+9k`N^xJ!X z{WP#-?u`aJ8?TJ%-EW<2$}l|gqErf|ty7q4{6Xtq&5h<|-IWjE&6(o#1GwpKo{M4x zN&ioc`+}bzV$?(N=C5|bOej7s>GII`ml6lQzCY6;0OQT=PJ)5{B)GMke?C;x!8o?` zY$chH!_d{h7r*Hv>5|gQz`n#}e{h8Y1&3~8gXfP1OCc8@!N712Zn{@p&6zP(VCT!k zH1SJl!h1zS0oxF@y!)1)L~-y295QB5?n3b5!q*K!-CdV;a$&Sf5Wa3O5>$CZI}}WF z-}v(t5m~X}Q`HjK_O7m0R0C!_Nls>A<#(fVK~*A&M$o&YY2$2rQI5OeV;hkc+Q*UR zn8fH0;Hy}U=XF}1{Px<RG@w|%0kc0fosj_9(mukA=(jOwQ+dKGj<AME8S`R=B~p<C zy>$lBig>M4?P$+!rq~9UAeihxef-N0CYOksbpum8{i`aeSiWIsCV0%56B_iM`WlEO z?#%9An5~AIU$oxfefCwtrD#Cq@;@`7p&fP~6>RYh(JfvHAQiFBTN+lW{P8*|>K6=f zA{ySXU4cX6vZAUx>Frk;qdwcYnapeeF%PAH&|2i6QC=rU-8B~(Wz4Wz=8lr8LcuVb zWII<nKm;9V$PHsS_SJy)F>K*s?LHERi@@Q@hTvl-71~D_735a~QhbYUK_<!v1pf-` z$?Q~7zglGMANn)vDF$Zr)g*wimjufmJ$HR@hzim8*3R%wT8<64dwH;WgAwQnO2*-D z;;zV|IH3<~pbDFg<aO~9=<2w$b#N#?Up`pY>n2*Ba$oJb-nm}RsKZsX_U0+9tmq@@ zU(l4YXD_MMdwKLA+U{&1NOR0Q17WltVe2{*pDP`@#&68iJRf&yLWwh1^^l??)$|z- zTMtG5!q~!_Uy^rvWadbJ1N$uDwTkDiBs%#hLC;9)3E?1ZlrCDN#Z~Pz!zYHioIYN# zZv+@yQ!W;JQaf#L`Y9w`_y@aSi!4>_*(=d^KFcG(9)Y2SN@;{=qgmN?#r!;{cbZ>v z|AH442=<YB0@^%tfg6$mjNKlp#Ve}#q-@ss@)=7s_!=D6uq_iCUE(y)gKsqQ9|vWn zN3(##lfnqe+0VJWrN-_HN^I?6{jH_67GyGCGgV1Ccl03X4od@Snw+Z+f-S-Wv6-5r zQ!Pv*MYQp6&?pj!@F@*B@vzs^apE92!J&eRA{$WkL6L`IJ<IRq?w5mt;HZt^M#qRa zR^|T3deZ6SW||oQCgCeQe<?K#6&_RW!&lq<p;2=R^pNc!RjWSe?u{g3wldjPde@B( zsHkMDs<haSi;t(S`6Tq>wMU}9vrve7s&SZPM}L*Jq31NoN|O;g)HSy?g~w}9h|uGF zOcQTGyH?bDA_`Zfu%&Tf)$dq1*+?Z-QPz<EVeVr?`WCx>9LVP@t_?yislh-T%CmCE z=t;kB%TkLSd|*vcgYfcEujU~AnFj0IkE4!xt9~qA`gbdox_B<!*_v36yt>Y-0NYNS zWcb|hnBj77nrf}luoo^zML7eXfol$ivzK?!8z<RhUeiQ)a@CKhiXjaoi}SAJi@9S{ z`m3fp%eu8ZYi694I^mnb$+^2wQA(Wu-$T61AUD8Qk6r51`d6$0(^08avJLxx`M723 literal 0 HcmV?d00001 diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 3457b7a6..5fbc6c15 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -18,6 +18,15 @@ module.exports = { backgroundImage: { 'world-trading': "url('/world-trading-background.webp')", }, + colors: { + 'greyscale-1': '#FBFBFF', + 'greyscale-2': '#E7E7F4', + 'greyscale-3': '#D8D8EB', + 'greyscale-4': '#B1B1C7', + 'greyscale-5': '#9191A7', + 'greyscale-6': '#66667C', + 'greyscale-7': '#111140', + }, typography: { quoteless: { css: { From 0819c3918fe4e1827a1dd8a0070c82f7bd0c6ee4 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 1 Aug 2022 09:03:46 -0600 Subject: [PATCH 381/519] Most popular=> Trending --- web/components/contract-search.tsx | 2 +- web/pages/contract-search-firestore.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 4581e1d8..6438a3aa 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -32,7 +32,7 @@ const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' const sortOptions = [ { label: 'Newest', value: 'newest' }, - { label: 'Most popular', value: 'score' }, + { label: 'Trending', value: 'score' }, { label: 'Most traded', value: 'most-traded' }, { label: '24h volume', value: '24-hour-vol' }, { label: 'Last updated', value: 'last-updated' }, diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 2d45e831..ea42b38a 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -102,7 +102,7 @@ export default function ContractSearchFirestore(props: { > <option value="newest">Newest</option> <option value="oldest">Oldest</option> - <option value="score">Most popular</option> + <option value="score">Trending</option> <option value="most-traded">Most traded</option> <option value="24-hour-vol">24h volume</option> <option value="close-date">Closing soon</option> From ec84245dd44326931f9d9ddcb0a4bb21095d9abe Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 1 Aug 2022 14:59:45 -0700 Subject: [PATCH 382/519] Add some API endpoints for reading group info (#707) --- docs/docs/api.md | 18 ++++++++++++++++++ web/pages/api/v0/group/[slug].ts | 18 ++++++++++++++++++ web/pages/api/v0/group/by-id/[id].ts | 18 ++++++++++++++++++ web/pages/api/v0/groups.ts | 15 +++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 web/pages/api/v0/group/[slug].ts create mode 100644 web/pages/api/v0/group/by-id/[id].ts create mode 100644 web/pages/api/v0/groups.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index 8b7dce30..c898ded0 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -46,6 +46,24 @@ Gets a user by their unique ID. Many other API endpoints return this as the `use Requires no authorization. +### `GET /v0/groups` + +Gets all groups, in no particular order. + +Requires no authorization. + +### `GET /v0/groups/[slug]` + +Gets a group by its slug. + +Requires no authorization. + +### `GET /v0/groups/by-id/[id]` + +Gets a group by its unique ID. + +Requires no authorization. + ### `GET /v0/markets` Lists all markets, ordered by creation date descending. diff --git a/web/pages/api/v0/group/[slug].ts b/web/pages/api/v0/group/[slug].ts new file mode 100644 index 00000000..f9271591 --- /dev/null +++ b/web/pages/api/v0/group/[slug].ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getGroupBySlug } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { slug } = req.query + const group = await getGroupBySlug(slug as string) + if (!group) { + res.status(404).json({ error: 'Group not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(group) +} diff --git a/web/pages/api/v0/group/by-id/[id].ts b/web/pages/api/v0/group/by-id/[id].ts new file mode 100644 index 00000000..3260302b --- /dev/null +++ b/web/pages/api/v0/group/by-id/[id].ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getGroup } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const group = await getGroup(id as string) + if (!group) { + res.status(404).json({ error: 'Group not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(group) +} diff --git a/web/pages/api/v0/groups.ts b/web/pages/api/v0/groups.ts new file mode 100644 index 00000000..84b773b3 --- /dev/null +++ b/web/pages/api/v0/groups.ts @@ -0,0 +1,15 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { listAllGroups } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +type Data = any[] + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<Data> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const groups = await listAllGroups() + res.setHeader('Cache-Control', 'max-age=0') + res.status(200).json(groups) +} From b4e8c5d6024dec97ce2bb257a96c4fee17447200 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 1 Aug 2022 15:40:04 -0700 Subject: [PATCH 383/519] Backfill missing group IDs (#708) --- functions/src/scripts/backfill-group-ids.ts | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 functions/src/scripts/backfill-group-ids.ts diff --git a/functions/src/scripts/backfill-group-ids.ts b/functions/src/scripts/backfill-group-ids.ts new file mode 100644 index 00000000..ddce5d99 --- /dev/null +++ b/functions/src/scripts/backfill-group-ids.ts @@ -0,0 +1,25 @@ +// We have some groups without IDs. Let's fill them in. + +import * as admin from 'firebase-admin' +import { initAdmin } from './script-init' +import { log, writeAsync } from '../utils' + +initAdmin() +const firestore = admin.firestore() + +if (require.main === module) { + const groupsQuery = firestore.collection('groups') + groupsQuery.get().then(async (groupSnaps) => { + log(`Loaded ${groupSnaps.size} groups.`) + const needsFilling = groupSnaps.docs.filter((ct) => { + return !('id' in ct.data()) + }) + log(`${needsFilling.length} groups need IDs.`) + const updates = needsFilling.map((group) => { + return { doc: group.ref, fields: { id: group.id } } + }) + log(`Updating ${updates.length} groups.`) + await writeAsync(firestore, updates) + log(`Updated all groups.`) + }) +} From 0b06ded5e5b6d20ceaf39ee67b4399fa58661580 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Mon, 1 Aug 2022 21:15:09 -0600 Subject: [PATCH 384/519] Groups contracts (#709) * Update group links in trigger and api * Remove extra call during creation * Remove grouplinks on frontend * Deserialize * Consolidate logic * Move function locally --- functions/src/create-contract.ts | 91 ++++++++++++++----- functions/src/on-update-group.ts | 24 ++++- .../groups/contract-groups-list.tsx | 5 +- web/components/groups/group-selector.tsx | 15 ++- web/hooks/use-group.ts | 11 +++ web/lib/firebase/groups.ts | 77 +++++++--------- web/pages/create.tsx | 8 +- 7 files changed, 149 insertions(+), 82 deletions(-) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 786ee8ae..44ced6a8 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -14,7 +14,7 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { chargeUser } from './utils' +import { chargeUser, getContract } from './utils' import { APIError, newEndpoint, validate, zTimestamp } from './api' import { @@ -28,11 +28,11 @@ import { Answer, getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { User } from '../../common/user' -import { Group, MAX_ID_LENGTH } from '../../common/group' +import { Group, GroupLink, MAX_ID_LENGTH } from '../../common/group' import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' -import { zip } from 'lodash' -import { Bet } from 'common/bet' +import { uniq, zip } from 'lodash' +import { Bet } from '../../common/bet' const descScehma: z.ZodType<JSONContent> = z.lazy(() => z.intersection( @@ -136,27 +136,6 @@ export const createmarket = newEndpoint({}, async (req, auth) => { const slug = await getSlug(question) const contractRef = firestore.collection('contracts').doc() - let group = null - if (groupId) { - const groupDocRef = firestore.collection('groups').doc(groupId) - const groupDoc = await groupDocRef.get() - if (!groupDoc.exists) { - throw new APIError(400, 'No group exists with the given group ID.') - } - - group = groupDoc.data() as Group - if (!group.memberIds.includes(user.id)) { - throw new APIError( - 400, - 'User must be a member of the group to add markets to it.' - ) - } - if (!group.contractIds.includes(contractRef.id)) - await groupDocRef.update({ - contractIds: [...group.contractIds, contractRef.id], - }) - } - console.log( 'creating contract for', user.username, @@ -188,6 +167,33 @@ export const createmarket = newEndpoint({}, async (req, auth) => { await contractRef.create(contract) + let group = null + if (groupId) { + const groupDocRef = firestore.collection('groups').doc(groupId) + const groupDoc = await groupDocRef.get() + if (!groupDoc.exists) { + throw new APIError(400, 'No group exists with the given group ID.') + } + + group = groupDoc.data() as Group + if ( + !group.memberIds.includes(user.id) && + !group.anyoneCanJoin && + group.creatorId !== user.id + ) { + throw new APIError( + 400, + 'User must be a member/creator of the group or group must be open to add markets to it.' + ) + } + if (!group.contractIds.includes(contractRef.id)) { + await createGroupLinks(group, [contractRef.id], auth.uid) + await groupDocRef.update({ + contractIds: uniq([...group.contractIds, contractRef.id]), + }) + } + } + const providerId = user.id if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { @@ -284,3 +290,38 @@ export async function getContractFromSlug(slug: string) { return snap.empty ? undefined : (snap.docs[0].data() as Contract) } + +async function createGroupLinks( + group: Group, + contractIds: string[], + userId: string +) { + for (const contractId of contractIds) { + const contract = await getContract(contractId) + if (!contract?.groupSlugs?.includes(group.slug)) { + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), + }) + } + if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) { + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + ...(contract?.groupLinks ?? []), + ], + }) + } + } +} diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index 3ab2a249..7e6a5697 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -1,6 +1,8 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { Group } from '../../common/group' +import { getContract } from './utils' +import { uniq } from 'lodash' const firestore = admin.firestore() export const onUpdateGroup = functions.firestore @@ -9,7 +11,7 @@ export const onUpdateGroup = functions.firestore const prevGroup = change.before.data() as Group const group = change.after.data() as Group - // ignore the update we just made + // Ignore the activity update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return @@ -27,3 +29,23 @@ export const onUpdateGroup = functions.firestore .doc(group.id) .update({ mostRecentActivityTime: Date.now() }) }) + +export async function removeGroupLinks(group: Group, contractIds: string[]) { + for (const contractId of contractIds) { + const contract = await getContract(contractId) + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupSlugs: uniq([ + ...(contract?.groupSlugs?.filter((slug) => slug !== group.slug) ?? + []), + ]), + groupLinks: [ + ...(contract?.groupLinks?.filter( + (link) => link.groupId !== group.id + ) ?? []), + ], + }) + } +} diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index 423cbb97..79f2390f 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -7,6 +7,7 @@ import { Button } from 'web/components/button' import { GroupSelector } from 'web/components/groups/group-selector' import { addContractToGroup, + canModifyGroupContracts, removeContractFromGroup, } from 'web/lib/firebase/groups' import { User } from 'common/user' @@ -57,11 +58,11 @@ export function ContractGroupsList(props: { <Row className="line-clamp-1 items-center gap-2"> <GroupLinkItem group={group} /> </Row> - {user && group.memberIds.includes(user.id) && ( + {user && canModifyGroupContracts(group, user.id) && ( <Button color={'gray-white'} size={'xs'} - onClick={() => removeContractFromGroup(group, contract)} + onClick={() => removeContractFromGroup(group, contract, user.id)} > <XIcon className="h-4 w-4 text-gray-500" /> </Button> diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index e6270a4d..d48256a6 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -9,7 +9,7 @@ import { import clsx from 'clsx' import { CreateGroupButton } from 'web/components/groups/create-group-button' import { useState } from 'react' -import { useMemberGroups } from 'web/hooks/use-group' +import { useMemberGroups, useOpenGroups } from 'web/hooks/use-group' import { User } from 'common/user' import { searchInAny } from 'common/util/parse' @@ -27,10 +27,15 @@ export function GroupSelector(props: { const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const { showSelector, showLabel, ignoreGroupIds } = options const [query, setQuery] = useState('') - const memberGroups = (useMemberGroups(creator?.id) ?? []).filter( - (group) => !ignoreGroupIds?.includes(group.id) - ) - const filteredGroups = memberGroups.filter((group) => + const openGroups = useOpenGroups() + const availableGroups = openGroups + .concat( + (useMemberGroups(creator?.id) ?? []).filter( + (g) => !openGroups.map((og) => og.id).includes(g.id) + ) + ) + .filter((group) => !ignoreGroupIds?.includes(group.id)) + const filteredGroups = availableGroups.filter((group) => searchInAny(query, group.name) ) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 84913962..aeeaf2ab 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -5,6 +5,7 @@ import { listenForGroup, listenForGroups, listenForMemberGroups, + listenForOpenGroups, listGroups, } from 'web/lib/firebase/groups' import { getUser, getUsers } from 'web/lib/firebase/users' @@ -32,6 +33,16 @@ export const useGroups = () => { return groups } +export const useOpenGroups = () => { + const [groups, setGroups] = useState<Group[]>([]) + + useEffect(() => { + return listenForOpenGroups(setGroups) + }, []) + + return groups +} + export const useMemberGroups = ( userId: string | null | undefined, options?: { withChatEnabled: boolean }, diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index debc9a97..3f5d18af 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -8,7 +8,6 @@ import { } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' -import { updateContract } from './contracts' import { coll, getValue, @@ -17,6 +16,7 @@ import { listenForValues, } from './utils' import { Contract } from 'common/contract' +import { updateContract } from 'web/lib/firebase/contracts' export const groups = coll<Group>('groups') @@ -52,6 +52,13 @@ export function listenForGroups(setGroups: (groups: Group[]) => void) { return listenForValues(groups, setGroups) } +export function listenForOpenGroups(setGroups: (groups: Group[]) => void) { + return listenForValues( + query(groups, where('anyoneCanJoin', '==', true)), + setGroups + ) +} + export function getGroup(groupId: string) { return getValue<Group>(doc(groups, groupId)) } @@ -129,23 +136,23 @@ export async function addContractToGroup( contract: Contract, userId: string ) { - if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { - const newGroupLinks = [ - ...(contract.groupLinks ?? []), - { - groupId: group.id, - createdTime: Date.now(), - slug: group.slug, - userId, - name: group.name, - } as GroupLink, - ] + if (!canModifyGroupContracts(group, userId)) return + const newGroupLinks = [ + ...(contract.groupLinks ?? []), + { + groupId: group.id, + createdTime: Date.now(), + slug: group.slug, + userId, + name: group.name, + } as GroupLink, + ] + // It's good to update the contract first, so the on-update-group trigger doesn't re-add them + await updateContract(contract.id, { + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, + }) - await updateContract(contract.id, { - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - groupLinks: newGroupLinks, - }) - } if (!group.contractIds.includes(contract.id)) { return await updateGroup(group, { contractIds: uniq([...group.contractIds, contract.id]), @@ -160,8 +167,11 @@ export async function addContractToGroup( export async function removeContractFromGroup( group: Group, - contract: Contract + contract: Contract, + userId: string ) { + if (!canModifyGroupContracts(group, userId)) return + if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { const newGroupLinks = contract.groupLinks?.filter( (link) => link.slug !== group.slug @@ -186,29 +196,10 @@ export async function removeContractFromGroup( } } -export async function setContractGroupLinks( - group: Group, - contractId: string, - userId: string -) { - await updateContract(contractId, { - groupSlugs: [group.slug], - groupLinks: [ - { - groupId: group.id, - name: group.name, - slug: group.slug, - userId, - createdTime: Date.now(), - } as GroupLink, - ], - }) - return await updateGroup(group, { - contractIds: uniq([...group.contractIds, contractId]), - }) - .then(() => group) - .catch((err) => { - console.error('error adding contract to group', err) - return err - }) +export function canModifyGroupContracts(group: Group, userId: string) { + return ( + group.creatorId === userId || + group.memberIds.includes(userId) || + group.anyoneCanJoin + ) } diff --git a/web/pages/create.tsx b/web/pages/create.tsx index ca29cba9..642cbaec 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { getGroup, setContractGroupLinks } from 'web/lib/firebase/groups' +import { canModifyGroupContracts, getGroup } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -122,7 +122,7 @@ export function NewContract(props: { useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { - if (group && group.memberIds.includes(creator.id)) { + if (group && canModifyGroupContracts(group, creator.id)) { setSelectedGroup(group) setShowGroupSelector(false) } @@ -239,10 +239,6 @@ export function NewContract(props: { selectedGroup: selectedGroup?.id, isFree: false, }) - if (result && selectedGroup) { - await setContractGroupLinks(selectedGroup, result.id, creator.id) - } - await router.push(contractPath(result as Contract)) } catch (e) { console.error('error creating contract', e, (e as any).details) From 6901507461a465da84c0871430e16fbfe5f7f259 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 1 Aug 2022 23:53:12 -0700 Subject: [PATCH 385/519] Allow unspecfied outcome as input to `sellshares` (#706) * Allow unspecfied outcome as input to `sellshares` * Fix small details --- docs/docs/api.md | 6 +++--- functions/src/sell-shares.ts | 34 ++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/docs/api.md b/docs/docs/api.md index c898ded0..1c73fc05 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -599,12 +599,12 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ ### `POST /v0/market/[marketId]/sell` -Sells some quantity of shares in a market on behalf of the authorized user. +Sells some quantity of shares in a binary market on behalf of the authorized user. Parameters: -- `outcome`: Required. One of `YES`, `NO`, or a `number` indicating the numeric - bucket ID, depending on the market type. +- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only + own one kind of shares, you will sell that kind of shares. - `shares`: Optional. The amount of shares to sell of the outcome given above. If not provided, all the shares you own will be sold. diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index b6238434..ec08ab86 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -1,4 +1,4 @@ -import { sumBy, uniq } from 'lodash' +import { mapValues, groupBy, sumBy, uniq } from 'lodash' import * as admin from 'firebase-admin' import { z } from 'zod' @@ -9,7 +9,7 @@ import { getCpmmSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' import { getValues, log } from './utils' import { Bet } from '../../common/bet' -import { floatingLesserEqual } from '../../common/util/math' +import { floatingEqual, floatingLesserEqual } from '../../common/util/math' import { getUnfilledBetsQuery, updateMakers } from './place-bet' import { FieldValue } from 'firebase-admin/firestore' import { redeemShares } from './redeem-shares' @@ -17,7 +17,7 @@ import { redeemShares } from './redeem-shares' const bodySchema = z.object({ contractId: z.string(), shares: z.number().optional(), // leave it out to sell all shares - outcome: z.enum(['YES', 'NO']), + outcome: z.enum(['YES', 'NO']).optional(), // leave it out to sell whichever you have }) export const sellshares = newEndpoint({}, async (req, auth) => { @@ -46,9 +46,31 @@ export const sellshares = newEndpoint({}, async (req, auth) => { throw new APIError(400, 'Trading is closed.') const prevLoanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0) + const betsByOutcome = groupBy(userBets, (bet) => bet.outcome) + const sharesByOutcome = mapValues(betsByOutcome, (bets) => + sumBy(bets, (b) => b.shares) + ) - const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) - const maxShares = sumBy(outcomeBets, (bet) => bet.shares) + let chosenOutcome: 'YES' | 'NO' + if (outcome != null) { + chosenOutcome = outcome + } else { + const nonzeroShares = Object.entries(sharesByOutcome).filter( + ([_k, v]) => !floatingEqual(0, v) + ) + if (nonzeroShares.length == 0) { + throw new APIError(400, "You don't own any shares in this market.") + } + if (nonzeroShares.length > 1) { + throw new APIError( + 400, + `You own multiple kinds of shares, but did not specify which to sell.` + ) + } + chosenOutcome = nonzeroShares[0][0] as 'YES' | 'NO' + } + + const maxShares = sharesByOutcome[chosenOutcome] const sharesToSell = shares ?? maxShares if (!floatingLesserEqual(sharesToSell, maxShares)) @@ -63,7 +85,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo( soldShares, - outcome, + chosenOutcome, contract, prevLoanAmount, unfilledBets From cfeb50826c4d181fa3e8e1b6b2a0edab5aed8f9d Mon Sep 17 00:00:00 2001 From: Keri Warr <keri@warr.ca> Date: Tue, 2 Aug 2022 03:06:23 -0400 Subject: [PATCH 386/519] Add endpoint for reading currently authenticated user (#710) --- docs/docs/api.md | 4 ++++ functions/src/get-current-user.ts | 18 ++++++++++++++++++ functions/src/index.ts | 3 +++ functions/src/serve.ts | 2 ++ web/lib/firebase/api.ts | 4 ++++ web/pages/api/v0/me.ts | 25 +++++++++++++++++++++++++ 6 files changed, 56 insertions(+) create mode 100644 functions/src/get-current-user.ts create mode 100644 web/pages/api/v0/me.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index 1c73fc05..59dd4768 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -46,6 +46,10 @@ Gets a user by their unique ID. Many other API endpoints return this as the `use Requires no authorization. +### GET /v0/me + +Returns the authenticated user. + ### `GET /v0/groups` Gets all groups, in no particular order. diff --git a/functions/src/get-current-user.ts b/functions/src/get-current-user.ts new file mode 100644 index 00000000..409f897f --- /dev/null +++ b/functions/src/get-current-user.ts @@ -0,0 +1,18 @@ +import { User } from 'common/user' +import * as admin from 'firebase-admin' +import { newEndpoint, APIError } from './api' + +export const getcurrentuser = newEndpoint( + { method: 'GET' }, + async (_req, auth) => { + const userDoc = firestore.doc(`users/${auth.uid}`) + const [userSnap] = await firestore.getAll(userDoc) + if (!userSnap.exists) throw new APIError(400, 'User not found.') + + const user = userSnap.data() as User + + return user + } +) + +const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 239806de..b8f3eedb 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -44,6 +44,7 @@ import { creategroup } from './create-group' import { resolvemarket } from './resolve-market' import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' +import { getcurrentuser } from './get-current-user' const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { return onRequest(opts, handler as any) @@ -66,6 +67,7 @@ const resolveMarketFunction = toCloudFunction(resolvemarket) const unsubscribeFunction = toCloudFunction(unsubscribe) const stripeWebhookFunction = toCloudFunction(stripewebhook) const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) +const getCurrentUserFunction = toCloudFunction(getcurrentuser) export { healthFunction as health, @@ -86,4 +88,5 @@ export { unsubscribeFunction as unsubscribe, stripeWebhookFunction as stripewebhook, createCheckoutSessionFunction as createcheckoutsession, + getCurrentUserFunction as getcurrentuser, } diff --git a/functions/src/serve.ts b/functions/src/serve.ts index 77282951..0064b69f 100644 --- a/functions/src/serve.ts +++ b/functions/src/serve.ts @@ -25,6 +25,7 @@ import { creategroup } from './create-group' import { resolvemarket } from './resolve-market' import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' +import { getcurrentuser } from './get-current-user' type Middleware = (req: Request, res: Response, next: NextFunction) => void const app = express() @@ -62,6 +63,7 @@ addJsonEndpointRoute('/creategroup', creategroup) addJsonEndpointRoute('/resolvemarket', resolvemarket) addJsonEndpointRoute('/unsubscribe', unsubscribe) addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession) +addJsonEndpointRoute('/getcurrentuser', getcurrentuser) addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) app.listen(PORT) diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts index 27d6caa3..87d94dce 100644 --- a/web/lib/firebase/api.ts +++ b/web/lib/firebase/api.ts @@ -80,3 +80,7 @@ export function claimManalink(params: any) { export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } + +export function getCurrentUser(params: any) { + return call(getFunctionUrl('getcurrentuser'), 'GET', params) +} diff --git a/web/pages/api/v0/me.ts b/web/pages/api/v0/me.ts new file mode 100644 index 00000000..da7edb10 --- /dev/null +++ b/web/pages/api/v0/me.ts @@ -0,0 +1,25 @@ +import { User } from 'common/user' +import { NextApiRequest, NextApiResponse } from 'next' +import { fetchBackend } from 'web/lib/api/proxy' +import { LiteUser, ApiError, toLiteUser } from './_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<LiteUser | ApiError> +) { + try { + const backendRes = await fetchBackend(req, 'getcurrentuser') + + const user = (await backendRes.json()) as User + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + res.status(200).json(toLiteUser(user)) + return + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ error: 'Error communicating with backend.' }) + } +} From b83caf4dd9567388325d79e8dff33f3e3e296dca Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 2 Aug 2022 00:21:51 -0700 Subject: [PATCH 387/519] Just make `me` endpoint forward the backend response --- web/pages/api/v0/me.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/web/pages/api/v0/me.ts b/web/pages/api/v0/me.ts index da7edb10..7ee3cc3f 100644 --- a/web/pages/api/v0/me.ts +++ b/web/pages/api/v0/me.ts @@ -1,7 +1,6 @@ -import { User } from 'common/user' import { NextApiRequest, NextApiResponse } from 'next' -import { fetchBackend } from 'web/lib/api/proxy' -import { LiteUser, ApiError, toLiteUser } from './_types' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' +import { LiteUser, ApiError } from './_types' export default async function handler( req: NextApiRequest, @@ -9,15 +8,7 @@ export default async function handler( ) { try { const backendRes = await fetchBackend(req, 'getcurrentuser') - - const user = (await backendRes.json()) as User - if (!user) { - res.status(404).json({ error: 'User not found' }) - return - } - res.setHeader('Cache-Control', 'no-cache') - res.status(200).json(toLiteUser(user)) - return + await forwardResponse(res, backendRes) } catch (err) { console.error('Error talking to cloud function: ', err) res.status(500).json({ error: 'Error communicating with backend.' }) From 53d89fa4aca0f05ed8cd3fb7044f6365d9cf07f6 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 2 Aug 2022 14:59:47 -0700 Subject: [PATCH 388/519] Show the value to 2 decimal places on hover --- web/components/contract/contract-card.tsx | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index e418178c..b1ab7731 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -30,7 +30,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract' import { useUser } from 'web/hooks/use-user' import { track } from '@amplitude/analytics-browser' import { trackCallback } from 'web/lib/service/analytics' -import { formatNumericProbability } from 'common/pseudo-numeric' +import { formatNumericProbability, getMappedValue } from 'common/pseudo-numeric' export function ContractCard(props: { contract: Contract @@ -317,6 +317,12 @@ export function PseudoNumericResolutionOrExpectation(props: { const { resolution, resolutionValue, resolutionProbability } = contract const textColor = `text-blue-400` + const value = resolution + ? resolutionValue + ? resolutionValue + : getMappedValue(contract)(resolutionProbability ?? 0) + : getMappedValue(contract)(getProbability(contract)) + return ( <Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}> {resolution ? ( @@ -326,20 +332,21 @@ export function PseudoNumericResolutionOrExpectation(props: { {resolution === 'CANCEL' ? ( <CancelLabel /> ) : ( - <div className="text-blue-400"> - {resolutionValue - ? formatLargeNumber(resolutionValue) - : formatNumericProbability( - resolutionProbability ?? 0, - contract - )} + <div + className={clsx('tooltip', textColor)} + data-tip={value.toFixed(2)} + > + {formatLargeNumber(value)} </div> )} </> ) : ( <> - <div className={clsx('text-3xl', textColor)}> - {formatNumericProbability(getProbability(contract), contract)} + <div + className={clsx('tooltip text-3xl', textColor)} + data-tip={value.toFixed(2)} + > + {formatLargeNumber(value)} </div> <div className={clsx('text-base', textColor)}>expected</div> </> From 164d9ef079398280bbf5c9ed81fd012504ab15a9 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 2 Aug 2022 15:08:07 -0700 Subject: [PATCH 389/519] manalinks: mention referral bonus --- web/pages/links.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 0f91d70c..be3015ee 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -1,4 +1,9 @@ import { useState } from 'react' + +import dayjs from 'dayjs' +import customParseFormat from 'dayjs/plugin/customParseFormat' +dayjs.extend(customParseFormat) + import { formatMoney } from 'common/util/format' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' @@ -16,12 +21,10 @@ import { UserLink } from 'web/components/user-page' import { CreateLinksButton } from 'web/components/manalinks/create-links-button' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' -import dayjs from 'dayjs' -import customParseFormat from 'dayjs/plugin/customParseFormat' import { ManalinkCardFromView } from 'web/components/manalink-card' import { Pagination } from 'web/components/pagination' import { Manalink } from 'common/manalink' -dayjs.extend(customParseFormat) +import { REFERRAL_AMOUNT } from 'common/user' const LINKS_PER_PAGE = 24 export const getServerSideProps = redirectIfLoggedOut('/') @@ -64,8 +67,10 @@ export default function LinkPage() { )} </Row> <p> - You can use manalinks to send mana to other people, even if they - don't yet have a Manifold account. + You can use manalinks to send mana (M$) to other people, even if they + don't yet have a Manifold account. Manalinks are also eligible + for the referral bonus. Invite a new user to Manifold and get M$ + {REFERRAL_AMOUNT} if they sign up! </p> <Subtitle text="Your Manalinks" /> <ManalinksDisplay From 96c08760530b47e70e77f564735e349d268df6a3 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 2 Aug 2022 15:09:55 -0700 Subject: [PATCH 390/519] manalinks: fix focus --- web/components/manalink-card.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index c8529609..b04fd0da 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -1,16 +1,18 @@ +import { useState } from 'react' import clsx from 'clsx' +import { QrcodeIcon } from '@heroicons/react/outline' +import { DotsHorizontalIcon } from '@heroicons/react/solid' + import { formatMoney } from 'common/util/format' import { fromNow } from 'web/lib/util/time' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Claim, Manalink } from 'common/manalink' -import { useState } from 'react' import { ShareIconButton } from './share-icon-button' -import { DotsHorizontalIcon } from '@heroicons/react/solid' import { contractDetailsButtonClassName } from './contract/contract-info-dialog' import { useUserById } from 'web/hooks/use-user' import getManalinkUrl from 'web/get-manalink-url' -import { QrcodeIcon } from '@heroicons/react/outline' + export type ManalinkInfo = { expiresTime: number | null maxUses: number | null @@ -133,12 +135,7 @@ export function ManalinkCardFromView(props: { <button onClick={() => (window.location.href = qrUrl)} - className={clsx( - contractDetailsButtonClassName, - showDetails - ? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600' - : '' - )} + className={clsx(contractDetailsButtonClassName)} > <QrcodeIcon className="h-6 w-6" /> </button> From 5e8b9711dc375f9968e59dce2009dfc04133eccc Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 2 Aug 2022 15:12:03 -0700 Subject: [PATCH 391/519] hide pagination if only one page --- web/components/pagination.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/components/pagination.tsx b/web/components/pagination.tsx index 3f4108bc..5f3d4da2 100644 --- a/web/components/pagination.tsx +++ b/web/components/pagination.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import { Spacer } from './layout/spacer' export function Pagination(props: { page: number @@ -23,6 +24,8 @@ export function Pagination(props: { const maxPage = Math.ceil(totalItems / itemsPerPage) - 1 + if (maxPage === 0) return <Spacer h={4} /> + return ( <nav className={clsx( From 6563082746bff0dc7445fed1ad5009ca87de9876 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 2 Aug 2022 15:21:39 -0700 Subject: [PATCH 392/519] move claim button --- web/pages/link/[slug].tsx | 64 ++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index af3f01a8..c7457f27 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -38,45 +38,47 @@ export default function ClaimPage() { <div className="mx-auto max-w-xl px-2"> <Row className="items-center justify-between"> <Title text={`Claim M$${manalink.amount} mana`} /> - <div className="my-auto"> - <Button - onClick={async () => { - setClaiming(true) - try { - if (user == null) { - await firebaseLogin() - setClaiming(false) - return - } - if (user?.id == manalink.fromId) { - throw new Error("You can't claim your own manalink.") - } - await claimManalink({ slug: manalink.slug }) - user && router.push(`/${user.username}?claimed-mana=yes`) - } catch (e) { - console.log(e) - const message = - e && e instanceof Object - ? e.toString() - : 'An error occurred.' - setError(message) - } - setClaiming(false) - }} - disabled={claiming} - size="lg" - > - {user ? 'Claim' : 'Login'} - </Button> - </div> + <div className="my-auto"></div> </Row> + <ManalinkCard info={info} /> + {error && ( <section className="my-5 text-red-500"> <p>Failed to claim manalink.</p> <p>{error}</p> </section> )} + + <Row className="items-center"> + <Button + onClick={async () => { + setClaiming(true) + try { + if (user == null) { + await firebaseLogin() + setClaiming(false) + return + } + if (user?.id == manalink.fromId) { + throw new Error("You can't claim your own manalink.") + } + await claimManalink({ slug: manalink.slug }) + user && router.push(`/${user.username}?claimed-mana=yes`) + } catch (e) { + console.log(e) + const message = + e && e instanceof Object ? e.toString() : 'An error occurred.' + setError(message) + } + setClaiming(false) + }} + disabled={claiming} + size="lg" + > + {user ? `Claim M$${manalink.amount}` : 'Login to claim'} + </Button> + </Row> </div> </> ) From f8a74aa4387423aac3f86c2074ceb3496282456d Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 2 Aug 2022 15:34:20 -0700 Subject: [PATCH 393/519] Allow admins to resolve any market (#711) --- functions/src/resolve-market.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 08778a41..cc07d4be 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -18,6 +18,7 @@ import { groupPayoutsByUser, Payout, } from '../../common/payouts' +import { isAdmin } from '../../common/envs/constants' import { removeUndefinedProps } from '../../common/util/object' import { LiquidityProvision } from '../../common/liquidity-provision' import { APIError, newEndpoint, validate } from './api' @@ -69,8 +70,6 @@ const opts = { secrets: ['MAILGUN_KEY'] } export const resolvemarket = newEndpoint(opts, async (req, auth) => { const { contractId } = validate(bodySchema, req.body) - const userId = auth.uid - const contractDoc = firestore.doc(`contracts/${contractId}`) const contractSnap = await contractDoc.get() if (!contractSnap.exists) @@ -83,7 +82,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => { req.body ) - if (creatorId !== userId) + if (creatorId !== auth.uid && !isAdmin(auth.uid)) throw new APIError(403, 'User is not creator of contract') if (contract.resolution) throw new APIError(400, 'Contract already resolved') From e700697423292c65c849368f4bbe7e2bc8222c25 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 2 Aug 2022 17:18:08 -0600 Subject: [PATCH 394/519] Fix group referrals not working --- web/hooks/use-save-referral.ts | 10 +++++++--- web/lib/firebase/users.ts | 15 +++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/web/hooks/use-save-referral.ts b/web/hooks/use-save-referral.ts index 788268b0..7772f9d2 100644 --- a/web/hooks/use-save-referral.ts +++ b/web/hooks/use-save-referral.ts @@ -18,10 +18,14 @@ export const useSaveReferral = ( referrer?: string } - const actualReferrer = referrer || options?.defaultReferrer + const referrerOrDefault = referrer || options?.defaultReferrer - if (!user && router.isReady && actualReferrer) { - writeReferralInfo(actualReferrer, options?.contractId, options?.groupId) + if (!user && router.isReady && referrerOrDefault) { + writeReferralInfo(referrerOrDefault, { + contractId: options?.contractId, + overwriteReferralUsername: referrer, + groupId: options?.groupId, + }) } }, [user, router, options]) } diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 4f618586..5e00affe 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -96,22 +96,25 @@ const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY' export function writeReferralInfo( defaultReferrerUsername: string, - contractId?: string, - referralUsername?: string, - groupId?: string + otherOptions?: { + contractId?: string + overwriteReferralUsername?: string + groupId?: string + } ) { const local = safeLocalStorage() const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) + const { contractId, overwriteReferralUsername, groupId } = otherOptions || {} // Write the first referral username we see. if (!cachedReferralUser) local?.setItem( CACHED_REFERRAL_USERNAME_KEY, - referralUsername || defaultReferrerUsername + overwriteReferralUsername || defaultReferrerUsername ) // If an explicit referral query is passed, overwrite the cached referral username. - if (referralUsername) - local?.setItem(CACHED_REFERRAL_USERNAME_KEY, referralUsername) + if (overwriteReferralUsername) + local?.setItem(CACHED_REFERRAL_USERNAME_KEY, overwriteReferralUsername) // Always write the most recent explicit group invite query value if (groupId) local?.setItem(CACHED_REFERRAL_GROUP_ID_KEY, groupId) From d45edb7887055780f0c67c36dc4507ea70410c17 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 2 Aug 2022 16:21:04 -0700 Subject: [PATCH 395/519] Add WagerWith.me and James' Bot to Awesome Manifold --- docs/docs/awesome-manifold.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/awesome-manifold.md b/docs/docs/awesome-manifold.md index 44167bcb..0871be52 100644 --- a/docs/docs/awesome-manifold.md +++ b/docs/docs/awesome-manifold.md @@ -10,6 +10,7 @@ A list of community-created projects built on, or related to, Manifold Markets. - [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government - [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold +- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$. ## API / Dev @@ -21,3 +22,4 @@ A list of community-created projects built on, or related to, Manifold Markets. ## Bots - [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon +- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets From c24b4e77a8d36310fcf973a975d0bfc6eca3433f Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 2 Aug 2022 17:24:59 -0600 Subject: [PATCH 396/519] Lint --- web/components/contract/contract-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index b1ab7731..4ef90884 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -30,7 +30,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract' import { useUser } from 'web/hooks/use-user' import { track } from '@amplitude/analytics-browser' import { trackCallback } from 'web/lib/service/analytics' -import { formatNumericProbability, getMappedValue } from 'common/pseudo-numeric' +import { getMappedValue } from 'common/pseudo-numeric' export function ContractCard(props: { contract: Contract From 3c9108de0dc588cdf60042012c8af2ea30eacd7d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 2 Aug 2022 17:01:30 -0700 Subject: [PATCH 397/519] Document creating a limit order in API --- docs/docs/api.md | 87 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/docs/docs/api.md b/docs/docs/api.md index 59dd4768..48564cb3 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -503,6 +503,20 @@ Parameters: answer. For numeric markets, this is a string representing the target bucket, and an additional `value` parameter is required which is a number representing the target value. (Bet on numeric markets at your own peril.) +- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing + the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the + probability percentage). + The bet will execute immediately in the direction of `outcome`, but not beyond this + specified limit. If not all the bet is filled, the bet will remain as an open offer + that can later be matched against an opposite direction bet. + - For example, if the current market probability is `50%`: + - A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market + probability moves down to `40%` and someone bets `M$15` of `NO` to match your + bet odds. + - A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely + depending on current unfilled limit bets and the AMM's liquidity. Any remaining + portion of the bet not filled would remain to be matched against in the future. + - An unfilled limit order bet can be cancelled using the cancel API. Example request: @@ -639,7 +653,7 @@ Requires no authorization. - Example request ``` - https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-california-abolish-daylight-sa + https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord ``` - Response type: A `Bet[]`. @@ -647,31 +661,60 @@ Requires no authorization. ```json [ + // Limit bet, partially filled. { - "probAfter": 0.44418877319153904, - "shares": -645.8346334931828, + "isFilled": false, + "amount": 15.596681605353808, + "userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2", + "contractId": "Tz5dA01GkK5QKiQfZeDL", + "probBefore": 0.5730753474948571, + "isCancelled": false, "outcome": "YES", - "contractId": "tgB1XmvFXZNhjr3xMNLp", - "sale": { - "betId": "RcOtarI3d1DUUTjiE0rx", - "amount": 474.9999999999998 - }, - "createdTime": 1644602886293, - "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", - "probBefore": 0.7229189477449224, - "id": "x9eNmCaqQeXW8AgJ8Zmp", - "amount": -499.9999999999998 + "fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 }, + "shares": 31.193363210707616, + "limitProb": 0.5, + "id": "yXB8lVbs86TKkhWA1FVi", + "loanAmount": 0, + "orderAmount": 100, + "probAfter": 0.5730753474948571, + "createdTime": 1659482775970, + "fills": [ + { + "timestamp": 1659483249648, + "matchedBetId": "MfrMd5HTiGASDXzqibr7", + "amount": 15.596681605353808, + "shares": 31.193363210707616 + } + ] }, + // Normal bet (no limitProb specified). { - "probAfter": 0.9901970375647697, - "contractId": "zdeaYVAfHlo9jKzWh57J", - "outcome": "YES", - "amount": 1, - "id": "8PqxKYwXCcLYoXy2m2Nm", - "shares": 1.0049875638533763, - "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", - "probBefore": 0.9900000000000001, - "createdTime": 1644705818872 + "shares": 17.350459904608414, + "probBefore": 0.5304358279113885, + "isFilled": true, + "probAfter": 0.5730753474948571, + "userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2", + "amount": 10, + "contractId": "Tz5dA01GkK5QKiQfZeDL", + "id": "1LPJHNz5oAX4K6YtJlP1", + "fees": { + "platformFee": 0, + "liquidityFee": 0, + "creatorFee": 0.4251333951457593 + }, + "isCancelled": false, + "loanAmount": 0, + "orderAmount": 10, + "fills": [ + { + "amount": 10, + "matchedBetId": null, + "shares": 17.350459904608414, + "timestamp": 1659482757271 + } + ], + "createdTime": 1659482757271, + "outcome": "YES" } ] ``` From b5d8acfef3b0ee1c84e4f4cb45efae057ea49406 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 2 Aug 2022 17:31:49 -0700 Subject: [PATCH 398/519] Switch profit in bets tab to match user page --- web/components/bets-list.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 25cd00ad..648c2c26 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -157,9 +157,7 @@ export function BetsList(props: { (c) => contractsMetrics[c.id].netPayout ) - const totalPortfolio = currentNetInvestment + user.balance - - const totalPnl = totalPortfolio - user.totalDeposits + const totalPnl = user.profitCached.allTime const totalProfitPercent = (totalPnl / user.totalDeposits) * 100 const investedProfitPercent = ((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100 From 280308b625f3d5abefd10c859209bcdc8b71b918 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 2 Aug 2022 17:40:34 -0700 Subject: [PATCH 399/519] Show # of bets equal to visible bets --- web/components/contract/contract-tabs.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index fbf056e3..5aee7899 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -23,6 +23,9 @@ export function ContractTabs(props: { const { outcomeType } = contract const userBets = user && bets.filter((bet) => bet.userId === user.id) + const visibleBets = bets.filter( + (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 + ) // Load comments here, so the badge count will be correct const updatedComments = useComments(contract.id) @@ -99,7 +102,7 @@ export function ContractTabs(props: { content: commentActivity, badge: `${comments.length}`, }, - { title: 'Bets', content: betActivity, badge: `${bets.length}` }, + { title: 'Bets', content: betActivity, badge: `${visibleBets.length}` }, ...(!user || !userBets?.length ? [] : [{ title: 'Your bets', content: yourTrades }]), From a761f8c65ec3623430bfcecdb45885a4c6ff61be Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 3 Aug 2022 14:17:45 -0700 Subject: [PATCH 400/519] Hide pills while searching --- web/components/contract-search.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 6438a3aa..c1e63175 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -107,7 +107,7 @@ export function ContractSearch(props: { const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' ) - const pillsEnabled = !additionalFilter + const pillsEnabled = !additionalFilter && !query const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) @@ -126,7 +126,7 @@ export function ContractSearch(props: { ? `groupLinks.slug:${additionalFilter.groupSlug}` : '', ] - let facetFilters = query + const facetFilters = query ? additionalFilters : [ ...additionalFilters, @@ -154,8 +154,6 @@ export function ContractSearch(props: { `uniqueBettorIds:${user.id}` : '', ].filter((f) => f) - // Hack to make Algolia work. - facetFilters = ['', ...facetFilters] const numericFilters = query ? [] From a7d80d62cb39482904dd2e7bd860f14d41a0abfb Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 3 Aug 2022 14:30:59 -0700 Subject: [PATCH 401/519] Don't show cancel button for other people's limit orders --- web/components/bets-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 648c2c26..18349597 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -352,7 +352,7 @@ function ContractBets(props: { <LimitOrderTable contract={contract} limitBets={limitBets} - isYou={true} + isYou={isYourBets} /> </div> )} From 82419d0b9272a57dd107cc4ee6949ef1d1d540c0 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 15:38:35 -0600 Subject: [PATCH 402/519] Groups chat ux (#713) * Add in group chat bubble * Show chat bubble on nav with unseen notifs * Spacing * More spacing * Remove chat tab * Show chat on help/welcome/updates/features groups * Cleanup * Scroll with updated height --- web/components/groups/group-chat.tsx | 136 ++++++++++++++++++++++++--- web/components/nav/sidebar.tsx | 63 ++++++------- web/lib/firebase/comments.ts | 9 ++ web/pages/group/[...slugs]/index.tsx | 54 +++++------ 4 files changed, 184 insertions(+), 78 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2cf2d73d..b9b2b3ff 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -1,6 +1,6 @@ import { Row } from 'web/components/layout/row' import { Col } from 'web/components/layout/col' -import { User } from 'common/user' +import { PrivateUser, User } from 'common/user' import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' @@ -23,6 +23,9 @@ import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' +import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' +import { ChatIcon, ChevronDownIcon } from '@heroicons/react/outline' +import { setNotificationsAsSeen } from 'web/pages/notifications' export function GroupChat(props: { messages: Comment[] @@ -70,9 +73,10 @@ export function GroupChat(props: { }, [scrollToMessageRef]) useEffect(() => { - if (!isSubmitting) - scrollToBottomRef?.scrollTo({ top: scrollToBottomRef?.scrollHeight || 0 }) - }, [scrollToBottomRef, isSubmitting]) + if (scrollToBottomRef) + scrollToBottomRef.scrollTo({ top: scrollToBottomRef.scrollHeight || 0 }) + // Must also listen to groupedMessages as they update the height of the messaging window + }, [scrollToBottomRef, groupedMessages]) useEffect(() => { const elementInUrl = router.asPath.split('#')[1] @@ -81,6 +85,10 @@ export function GroupChat(props: { } }, [messages, router.asPath]) + useEffect(() => { + if (inputRef) inputRef.focus() + }, [inputRef]) + function onReplyClick(comment: Comment) { setReplyToUsername(comment.userUsername) } @@ -98,18 +106,13 @@ export function GroupChat(props: { setReplyToUsername('') inputRef?.focus() } - function focusInput() { - inputRef?.focus() - } const { width, height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) // Subtract bottom bar when it's showing (less than lg screen) const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 const remainingHeight = - (height ?? window.innerHeight) - - (containerRef?.offsetTop ?? 0) - - bottomBarHeight + (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight return ( <Col ref={setContainerRef} style={{ height: remainingHeight }}> @@ -140,7 +143,7 @@ export function GroupChat(props: { No messages yet. Why not{isMember ? ` ` : ' join and '} <button className={'cursor-pointer font-bold text-gray-700'} - onClick={() => focusInput()} + onClick={() => inputRef?.focus()} > add one? </button> @@ -175,6 +178,117 @@ export function GroupChat(props: { ) } +export function GroupChatInBubble(props: { + messages: Comment[] + user: User | null | undefined + privateUser: PrivateUser | null | undefined + group: Group + tips: CommentTipMap +}) { + const { messages, user, group, tips, privateUser } = props + const [shouldShowChat, setShouldShowChat] = useState(false) + const router = useRouter() + + useEffect(() => { + const groupsWithChatEmphasis = [ + 'welcome', + 'bugs', + 'manifold-features-25bad7c7792e', + 'updates', + ] + if ( + router.asPath.includes('/chat') || + groupsWithChatEmphasis.includes( + router.asPath.split('/group/')[1].split('/')[0] + ) + ) { + setShouldShowChat(true) + } + // Leave chat open between groups if user is using chat? + else { + setShouldShowChat(false) + } + }, [router.asPath]) + + return ( + <Col + className={clsx( + 'fixed right-0 bottom-[20px] h-screen w-full sm:bottom-[20px] sm:right-20 sm:w-2/3 md:w-1/2 lg:right-24 lg:w-1/3 xl:right-32 xl:w-1/4', + shouldShowChat ? 'z-10 bg-white p-2' : '' + )} + > + {shouldShowChat && ( + <GroupChat messages={messages} user={user} group={group} tips={tips} /> + )} + <button + type="button" + className={clsx( + 'fixed right-1 inline-flex items-center rounded-full border md:right-2 lg:right-5 xl:right-10' + + ' border-transparent p-3 text-white shadow-sm lg:p-4' + + ' focus:outline-none focus:ring-2 focus:ring-offset-2 ' + + ' bottom-[70px] ', + shouldShowChat + ? 'bottom-auto top-2 bg-gray-600 hover:bg-gray-400 focus:ring-gray-500 sm:bottom-[70px] sm:top-auto ' + : ' bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500' + )} + onClick={() => { + // router.push('/chat') + setShouldShowChat(!shouldShowChat) + track('mobile group chat button') + }} + > + {!shouldShowChat ? ( + <ChatIcon className="h-10 w-10" aria-hidden="true" /> + ) : ( + <ChevronDownIcon className={'h-10 w-10'} aria-hidden={'true'} /> + )} + {privateUser && ( + <GroupChatNotificationsIcon + group={group} + privateUser={privateUser} + shouldSetAsSeen={shouldShowChat} + /> + )} + </button> + </Col> + ) +} + +function GroupChatNotificationsIcon(props: { + group: Group + privateUser: PrivateUser + shouldSetAsSeen: boolean +}) { + const { privateUser, group, shouldSetAsSeen } = props + const preferredNotificationsForThisGroup = useUnseenPreferredNotifications( + privateUser, + { + customHref: `/group/${group.slug}`, + } + ) + useEffect(() => { + preferredNotificationsForThisGroup.forEach((notification) => { + if ( + (shouldSetAsSeen && notification.isSeenOnHref?.includes('chat')) || + // old style chat notif that simply ended with the group slug + notification.isSeenOnHref?.endsWith(group.slug) + ) { + setNotificationsAsSeen([notification]) + } + }) + }, [group.slug, preferredNotificationsForThisGroup, shouldSetAsSeen]) + + return ( + <div + className={ + preferredNotificationsForThisGroup.length > 0 && !shouldSetAsSeen + ? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500' + : 'hidden' + } + ></div> + ) +} + const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined comment: Comment diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 581dd5fa..de9fd1ba 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo' import { MenuButton } from './menu' import { ProfileSummary } from './profile-menu' import NotificationsIcon from 'web/components/notifications-icon' -import React, { useEffect, useState } from 'react' +import React, { useMemo, useState } from 'react' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' @@ -27,7 +27,6 @@ import { trackCallback, withTracking } from 'web/lib/service/analytics' import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' -import { setNotificationsAsSeen } from 'web/pages/notifications' import { PrivateUser } from 'common/user' import { useWindowSize } from 'web/hooks/use-window-size' @@ -216,7 +215,7 @@ export default function Sidebar(props: { className?: string }) { ) ?? [] ).map((group: Group) => ({ name: group.name, - href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, + href: `${groupPath(group.slug)}`, })) return ( @@ -294,30 +293,22 @@ function GroupsList(props: { memberItems.length > 0 ? memberItems.length : undefined ) - // Set notification as seen if our current page is equal to the isSeenOnHref property - useEffect(() => { - const currentPageWithoutQuery = currentPage.split('?')[0] - const currentPageGroupSlug = currentPageWithoutQuery.split('/')[2] - preferredNotifications.forEach((notification) => { - if ( - notification.isSeenOnHref === currentPage || - // Old chat style group chat notif was just /group/slug - (notification.isSeenOnHref && - currentPageWithoutQuery.includes(notification.isSeenOnHref)) || - // They're on the home page, so if they've a chat notif, they're seeing the chat - (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && - currentPageWithoutQuery.endsWith(currentPageGroupSlug)) - ) { - setNotificationsAsSeen([notification]) - } - }) - }, [currentPage, preferredNotifications]) - const { height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) const remainingHeight = (height ?? window.innerHeight) - (containerRef?.offsetTop ?? 0) + const notifIsForThisItem = useMemo( + () => (itemHref: string) => + preferredNotifications.some( + (n) => + !n.isSeen && + (n.isSeenOnHref === itemHref || + n.isSeenOnHref?.replace('/chat', '') === itemHref) + ), + [preferredNotifications] + ) + return ( <> <SidebarItem @@ -331,21 +322,23 @@ function GroupsList(props: { ref={setContainerRef} > {memberItems.map((item) => ( - <a + <Link + href={ + item.href + + (notifIsForThisItem(item.href) ? '/' + GROUP_CHAT_SLUG : '') + } key={item.href} - href={item.href} - className={clsx( - 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', - preferredNotifications.some( - (n) => - !n.isSeen && - (n.isSeenOnHref === item.href || - n.isSeenOnHref === item.href.replace('/chat', '')) - ) && 'font-bold' - )} > - <span className="truncate">{item.name}</span> - </a> + <span + className={clsx( + 'cursor-pointer truncate', + 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', + notifIsForThisItem(item.href) && 'font-bold' + )} + > + {item.name} + </span> + </Link> ))} </div> </> diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index 3093f764..5775a2bb 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -81,6 +81,7 @@ export async function createCommentOnGroup( function getCommentsCollection(contractId: string) { return collection(db, 'contracts', contractId, 'comments') } + function getCommentsOnGroupCollection(groupId: string) { return collection(db, 'groups', groupId, 'comments') } @@ -91,6 +92,14 @@ export async function listAllComments(contractId: string) { return comments } +export async function listAllCommentsOnGroup(groupId: string) { + const comments = await getValues<Comment>( + getCommentsOnGroupCollection(groupId) + ) + comments.sort((c1, c2) => c1.createdTime - c2.createdTime) + return comments +} + export function listenForCommentsOnContract( contractId: string, setComments: (comments: Comment[]) => void diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 5c52c7dc..642a2afd 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -16,7 +16,7 @@ import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' -import { useUser } from 'web/hooks/use-user' +import { usePrivateUser, useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' @@ -30,7 +30,7 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { CreateQuestionButton } from 'web/components/create-question-button' import React, { useState } from 'react' -import { GroupChat } from 'web/components/groups/group-chat' +import { GroupChatInBubble } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' @@ -45,11 +45,12 @@ import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { searchInAny } from 'common/util/parse' -import { useWindowSize } from 'web/hooks/use-window-size' import { CopyLinkButton } from 'web/components/copy-link-button' import { ENV_CONFIG } from 'common/envs/constants' import { useSaveReferral } from 'web/hooks/use-save-referral' import { Button } from 'web/components/button' +import { listAllCommentsOnGroup } from 'web/lib/firebase/comments' +import { Comment } from 'common/comment' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -65,6 +66,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const bets = await Promise.all( contracts.map((contract: Contract) => listAllBets(contract.id)) ) + const messages = group && (await listAllCommentsOnGroup(group.id)) const creatorScores = scoreCreators(contracts) const traderScores = scoreTraders(contracts, bets) @@ -86,6 +88,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { topTraders, creatorScores, topCreators, + messages, }, revalidate: 60, // regenerate after a minute @@ -123,6 +126,7 @@ export default function GroupPage(props: { topTraders: User[] creatorScores: { [userId: string]: number } topCreators: User[] + messages: Comment[] }) { props = usePropz(props, getStaticPropz) ?? { group: null, @@ -132,6 +136,7 @@ export default function GroupPage(props: { topTraders: [], creatorScores: {}, topCreators: [], + messages: [], } const { creator, @@ -149,19 +154,18 @@ export default function GroupPage(props: { const group = useGroup(props.group?.id) ?? props.group const tips = useTipTxns({ groupId: group?.id }) - const messages = useCommentsOnGroup(group?.id) + const messages = useCommentsOnGroup(group?.id) ?? props.messages const user = useUser() + const privateUser = usePrivateUser(user?.id) useSaveReferral(user, { defaultReferrer: creator.username, groupId: group?.id, }) - const { width } = useWindowSize() const chatDisabled = !group || group.chatDisabled - const showChatSidebar = !chatDisabled && (width ?? 1280) >= 1280 - const showChatTab = !chatDisabled && !showChatSidebar + const showChatBubble = !chatDisabled if (group === null || !groupSubpages.includes(page) || slugs[2]) { return <Custom404 /> @@ -195,16 +199,6 @@ export default function GroupPage(props: { </Col> ) - const chatTab = ( - <Col className=""> - {messages ? ( - <GroupChat messages={messages} user={user} group={group} tips={tips} /> - ) : ( - <LoadingIndicator /> - )} - </Col> - ) - const questionsTab = ( <ContractSearch querySortOptions={{ @@ -217,15 +211,6 @@ export default function GroupPage(props: { ) const tabs = [ - ...(!showChatTab - ? [] - : [ - { - title: 'Chat', - content: chatTab, - href: groupPath(group.slug, GROUP_CHAT_SLUG), - }, - ]), { title: 'Markets', content: questionsTab, @@ -246,17 +231,13 @@ export default function GroupPage(props: { const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) return ( - <Page - rightSidebar={showChatSidebar ? chatTab : undefined} - rightSidebarClassName={showChatSidebar ? '!top-0' : ''} - className={showChatSidebar ? '!max-w-7xl !pb-0' : ''} - > + <Page> <SEO title={group.name} description={`Created by ${creator.name}. ${group.about}`} url={groupPath(group.slug)} /> - <Col className="px-3"> + <Col className="relative px-3"> <Row className={'items-center justify-between gap-4'}> <div className={'sm:mb-1'}> <div @@ -283,6 +264,15 @@ export default function GroupPage(props: { defaultIndex={tabIndex > 0 ? tabIndex : 0} tabs={tabs} /> + {showChatBubble && ( + <GroupChatInBubble + group={group} + user={user} + privateUser={privateUser} + tips={tips} + messages={messages} + /> + )} </Page> ) } From aa3101baa9d63f53fb3f583ab0a30005cba3ee2c Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 16:10:02 -0600 Subject: [PATCH 403/519] Fix group chat padding --- web/components/groups/group-chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index b9b2b3ff..741e55bd 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -213,7 +213,7 @@ export function GroupChatInBubble(props: { return ( <Col className={clsx( - 'fixed right-0 bottom-[20px] h-screen w-full sm:bottom-[20px] sm:right-20 sm:w-2/3 md:w-1/2 lg:right-24 lg:w-1/3 xl:right-32 xl:w-1/4', + 'fixed right-0 bottom-[0px] h-screen w-full sm:bottom-[20px] sm:right-20 sm:w-2/3 md:w-1/2 lg:right-24 lg:w-1/3 xl:right-32 xl:w-1/4', shouldShowChat ? 'z-10 bg-white p-2' : '' )} > From fab83cfc33f8cfcefb0ab72eccfcfe9dbf12d758 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 16:16:46 -0600 Subject: [PATCH 404/519] Don't auotfocus on mobile --- web/components/groups/group-chat.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 741e55bd..2b5bd6e1 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -47,6 +47,13 @@ export function GroupChat(props: { const router = useRouter() const isMember = user && group.memberIds.includes(user?.id) + const { width, height } = useWindowSize() + const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) + // Subtract bottom bar when it's showing (less than lg screen) + const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 + const remainingHeight = + (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight + useMemo(() => { // Group messages with createdTime within 2 minutes of each other. const tempMessages = [] @@ -86,8 +93,9 @@ export function GroupChat(props: { }, [messages, router.asPath]) useEffect(() => { - if (inputRef) inputRef.focus() - }, [inputRef]) + // is mobile? + if (inputRef && width && width > 720) inputRef.focus() + }, [inputRef, width]) function onReplyClick(comment: Comment) { setReplyToUsername(comment.userUsername) @@ -107,13 +115,6 @@ export function GroupChat(props: { inputRef?.focus() } - const { width, height } = useWindowSize() - const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) - // Subtract bottom bar when it's showing (less than lg screen) - const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 - const remainingHeight = - (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight - return ( <Col ref={setContainerRef} style={{ height: remainingHeight }}> <Col From 756115ba54e4b206758628e637accead0bb0cdd4 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 16:30:05 -0600 Subject: [PATCH 405/519] Link tags aren't hiding sidebar on click --- web/components/nav/sidebar.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index de9fd1ba..a051faed 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -322,23 +322,21 @@ function GroupsList(props: { ref={setContainerRef} > {memberItems.map((item) => ( - <Link + <a href={ item.href + (notifIsForThisItem(item.href) ? '/' + GROUP_CHAT_SLUG : '') } - key={item.href} + key={item.name} + onClick={trackCallback('sidebar: ' + item.name)} + className={clsx( + 'cursor-pointer truncate', + 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', + notifIsForThisItem(item.href) && 'font-bold' + )} > - <span - className={clsx( - 'cursor-pointer truncate', - 'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900', - notifIsForThisItem(item.href) && 'font-bold' - )} - > - {item.name} - </span> - </Link> + {item.name} + </a> ))} </div> </> From b4c6b99e091fdbb84be0a85192a975a5c9d35732 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 16:38:00 -0600 Subject: [PATCH 406/519] Remove bottom bar height correction --- web/components/groups/group-chat.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2b5bd6e1..dc350cc0 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -50,9 +50,8 @@ export function GroupChat(props: { const { width, height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) // Subtract bottom bar when it's showing (less than lg screen) - const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 - const remainingHeight = - (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight + // const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 + const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) useMemo(() => { // Group messages with createdTime within 2 minutes of each other. From 5bc905b35828ad880a296c4164290c439f6afdce Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 16:42:51 -0600 Subject: [PATCH 407/519] Bottom padding works on mobile, broken on desktop :( --- web/components/groups/group-chat.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index dc350cc0..2b5bd6e1 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -50,8 +50,9 @@ export function GroupChat(props: { const { width, height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) // Subtract bottom bar when it's showing (less than lg screen) - // const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 - const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) + const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 + const remainingHeight = + (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight useMemo(() => { // Group messages with createdTime within 2 minutes of each other. From d83e103fab171215512df8df0bebf6db5a898929 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Wed, 3 Aug 2022 18:42:40 -0600 Subject: [PATCH 408/519] Ignore clicks when hidden --- web/components/groups/group-chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2b5bd6e1..91de63c6 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -214,8 +214,8 @@ export function GroupChatInBubble(props: { return ( <Col className={clsx( - 'fixed right-0 bottom-[0px] h-screen w-full sm:bottom-[20px] sm:right-20 sm:w-2/3 md:w-1/2 lg:right-24 lg:w-1/3 xl:right-32 xl:w-1/4', - shouldShowChat ? 'z-10 bg-white p-2' : '' + 'fixed right-0 bottom-[0px] h-1 w-full sm:bottom-[20px] sm:right-20 sm:w-2/3 md:w-1/2 lg:right-24 lg:w-1/3 xl:right-32 xl:w-1/4', + shouldShowChat ? 'p-2m z-10 h-screen bg-white' : '' )} > {shouldShowChat && ( From 7e46188107518aeb6e83a2e6e77bf8ec3965971c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 3 Aug 2022 22:21:22 -0700 Subject: [PATCH 409/519] Add lite market endpoint --- web/pages/api/v0/market/[id]/lite.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 web/pages/api/v0/market/[id]/lite.ts diff --git a/web/pages/api/v0/market/[id]/lite.ts b/web/pages/api/v0/market/[id]/lite.ts new file mode 100644 index 00000000..7688caa8 --- /dev/null +++ b/web/pages/api/v0/market/[id]/lite.ts @@ -0,0 +1,23 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getContractFromId } from 'web/lib/firebase/contracts' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { ApiError, toLiteMarket, LiteMarket } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<LiteMarket | ApiError> +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const contractId = id as string + + const contract = await getContractFromId(contractId) + + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(toLiteMarket(contract)) +} From 2d3ca47b52001ea8b668060a533351357e50d37d Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Fri, 5 Aug 2022 03:03:02 +0900 Subject: [PATCH 410/519] 500 mana email (#687) * Create 500-mana.html * Update 500-mana.html Fixed typos and links not working * Added "create a good market" guide added page creating-market.html For Stephen to set up condition (email 3 days after signing up) * Update 500-mana.html updated 500 Mana email (still need to make changes to create market guide) * email changes * sendOneWeekBonusEmail logic * add dayjs as dependency * don't use mailgun scheduling Co-authored-by: mantikoros <sgrugett@gmail.com> --- common/user.ts | 2 + functions/package.json | 1 + functions/src/create-user.ts | 5 +- functions/src/email-templates/500-mana.html | 267 ++++++- .../src/email-templates/creating-market.html | 738 ++++++++++++++++++ functions/src/emails.ts | 5 +- functions/src/index.ts | 19 + functions/src/mana-bonus-email.ts | 42 + functions/src/send-email.ts | 6 +- yarn.lock | 5 + 10 files changed, 1065 insertions(+), 25 deletions(-) create mode 100644 functions/src/email-templates/creating-market.html create mode 100644 functions/src/mana-bonus-email.ts diff --git a/common/user.ts b/common/user.ts index 78b76511..2aeb7122 100644 --- a/common/user.ts +++ b/common/user.ts @@ -47,6 +47,7 @@ export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000 // for sus users, i.e. multiple sign ups for same person export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10 export const REFERRAL_AMOUNT = ENV_CONFIG.referralBonus ?? 500 + export type PrivateUser = { id: string // same as User.id username: string // denormalized from User @@ -56,6 +57,7 @@ export type PrivateUser = { unsubscribedFromCommentEmails?: boolean unsubscribedFromAnswerEmails?: boolean unsubscribedFromGenericEmails?: boolean + manaBonusEmailSent?: boolean initialDeviceToken?: string initialIpAddress?: string apiKey?: string diff --git a/functions/package.json b/functions/package.json index b20a8fd0..b0d8e458 100644 --- a/functions/package.json +++ b/functions/package.json @@ -31,6 +31,7 @@ "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", + "dayjs": "1.11.4", "cors": "2.8.5", "express": "4.18.1", "firebase-admin": "10.0.0", diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 70e81055..c30e78c3 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -1,5 +1,7 @@ import * as admin from 'firebase-admin' import { z } from 'zod' +import { uniq } from 'lodash' + import { MANIFOLD_AVATAR_URL, MANIFOLD_USERNAME, @@ -24,7 +26,6 @@ import { import { track } from './analytics' import { APIError, newEndpoint, validate } from './api' import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group' -import { uniq } from 'lodash' import { DEV_HOUSE_LIQUIDITY_PROVIDER_ID, HOUSE_LIQUIDITY_PROVIDER_ID, @@ -93,8 +94,8 @@ export const createuser = newEndpoint(opts, async (req, auth) => { await firestore.collection('private-users').doc(auth.uid).create(privateUser) - await sendWelcomeEmail(user, privateUser) await addUserToDefaultGroups(user) + await sendWelcomeEmail(user, privateUser) await track(auth.uid, 'create user', { username }, { ip: req.ip }) return user diff --git a/functions/src/email-templates/500-mana.html b/functions/src/email-templates/500-mana.html index 5f0c450e..1ef9dbb7 100644 --- a/functions/src/email-templates/500-mana.html +++ b/functions/src/email-templates/500-mana.html @@ -1,12 +1,48 @@ -<iframe - style="border: 0px; width: 100%; height: 100%" - seamless - sandbox - srcdoc='<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title>7th Day Anniversary Gift! + + + + + + +
+

This e-mail has been sent to [[EMAIL_TO]], click here to unsubscribe.

' +> From eb4906cb97a961d5cd781f9556059cd41c2c0dd6 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 18 Jul 2022 08:18:40 -0600 Subject: [PATCH 215/519] Remove query from notif isSeen logic --- web/components/nav/sidebar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 821d1397..77af99e0 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -289,15 +289,17 @@ function GroupsList(props: { // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { - const currentPageGroupSlug = currentPage.split('/')[2] + const currentPageWithoutQuery = currentPage.split('?')[0] + const currentPageGroupSlug = currentPageWithoutQuery.split('/')[2] preferredNotifications.forEach((notification) => { if ( notification.isSeenOnHref === currentPage || - // Old chat style group chat notif ended just with the group slug - notification.isSeenOnHref?.includes(currentPageGroupSlug) || + // Old chat style group chat notif was just /group/slug + (notification.isSeenOnHref && + currentPageWithoutQuery.includes(notification.isSeenOnHref)) || // They're on the home page, so if they've a chat notif, they're seeing the chat (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && - currentPage.endsWith(currentPageGroupSlug)) + currentPageWithoutQuery.endsWith(currentPageGroupSlug)) ) { setNotificationsAsSeen([notification]) } From 229d270d2546e545e48e1a87d44c54c7539e08a0 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 18 Jul 2022 08:34:20 -0600 Subject: [PATCH 216/519] Set max width on avatars --- web/components/avatar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 53257deb..0436d61c 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -31,6 +31,7 @@ export function Avatar(props: { !noLink && 'cursor-pointer', className )} + style={{ maxWidth: `${s * 0.25}rem` }} src={avatarUrl} onClick={onClick} alt={username} From db537a97bafdeb6c4163903bc90fa9af66ecc567 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 18 Jul 2022 08:35:59 -0600 Subject: [PATCH 217/519] Allow click on group card avatar --- web/pages/groups.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 9e21c346..c87f801b 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -185,7 +185,7 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) {
Date: Mon, 18 Jul 2022 10:40:44 -0600 Subject: [PATCH 218/519] Referrals bug fix and attribute group --- common/notification.ts | 1 + common/user.ts | 1 + firestore.rules | 4 +- functions/src/create-notification.ts | 65 +++++++++++++++++++++------- functions/src/on-update-user.ts | 25 +++++++---- web/lib/firebase/groups.ts | 6 +-- web/lib/firebase/users.ts | 22 +++++----- web/pages/group/[...slugs]/index.tsx | 2 +- web/pages/notifications.tsx | 10 ++++- 9 files changed, 94 insertions(+), 42 deletions(-) diff --git a/common/notification.ts b/common/notification.ts index 63a44a52..5fd4236b 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -63,3 +63,4 @@ export type notification_reason_types = | 'on_group_you_are_member_of' | 'tip_received' | 'bet_fill' + | 'user_joined_from_your_group_invite' diff --git a/common/user.ts b/common/user.ts index 6eed3bdb..1995ce34 100644 --- a/common/user.ts +++ b/common/user.ts @@ -38,6 +38,7 @@ export type User = { referredByUserId?: string referredByContractId?: string + referredByGroupId?: string lastPingTime?: number } diff --git a/firestore.rules b/firestore.rules index 84c3e990..63df4d16 100644 --- a/firestore.rules +++ b/firestore.rules @@ -22,11 +22,11 @@ service cloud.firestore { allow read; allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId', 'lastPingTime']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime']); // User referral rules allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['referredByUserId']) + .hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId']) // only one referral allowed per user && !("referredByUserId" in resource.data) // user can't refer themselves diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 56493043..bf2dd28a 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -253,20 +253,6 @@ export const createNotification = async ( } } - const notifyUserReceivedReferralBonus = async ( - userToReasonTexts: user_to_reason_texts, - relatedUserId: string - ) => { - if (shouldGetNotification(relatedUserId, userToReasonTexts)) - userToReasonTexts[relatedUserId] = { - // If the referrer is the market creator, just tell them they joined to bet on their market - reason: - sourceContract?.creatorId === relatedUserId - ? 'user_joined_to_bet_on_your_market' - : 'you_referred_user', - } - } - const notifyContractCreatorOfUniqueBettorsBonus = async ( userToReasonTexts: user_to_reason_texts, userId: string @@ -284,8 +270,6 @@ export const createNotification = async ( } else if (sourceType === 'group' && relatedUserId) { if (sourceUpdateType === 'created') await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) - } else if (sourceType === 'user' && relatedUserId) { - await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. @@ -435,3 +419,52 @@ export const createGroupCommentNotification = async ( } await notificationRef.set(removeUndefinedProps(notification)) } + +export const createReferralNotification = async ( + toUser: User, + referredUser: User, + idempotencyKey: string, + bonusAmount: string, + referredByContract?: Contract, + referredByGroup?: Group +) => { + const notificationRef = firestore + .collection(`/users/${toUser.id}/notifications`) + .doc(idempotencyKey) + const notification: Notification = { + id: idempotencyKey, + userId: toUser.id, + reason: referredByGroup + ? 'user_joined_from_your_group_invite' + : referredByContract?.creatorId === toUser.id + ? 'user_joined_to_bet_on_your_market' + : 'you_referred_user', + createdTime: Date.now(), + isSeen: false, + sourceId: referredUser.id, + sourceType: 'user', + sourceUpdateType: 'updated', + sourceContractId: referredByContract?.id, + sourceUserName: referredUser.name, + sourceUserUsername: referredUser.username, + sourceUserAvatarUrl: referredUser.avatarUrl, + sourceText: bonusAmount, + // Only pass the contract referral details if they weren't referred to a group + sourceContractCreatorUsername: !referredByGroup + ? referredByContract?.creatorUsername + : undefined, + sourceContractTitle: !referredByGroup + ? referredByContract?.question + : undefined, + sourceContractSlug: !referredByGroup ? referredByContract?.slug : undefined, + sourceSlug: referredByGroup + ? groupPath(referredByGroup.slug) + : referredByContract?.slug, + sourceTitle: referredByGroup + ? referredByGroup.name + : referredByContract?.question, + } + await notificationRef.set(removeUndefinedProps(notification)) +} + +const groupPath = (groupSlug: string) => `/group/${groupSlug}` diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts index 0ace3c53..f5558730 100644 --- a/functions/src/on-update-user.ts +++ b/functions/src/on-update-user.ts @@ -2,11 +2,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { REFERRAL_AMOUNT, User } from '../../common/user' import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' -import { createNotification } from './create-notification' +import { createReferralNotification } from './create-notification' import { ReferralTxn } from '../../common/txn' import { Contract } from '../../common/contract' import { LimitBet } from 'common/bet' import { QuerySnapshot } from 'firebase-admin/firestore' +import { Group } from 'common/group' const firestore = admin.firestore() export const onUpdateUser = functions.firestore @@ -54,6 +55,17 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { } console.log(`referredByContract: ${referredByContract}`) + let referredByGroup: Group | undefined = undefined + if (user.referredByGroupId) { + const referredByGroupDoc = firestore.doc( + `groups/${user.referredByGroupId}` + ) + referredByGroup = await transaction + .get(referredByGroupDoc) + .then((snap) => snap.data() as Group) + } + console.log(`referredByGroup: ${referredByGroup}`) + const txns = ( await firestore .collection('txns') @@ -100,18 +112,13 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { totalDeposits: referredByUser.totalDeposits + REFERRAL_AMOUNT, }) - await createNotification( - user.id, - 'user', - 'updated', + await createReferralNotification( + referredByUser, user, eventId, txn.amount.toString(), referredByContract, - 'user', - referredByUser.id, - referredByContract?.slug, - referredByContract?.question + referredByGroup ) }) } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 6dfc1b88..8adb5606 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -89,11 +89,11 @@ export async function getGroupsWithContractId( setGroups(await getValues(q)) } -export async function addUserToGroupViaSlug(groupSlug: string, userId: string) { +export async function addUserToGroupViaId(groupId: string, userId: string) { // get group to get the member ids - const group = await getGroupBySlug(groupSlug) + const group = await getGroup(groupId) if (!group) { - console.error(`Group not found: ${groupSlug}`) + console.error(`Group not found: ${groupId}`) return } return await joinGroup(group, userId) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 29cc9266..f3242a7e 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -35,7 +35,7 @@ import { feed } from 'common/feed' import { CATEGORY_LIST } from 'common/categories' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' -import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' +import { addUserToGroupViaId } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' import { randomString } from 'common/util/random' import dayjs from 'dayjs' @@ -99,13 +99,13 @@ export function listenForPrivateUser( const CACHED_USER_KEY = 'CACHED_USER_KEY' const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' -const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY' +const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY' export function writeReferralInfo( defaultReferrerUsername: string, contractId?: string, referralUsername?: string, - groupSlug?: string + groupId?: string ) { const local = safeLocalStorage() const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) @@ -121,7 +121,7 @@ export function writeReferralInfo( local?.setItem(CACHED_REFERRAL_USERNAME_KEY, referralUsername) // Always write the most recent explicit group invite query value - if (groupSlug) local?.setItem(CACHED_REFERRAL_GROUP_SLUG_KEY, groupSlug) + if (groupId) local?.setItem(CACHED_REFERRAL_GROUP_ID_KEY, groupId) // Write the first contract id that we see. const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY) @@ -134,14 +134,14 @@ async function setCachedReferralInfoForUser(user: User | null) { // if the user wasn't created in the last minute, don't bother const now = dayjs().utc() const userCreatedTime = dayjs(user.createdTime) - if (now.diff(userCreatedTime, 'minute') > 1) return + if (now.diff(userCreatedTime, 'minute') > 5) return const local = safeLocalStorage() const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) const cachedReferralContractId = local?.getItem( CACHED_REFERRAL_CONTRACT_ID_KEY ) - const cachedReferralGroupSlug = local?.getItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + const cachedReferralGroupId = local?.getItem(CACHED_REFERRAL_GROUP_ID_KEY) // get user via username if (cachedReferralUsername) @@ -155,6 +155,9 @@ async function setCachedReferralInfoForUser(user: User | null) { referredByContractId: cachedReferralContractId ? cachedReferralContractId : undefined, + referredByGroupId: cachedReferralGroupId + ? cachedReferralGroupId + : undefined, }) ) .catch((err) => { @@ -165,15 +168,14 @@ async function setCachedReferralInfoForUser(user: User | null) { userId: user.id, referredByUserId: referredByUser.id, referredByContractId: cachedReferralContractId, - referredByGroupSlug: cachedReferralGroupSlug, + referredByGroupId: cachedReferralGroupId, }) }) }) - if (cachedReferralGroupSlug) - addUserToGroupViaSlug(cachedReferralGroupSlug, user.id) + if (cachedReferralGroupId) addUserToGroupViaId(cachedReferralGroupId, user.id) - local?.removeItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + local?.removeItem(CACHED_REFERRAL_GROUP_ID_KEY) local?.removeItem(CACHED_REFERRAL_USERNAME_KEY) local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c914b7f0..3ec688b8 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -161,7 +161,7 @@ export default function GroupPage(props: { referrer?: string } if (!user && router.isReady) - writeReferralInfo(creator.username, undefined, referrer, group?.slug) + writeReferralInfo(creator.username, undefined, referrer, group?.id) }, [user, creator, group, router]) const { width } = useWindowSize() diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2d4d2f2b..9166109f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -713,8 +713,12 @@ function QuestionOrGroupLink(props: { href={ sourceContractCreatorUsername ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : (sourceType === 'group' || sourceType === 'tip') && sourceSlug + : // User's added to group or received a tip there + (sourceType === 'group' || sourceType === 'tip') && sourceSlug ? `${groupPath(sourceSlug)}` + : // User referral via group + sourceSlug?.includes('/group/') + ? `${sourceSlug}` : '' } onClick={() => @@ -745,12 +749,16 @@ function getSourceUrl(notification: Notification) { } = notification if (sourceType === 'follow') return `/${sourceUserUsername}` if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + // User referral via contract: if ( sourceContractCreatorUsername && sourceContractSlug && sourceType === 'user' ) return `/${sourceContractCreatorUsername}/${sourceContractSlug}` + // User referral: + if (sourceType === 'user' && !sourceContractSlug) + return `/${sourceUserUsername}` if (sourceType === 'tip' && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}` From 65e4f2453181bb33f0aedae888823c97ef7083c3 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Mon, 18 Jul 2022 12:55:31 -0500 Subject: [PATCH 219/519] groups: only change layout if sidebar chat, smaller leave button --- web/components/groups/groups-button.tsx | 2 +- web/pages/group/[...slugs]/index.tsx | 36 +++++++++++++------------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index b510f44d..39c75d40 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -135,7 +135,7 @@ export function JoinOrLeaveGroupButton(props: { return (

Running low on Mana? Click the link below to recieve a one time gift of 500 Mana!

Did you know, besides making correct predictions, there are plenty of other ways to earn Mana?

  • Recieving tips on comments
  • Unique trader bonus for each user who bets on you markets
  • Reffering friends (click the share button on  a market or group!)
  • Reporting bugs and giving feedback

 

Cheers,

David from Manifold

 

-

This e-mail has been sent to [[EMAIL_TO]], click here to unsubscribe.

' -> + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+

Thanks for + using Manifold Markets. Running low + on mana (M$)? Click the link below to receive a one time gift of M$500!

+
+
+

+
+ + + + +
+ + + + +
+ + Claim M$500 + +
+
+
+
+

+ +

 

+

Cheers,

+

David from Manifold

+

 

+
+
+
+ +
+
+ +
+ + + +
+ +
+ + + +
+ + + +
+
+ + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

This e-mail has been sent to {{name}}, click here to unsubscribe.

+
+
+
+
+
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/functions/src/email-templates/creating-market.html b/functions/src/email-templates/creating-market.html new file mode 100644 index 00000000..64273e7c --- /dev/null +++ b/functions/src/email-templates/creating-market.html @@ -0,0 +1,738 @@ + + + + (no subject) + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

+ On Manifold Markets, several important factors + go into making a good question. These lead to + more people betting on them and allowing a more + accurate prediction to be formed! +

+

+   +

+

+ Manifold also gives its creators 10 Mana for + each unique trader that bets on your + market! +

+

+   +

+

+ What makes a good question? +

+
    +
  • + Clear resolution criteria. This is + needed so users know how you are going to + decide on what the correct answer is. +
  • +
  • + Clear resolution date. This is + sometimes slightly different from the closing + date. We recommend leaving the market open up + until you resolve it, but if it is different + make sure you say what day you intend to + resolve it in the description! +
  • +
  • + Detailed description. Use the rich + text editor to create an easy to read + description. Include any context or background + information that could be useful to people who + are interested in learning more that are + uneducated on the subject. +
  • +
  • + Add it to a group. Groups are the + primary way users filter for relevant markets. + Also, consider making your own groups and + inviting friends/interested communities to + them from other sites! +
  • +
  • + Bonus: Add a comment on your + prediction and explain (with links and + sources) supporting it. +
  • +
+

+   +

+

+ Examples of markets you should + emulate!  +

+ +

+   +

+

+ Why not + + + + create a market + while it is still fresh on your mind? +

+

+ Thanks for reading! +

+

+ David from Manifold +

+
+
+
+ +
+
+ +
+ + + + + +
+ + </div> + <Col className="w-full items-center justify-start gap-2"> + <Row className={'w-full justify-start gap-20'}> + <span className={'min-w-[4rem] font-bold'}>Cost to you:</span>{' '} + <span className={'text-red-500'}> + {formatMoney(acceptorAmount)} + </span> + </Row> + <Col className={'w-full items-center justify-start'}> + <Row className={'w-full justify-start gap-10'}> + <span className={'min-w-[4rem] font-bold'}> + Potential payout: + </span>{' '} + <Row className={'items-center justify-center'}> + <span className={'text-primary'}> + {formatMoney(creatorAmount + acceptorAmount)} + </span> + </Row> + </Row> + </Col> + </Col> + <Row className={'mt-4 justify-end gap-4'}> + <Button + color={'gray'} + disabled={loading} + onClick={() => setOpen(false)} + className={clsx('whitespace-nowrap')} + > + I'm out + </Button> + <Button + color={'indigo'} + disabled={loading} + onClick={() => iAcceptChallenge()} + className={clsx('min-w-[6rem] whitespace-nowrap')} + > + I'm in + </Button> + </Row> + <Row> + <span className={'text-error'}>{errorText}</span> + </Row> + </Col> + </Col> + </Modal> + + {challenge.creatorId != user.id && ( + <Button + color="gradient" + size="2xl" + onClick={() => setOpen(true)} + className={clsx('whitespace-nowrap')} + > + Accept bet + </Button> + )} + </> + ) +} diff --git a/web/components/challenges/create-challenge-button.tsx b/web/components/challenges/create-challenge-button.tsx new file mode 100644 index 00000000..6eab9bc5 --- /dev/null +++ b/web/components/challenges/create-challenge-button.tsx @@ -0,0 +1,255 @@ +import clsx from 'clsx' +import dayjs from 'dayjs' +import React, { useEffect, useState } from 'react' +import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline' + +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { Title } from '../title' +import { User } from 'common/user' +import { Modal } from 'web/components/layout/modal' +import { Button } from '../button' +import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' +import { BinaryContract } from 'common/contract' +import { SiteLink } from 'web/components/site-link' +import { formatMoney } from 'common/util/format' +import { NoLabel, YesLabel } from '../outcome-label' +import { QRCode } from '../qr-code' +import { copyToClipboard } from 'web/lib/util/copy' +import toast from 'react-hot-toast' + +type challengeInfo = { + amount: number + expiresTime: number | null + message: string + outcome: 'YES' | 'NO' | number + acceptorAmount: number +} +export function CreateChallengeButton(props: { + user: User | null | undefined + contract: BinaryContract +}) { + const { user, contract } = props + const [open, setOpen] = useState(false) + const [challengeSlug, setChallengeSlug] = useState('') + + return ( + <> + <Modal open={open} setOpen={(newOpen) => setOpen(newOpen)} size={'sm'}> + <Col className="gap-4 rounded-md bg-white px-8 py-6"> + {/*// add a sign up to challenge button?*/} + {user && ( + <CreateChallengeForm + user={user} + contract={contract} + onCreate={async (newChallenge) => { + const challenge = await createChallenge({ + creator: user, + creatorAmount: newChallenge.amount, + expiresTime: newChallenge.expiresTime, + message: newChallenge.message, + acceptorAmount: newChallenge.acceptorAmount, + outcome: newChallenge.outcome, + contract: contract, + }) + challenge && setChallengeSlug(getChallengeUrl(challenge)) + }} + challengeSlug={challengeSlug} + /> + )} + </Col> + </Modal> + + <button + onClick={() => setOpen(true)} + className="btn btn-outline mb-4 max-w-xs whitespace-nowrap normal-case" + > + Challenge a friend + </button> + </> + ) +} + +function CreateChallengeForm(props: { + user: User + contract: BinaryContract + onCreate: (m: challengeInfo) => Promise<void> + challengeSlug: string +}) { + const { user, onCreate, contract, challengeSlug } = props + const [isCreating, setIsCreating] = useState(false) + const [finishedCreating, setFinishedCreating] = useState(false) + const [error, setError] = useState<string>('') + const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false) + const defaultExpire = 'week' + + const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}` + + const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({ + expiresTime: dayjs().add(2, defaultExpire).valueOf(), + outcome: 'YES', + amount: 100, + acceptorAmount: 100, + message: defaultMessage, + }) + useEffect(() => { + setError('') + }, [challengeInfo]) + + return ( + <> + {!finishedCreating && ( + <form + onSubmit={(e) => { + e.preventDefault() + if (user.balance < challengeInfo.amount) { + setError('You do not have enough mana to create this challenge') + return + } + setIsCreating(true) + onCreate(challengeInfo).finally(() => setIsCreating(false)) + setFinishedCreating(true) + }} + > + <Title className="!mt-2" text="Challenge a friend to bet " /> + <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> + <div>You'll bet:</div> + <Row + className={ + 'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' + } + > + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.amount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + amount: parseInt(e.target.value), + acceptorAmount: editingAcceptorAmount + ? m.acceptorAmount + : parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + <span className={''}>on</span> + {challengeInfo.outcome === 'YES' ? <YesLabel /> : <NoLabel />} + </Row> + <Row className={'mt-3 max-w-xs justify-end'}> + <Button + color={'gradient'} + className={'opacity-80'} + onClick={() => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + outcome: m.outcome === 'YES' ? 'NO' : 'YES', + } + }) + } + > + <SwitchVerticalIcon className={'h-4 w-4'} /> + </Button> + </Row> + <Row className={'items-center'}>If they bet:</Row> + <Row className={'max-w-xs items-center justify-between gap-4 pr-3'}> + <div className={'w-32 sm:mr-1'}> + {editingAcceptorAmount ? ( + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.acceptorAmount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + acceptorAmount: parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + ) : ( + <span className="ml-1 font-bold"> + {formatMoney(challengeInfo.acceptorAmount)} + </span> + )} + </div> + <span>on</span> + {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} + </Row> + </div> + <Row + className={clsx( + 'mt-8', + !editingAcceptorAmount ? 'justify-between' : 'justify-end' + )} + > + {!editingAcceptorAmount && ( + <Button + color={'gray-white'} + onClick={() => setEditingAcceptorAmount(!editingAcceptorAmount)} + > + Edit + </Button> + )} + <Button + type="submit" + color={'indigo'} + className={clsx( + 'whitespace-nowrap drop-shadow-md', + isCreating ? 'disabled' : '' + )} + > + Continue + </Button> + </Row> + <Row className={'text-error'}>{error} </Row> + </form> + )} + {finishedCreating && ( + <> + <Title className="!my-0" text="Challenge Created!" /> + + <div>Share the challenge using the link.</div> + <button + onClick={() => { + copyToClipboard(challengeSlug) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + <LinkIcon className={'mr-2 h-5 w-5'} /> + Copy link + </button> + + <QRCode url={challengeSlug} className="self-center" /> + <Row className={'gap-1 text-gray-500'}> + See your other + <SiteLink className={'underline'} href={'/challenges'}> + challenges + </SiteLink> + </Row> + </> + )} + </> + ) +} diff --git a/web/components/contract/contract-card-preview.tsx b/web/components/contract/contract-card-preview.tsx new file mode 100644 index 00000000..06a7f7f6 --- /dev/null +++ b/web/components/contract/contract-card-preview.tsx @@ -0,0 +1,36 @@ +import { Contract } from 'common/contract' +import { getBinaryProbPercent } from 'web/lib/firebase/contracts' +import { richTextToString } from 'common/util/parse' +import { contractTextDetails } from 'web/components/contract/contract-details' + +export const getOpenGraphProps = (contract: Contract) => { + const { + resolution, + question, + creatorName, + creatorUsername, + outcomeType, + creatorAvatarUrl, + description: desc, + } = contract + const probPercent = + outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined + + const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) + + const description = resolution + ? `Resolved ${resolution}. ${stringDesc}` + : probPercent + ? `${probPercent} chance. ${stringDesc}` + : stringDesc + + return { + question, + probability: probPercent, + metadata: contractTextDetails(contract), + creatorName, + creatorUsername, + creatorAvatarUrl, + description, + } +} diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 50c5a7e6..28eabb04 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -1,4 +1,4 @@ -import { tradingAllowed } from 'web/lib/firebase/contracts' +import { contractUrl, tradingAllowed } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { ContractProbGraph } from './contract-prob-graph' @@ -8,8 +8,8 @@ import { Linkify } from '../linkify' import clsx from 'clsx' import { - FreeResponseResolutionOrChance, BinaryResolutionOrChance, + FreeResponseResolutionOrChance, NumericResolutionOrExpectation, PseudoNumericResolutionOrExpectation, } from './contract-card' @@ -19,8 +19,13 @@ import { AnswersGraph } from '../answers/answers-graph' import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' -import { ShareMarket } from '../share-market' import { NumericGraph } from './numeric-graph' +import { CreateChallengeButton } from 'web/components/challenges/create-challenge-button' +import React from 'react' +import { copyToClipboard } from 'web/lib/util/copy' +import toast from 'react-hot-toast' +import { LinkIcon } from '@heroicons/react/outline' +import { CHALLENGES_ENABLED } from 'common/challenge' export const ContractOverview = (props: { contract: Contract @@ -32,8 +37,10 @@ export const ContractOverview = (props: { const user = useUser() const isCreator = user?.id === creatorId + const isBinary = outcomeType === 'BINARY' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const showChallenge = user && isBinary && !resolution && CHALLENGES_ENABLED return ( <Col className={clsx('mb-6', className)}> @@ -116,13 +123,47 @@ export const ContractOverview = (props: { <AnswersGraph contract={contract} bets={bets} /> )} {outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />} - {(contract.description || isCreator) && <Spacer h={6} />} - {isCreator && <ShareMarket className="px-2" contract={contract} />} + {/* {(contract.description || isCreator) && <Spacer h={6} />} */} <ContractDescription className="px-2" contract={contract} isCreator={isCreator} /> + {/*<Row className="mx-4 mt-4 hidden justify-around sm:block">*/} + {/* {showChallenge && (*/} + {/* <Col className="gap-3">*/} + {/* <div className="text-lg">⚔️ Challenge a friend ⚔️</div>*/} + {/* <CreateChallengeButton user={user} contract={contract} />*/} + {/* </Col>*/} + {/* )}*/} + {/* {isCreator && (*/} + {/* <Col className="gap-3">*/} + {/* <div className="text-lg">Share your market</div>*/} + {/* <ShareMarketButton contract={contract} />*/} + {/* </Col>*/} + {/* )}*/} + {/*</Row>*/} + <Row className="mx-4 mt-6 block justify-around"> + {showChallenge && ( + <Col className="gap-3"> + <CreateChallengeButton user={user} contract={contract} /> + </Col> + )} + {isCreator && ( + <Col className="gap-3"> + <button + onClick={() => { + copyToClipboard(contractUrl(contract)) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + <LinkIcon className={'mr-2 h-5 w-5'} /> + Share market + </button> + </Col> + )} + </Row> </Col> ) } diff --git a/web/components/copy-link-button.tsx b/web/components/copy-link-button.tsx index 4ce4140d..f3489f3d 100644 --- a/web/components/copy-link-button.tsx +++ b/web/components/copy-link-button.tsx @@ -2,7 +2,6 @@ import React, { Fragment } from 'react' import { LinkIcon } from '@heroicons/react/outline' import { Menu, Transition } from '@headlessui/react' import clsx from 'clsx' - import { copyToClipboard } from 'web/lib/util/copy' import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' @@ -14,6 +13,8 @@ export function CopyLinkButton(props: { tracking?: string buttonClassName?: string toastClassName?: string + icon?: React.ComponentType<{ className?: string }> + label?: string }) { const { url, displayUrl, tracking, buttonClassName, toastClassName } = props diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index b1c8f6ee..cd490701 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -26,7 +26,10 @@ export function ContractActivity(props: { const contract = useContractWithPreload(props.contract) ?? props.contract const comments = props.comments - const updatedBets = useBets(contract.id) + const updatedBets = useBets(contract.id, { + filterChallenges: false, + filterRedemptions: true, + }) const bets = (updatedBets ?? props.bets).filter( (bet) => !bet.isRedemption && bet.amount !== 0 ) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 408404ba..29645136 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -10,11 +10,14 @@ import { UsersIcon } from '@heroicons/react/solid' import { formatMoney, formatPercent } from 'common/util/format' import { OutcomeLabel } from 'web/components/outcome-label' import { RelativeTimestamp } from 'web/components/relative-timestamp' -import React, { Fragment } from 'react' +import React, { Fragment, useEffect } from 'react' import { uniqBy, partition, sumBy, groupBy } from 'lodash' import { JoinSpans } from 'web/components/join-spans' import { UserLink } from '../user-page' import { formatNumericProbability } from 'common/pseudo-numeric' +import { SiteLink } from 'web/components/site-link' +import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' +import { Challenge } from 'common/challenge' export function FeedBet(props: { contract: Contract @@ -79,7 +82,15 @@ export function BetStatusText(props: { const { outcomeType } = contract const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isFreeResponse = outcomeType === 'FREE_RESPONSE' - const { amount, outcome, createdTime } = bet + const { amount, outcome, createdTime, challengeSlug } = bet + const [challenge, setChallenge] = React.useState<Challenge>() + useEffect(() => { + if (challengeSlug) { + getChallenge(challengeSlug, contract.id).then((c) => { + setChallenge(c) + }) + } + }, [challengeSlug, contract.id]) const bought = amount >= 0 ? 'bought' : 'sold' const outOfTotalAmount = @@ -133,6 +144,14 @@ export function BetStatusText(props: { {fromProb === toProb ? `at ${fromProb}` : `from ${fromProb} to ${toProb}`} + {challengeSlug && ( + <SiteLink + href={challenge ? getChallengeUrl(challenge) : ''} + className={'mx-1'} + > + [challenge] + </SiteLink> + )} </> )} <RelativeTimestamp time={createdTime} /> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index a051faed..713bc575 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -29,6 +29,7 @@ import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { PrivateUser } from 'common/user' import { useWindowSize } from 'web/hooks/use-window-size' +import { CHALLENGES_ENABLED } from 'common/challenge' const logout = async () => { // log out, and then reload the page, in case SSR wants to boot them out @@ -60,26 +61,50 @@ function getMoreNavigation(user?: User | null) { } if (!user) { - return [ - { name: 'Charity', href: '/charity' }, - { name: 'Blog', href: 'https://news.manifold.markets' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, - ] + if (CHALLENGES_ENABLED) + return [ + { name: 'Challenges', href: '/challenges' }, + { name: 'Charity', href: '/charity' }, + { name: 'Blog', href: 'https://news.manifold.markets' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, + ] + else + return [ + { name: 'Charity', href: '/charity' }, + { name: 'Blog', href: 'https://news.manifold.markets' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, + ] } - return [ - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, - { - name: 'Sign out', - href: '#', - onClick: logout, - }, - ] + if (CHALLENGES_ENABLED) + return [ + { name: 'Challenges', href: '/challenges' }, + { name: 'Referrals', href: '/referrals' }, + { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, + { + name: 'Sign out', + href: '#', + onClick: logout, + }, + ] + else + return [ + { name: 'Referrals', href: '/referrals' }, + { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, + { + name: 'Sign out', + href: '#', + onClick: logout, + }, + ] } const signedOutNavigation = [ @@ -119,6 +144,14 @@ function getMoreMobileNav() { return [ ...(IS_PRIVATE_MANIFOLD ? [] + : CHALLENGES_ENABLED + ? [ + { name: 'Challenges', href: '/challenges' }, + { name: 'Referrals', href: '/referrals' }, + { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ] : [ { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index fa50365b..611a19d1 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -10,8 +10,9 @@ import { PortfolioValueGraph } from './portfolio-value-graph' export const PortfolioValueSection = memo( function PortfolioValueSection(props: { portfolioHistory: PortfolioMetrics[] + disableSelector?: boolean }) { - const { portfolioHistory } = props + const { portfolioHistory, disableSelector } = props const lastPortfolioMetrics = last(portfolioHistory) const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime') @@ -30,7 +31,9 @@ export const PortfolioValueSection = memo( <div> <Row className="gap-8"> <div className="mb-4 w-full"> - <Col> + <Col + className={disableSelector ? 'items-center justify-center' : ''} + > <div className="text-sm text-gray-500">Portfolio value</div> <div className="text-lg"> {formatMoney( @@ -40,16 +43,18 @@ export const PortfolioValueSection = memo( </div> </Col> </div> - <select - className="select select-bordered self-start" - onChange={(e) => { - setPortfolioPeriod(e.target.value as Period) - }} - > - <option value="allTime">{allTimeLabel}</option> - <option value="weekly">7 days</option> - <option value="daily">24 hours</option> - </select> + {!disableSelector && ( + <select + className="select select-bordered self-start" + onChange={(e) => { + setPortfolioPeriod(e.target.value as Period) + }} + > + <option value="allTime">{allTimeLabel}</option> + <option value="weekly">7 days</option> + <option value="daily">24 hours</option> + </select> + )} </Row> <PortfolioValueGraph portfolioHistory={portfolioHistory} diff --git a/web/components/share-market-button.tsx b/web/components/share-market-button.tsx new file mode 100644 index 00000000..ef7b688d --- /dev/null +++ b/web/components/share-market-button.tsx @@ -0,0 +1,18 @@ +import { ENV_CONFIG } from 'common/envs/constants' +import { Contract, contractPath, contractUrl } from 'web/lib/firebase/contracts' +import { CopyLinkButton } from './copy-link-button' + +export function ShareMarketButton(props: { contract: Contract }) { + const { contract } = props + + const url = `https://${ENV_CONFIG.domain}${contractPath(contract)}` + + return ( + <CopyLinkButton + url={url} + displayUrl={contractUrl(contract)} + buttonClassName="btn-md rounded-l-none" + toastClassName={'-left-28 mt-1'} + /> + ) +} diff --git a/web/components/share-market.tsx b/web/components/share-market.tsx deleted file mode 100644 index be943a34..00000000 --- a/web/components/share-market.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import clsx from 'clsx' - -import { ENV_CONFIG } from 'common/envs/constants' - -import { Contract, contractPath, contractUrl } from 'web/lib/firebase/contracts' -import { CopyLinkButton } from './copy-link-button' -import { Col } from './layout/col' -import { Row } from './layout/row' - -export function ShareMarket(props: { contract: Contract; className?: string }) { - const { contract, className } = props - - const url = `https://${ENV_CONFIG.domain}${contractPath(contract)}` - - return ( - <Col className={clsx(className, 'gap-3')}> - <div>Share your market</div> - <Row className="mb-6 items-center"> - <CopyLinkButton - url={url} - displayUrl={contractUrl(contract)} - buttonClassName="btn-md rounded-l-none" - toastClassName={'-left-28 mt-1'} - /> - </Row> - </Col> - ) -} diff --git a/web/components/sign-up-prompt.tsx b/web/components/sign-up-prompt.tsx index 0edce22c..8882ccfd 100644 --- a/web/components/sign-up-prompt.tsx +++ b/web/components/sign-up-prompt.tsx @@ -2,16 +2,20 @@ import React from 'react' import { useUser } from 'web/hooks/use-user' import { firebaseLogin } from 'web/lib/firebase/users' import { withTracking } from 'web/lib/service/analytics' +import { Button } from './button' -export function SignUpPrompt() { +export function SignUpPrompt(props: { label?: string; className?: string }) { + const { label, className } = props const user = useUser() return user === null ? ( - <button - className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-indigo-500 to-blue-500 px-10 text-lg font-medium normal-case hover:from-indigo-600 hover:to-blue-600" + <Button onClick={withTracking(firebaseLogin, 'sign up to bet')} + className={className} + size="lg" + color="gradient" > - Sign up to bet! - </button> + {label ?? 'Sign up to bet!'} + </Button> ) : null } diff --git a/web/hooks/use-bets.ts b/web/hooks/use-bets.ts index 68b296cd..38b73dd1 100644 --- a/web/hooks/use-bets.ts +++ b/web/hooks/use-bets.ts @@ -9,12 +9,26 @@ import { } from 'web/lib/firebase/bets' import { LimitBet } from 'common/bet' -export const useBets = (contractId: string) => { +export const useBets = ( + contractId: string, + options?: { filterChallenges: boolean; filterRedemptions: boolean } +) => { const [bets, setBets] = useState<Bet[] | undefined>() useEffect(() => { - if (contractId) return listenForBets(contractId, setBets) - }, [contractId]) + if (contractId) + return listenForBets(contractId, (bets) => { + if (options) + setBets( + bets.filter( + (bet) => + (options.filterChallenges ? !bet.challengeSlug : true) && + (options.filterRedemptions ? !bet.isRedemption : true) + ) + ) + else setBets(bets) + }) + }, [contractId, options]) return bets } diff --git a/web/hooks/use-save-referral.ts b/web/hooks/use-save-referral.ts index 7772f9d2..cc96ec72 100644 --- a/web/hooks/use-save-referral.ts +++ b/web/hooks/use-save-referral.ts @@ -6,7 +6,7 @@ import { User, writeReferralInfo } from 'web/lib/firebase/users' export const useSaveReferral = ( user?: User | null, options?: { - defaultReferrer?: string + defaultReferrerUsername?: string contractId?: string groupId?: string } @@ -18,7 +18,7 @@ export const useSaveReferral = ( referrer?: string } - const referrerOrDefault = referrer || options?.defaultReferrer + const referrerOrDefault = referrer || options?.defaultReferrerUsername if (!user && router.isReady && referrerOrDefault) { writeReferralInfo(referrerOrDefault, { diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index 4c492d6c..d84c7d03 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { QueryClient } from 'react-query' -import { doc, DocumentData } from 'firebase/firestore' +import { doc, DocumentData, where } from 'firebase/firestore' import { PrivateUser } from 'common/user' import { getUser, diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts index 87d94dce..5f250ce7 100644 --- a/web/lib/firebase/api.ts +++ b/web/lib/firebase/api.ts @@ -81,6 +81,10 @@ export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } +export function acceptChallenge(params: any) { + return call(getFunctionUrl('acceptchallenge'), 'POST', params) +} + export function getCurrentUser(params: any) { return call(getFunctionUrl('getcurrentuser'), 'GET', params) } diff --git a/web/lib/firebase/challenges.ts b/web/lib/firebase/challenges.ts new file mode 100644 index 00000000..d62d5aac --- /dev/null +++ b/web/lib/firebase/challenges.ts @@ -0,0 +1,150 @@ +import { + collectionGroup, + doc, + getDoc, + orderBy, + query, + setDoc, + where, +} from 'firebase/firestore' +import { Challenge } from 'common/challenge' +import { customAlphabet } from 'nanoid' +import { coll, listenForValue, listenForValues } from './utils' +import { useEffect, useState } from 'react' +import { User } from 'common/user' +import { db } from './init' +import { Contract } from 'common/contract' +import { ENV_CONFIG } from 'common/envs/constants' + +export const challenges = (contractId: string) => + coll<Challenge>(`contracts/${contractId}/challenges`) + +export function getChallengeUrl(challenge: Challenge) { + return `https://${ENV_CONFIG.domain}/challenges/${challenge.creatorUsername}/${challenge.contractSlug}/${challenge.slug}` +} +export async function createChallenge(data: { + creator: User + outcome: 'YES' | 'NO' | number + contract: Contract + creatorAmount: number + acceptorAmount: number + expiresTime: number | null + message: string +}) { + const { + creator, + creatorAmount, + expiresTime, + message, + contract, + outcome, + acceptorAmount, + } = data + + // At 100 IDs per hour, using this alphabet and 8 chars, there's a 1% chance of collision in 2 years + // See https://zelark.github.io/nano-id-cc/ + const nanoid = customAlphabet( + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 8 + ) + const slug = nanoid() + + if (creatorAmount <= 0 || isNaN(creatorAmount) || !isFinite(creatorAmount)) + return null + + const challenge: Challenge = { + slug, + creatorId: creator.id, + creatorUsername: creator.username, + creatorName: creator.name, + creatorAvatarUrl: creator.avatarUrl, + creatorAmount, + creatorOutcome: outcome.toString(), + creatorOutcomeProb: creatorAmount / (creatorAmount + acceptorAmount), + acceptorOutcome: outcome === 'YES' ? 'NO' : 'YES', + acceptorAmount, + contractSlug: contract.slug, + contractId: contract.id, + contractQuestion: contract.question, + contractCreatorUsername: contract.creatorUsername, + createdTime: Date.now(), + expiresTime, + maxUses: 1, + acceptedByUserIds: [], + acceptances: [], + isResolved: false, + message, + } + + await setDoc(doc(challenges(contract.id), slug), challenge) + return challenge +} + +// TODO: This required an index, make sure to also set up in prod +function listUserChallenges(fromId?: string) { + return query( + collectionGroup(db, 'challenges'), + where('creatorId', '==', fromId), + orderBy('createdTime', 'desc') + ) +} + +function listChallenges() { + return query(collectionGroup(db, 'challenges')) +} + +export const useAcceptedChallenges = () => { + const [links, setLinks] = useState<Challenge[]>([]) + + useEffect(() => { + listenForValues(listChallenges(), (challenges: Challenge[]) => { + setLinks( + challenges + .sort((a: Challenge, b: Challenge) => b.createdTime - a.createdTime) + .filter((challenge) => challenge.acceptedByUserIds.length > 0) + ) + }) + }, []) + + return links +} + +export function listenForChallenge( + slug: string, + contractId: string, + setLinks: (challenge: Challenge | null) => void +) { + return listenForValue<Challenge>(doc(challenges(contractId), slug), setLinks) +} + +export function useChallenge(slug: string, contractId: string | undefined) { + const [challenge, setChallenge] = useState<Challenge | null>() + useEffect(() => { + if (slug && contractId) { + listenForChallenge(slug, contractId, setChallenge) + } + }, [contractId, slug]) + return challenge +} + +export function listenForUserChallenges( + fromId: string | undefined, + setLinks: (links: Challenge[]) => void +) { + return listenForValues<Challenge>(listUserChallenges(fromId), setLinks) +} + +export const useUserChallenges = (fromId: string) => { + const [links, setLinks] = useState<Challenge[]>([]) + + useEffect(() => { + return listenForUserChallenges(fromId, setLinks) + }, [fromId]) + + return links +} + +export const getChallenge = async (slug: string, contractId: string) => { + const challenge = await getDoc(doc(challenges(contractId), slug)) + return challenge.data() as Challenge +} diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 9e5de871..3a751c18 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -35,6 +35,13 @@ export function contractPath(contract: Contract) { return `/${contract.creatorUsername}/${contract.slug}` } +export function contractPathWithoutContract( + creatorUsername: string, + slug: string +) { + return `/${creatorUsername}/${slug}` +} + export function homeContractPath(contract: Contract) { return `/home?c=${contract.slug}` } diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 58e7c2e8..0da6c994 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,18 +1,18 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { ArrowLeftIcon } from '@heroicons/react/outline' +import { groupBy, keyBy, mapValues, sortBy, sumBy } from 'lodash' import { useContractWithPreload } from 'web/hooks/use-contract' import { ContractOverview } from 'web/components/contract/contract-overview' import { BetPanel } from 'web/components/bet-panel' import { Col } from 'web/components/layout/col' -import { useUser } from 'web/hooks/use-user' +import { useUser, useUserById } from 'web/hooks/use-user' import { ResolutionPanel } from 'web/components/resolution-panel' import { Spacer } from 'web/components/layout/spacer' import { Contract, getContractFromSlug, tradingAllowed, - getBinaryProbPercent, } from 'web/lib/firebase/contracts' import { SEO } from 'web/components/SEO' import { Page } from 'web/components/page' @@ -21,26 +21,29 @@ import { Comment, listAllComments } from 'web/lib/firebase/comments' import Custom404 from '../404' import { AnswersPanel } from 'web/components/answers/answers-panel' import { fromPropz, usePropz } from 'web/hooks/use-propz' +import { Leaderboard } from 'web/components/leaderboard' +import { resolvedPayout } from 'common/calculate' +import { formatMoney } from 'common/util/format' import { ContractTabs } from 'web/components/contract/contract-tabs' -import { contractTextDetails } from 'web/components/contract/contract-details' import { useWindowSize } from 'web/hooks/use-window-size' import Confetti from 'react-confetti' -import { NumericBetPanel } from '../../components/numeric-bet-panel' -import { NumericResolutionPanel } from '../../components/numeric-resolution-panel' +import { NumericBetPanel } from 'web/components/numeric-bet-panel' +import { NumericResolutionPanel } from 'web/components/numeric-resolution-panel' import { useIsIframe } from 'web/hooks/use-is-iframe' import ContractEmbedPage from '../embed/[username]/[contractSlug]' import { useBets } from 'web/hooks/use-bets' import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' -import { useTipTxns } from 'web/hooks/use-tip-txns' +import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' import { useLiquidity } from 'web/hooks/use-liquidity' -import { richTextToString } from 'common/util/parse' import { useSaveReferral } from 'web/hooks/use-save-referral' -import { - ContractLeaderboard, - ContractTopTrades, -} from 'web/components/contract/contract-leaderboard' +import { getOpenGraphProps } from 'web/components/contract/contract-card-preview' +import { User } from 'common/user' +import { listUsers } from 'web/lib/firebase/users' +import { FeedComment } from 'web/components/feed/feed-comments' +import { Title } from 'web/components/title' +import { FeedBet } from 'web/components/feed/feed-bets' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -153,7 +156,7 @@ export function ContractPageContent( const ogCardProps = getOpenGraphProps(contract) useSaveReferral(user, { - defaultReferrer: contract.creatorUsername, + defaultReferrerUsername: contract.creatorUsername, contractId: contract.id, }) @@ -208,7 +211,10 @@ export function ContractPageContent( </button> )} - <ContractOverview contract={contract} bets={bets} /> + <ContractOverview + contract={contract} + bets={bets.filter((b) => !b.challengeSlug)} + /> {isNumeric && ( <AlertBox @@ -258,34 +264,125 @@ export function ContractPageContent( ) } -const getOpenGraphProps = (contract: Contract) => { - const { - resolution, - question, - creatorName, - creatorUsername, - outcomeType, - creatorAvatarUrl, - description: desc, - } = contract - const probPercent = - outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined +function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + const [users, setUsers] = useState<User[]>() - const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) + const { userProfits, top5Ids } = useMemo(() => { + // Create a map of userIds to total profits (including sales) + const openBets = bets.filter((bet) => !bet.isSold && !bet.sale) + const betsByUser = groupBy(openBets, 'userId') - const description = resolution - ? `Resolved ${resolution}. ${stringDesc}` - : probPercent - ? `${probPercent} chance. ${stringDesc}` - : stringDesc + const userProfits = mapValues(betsByUser, (bets) => + sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) + ) + // Find the 5 users with the most profits + const top5Ids = Object.entries(userProfits) + .sort(([_i1, p1], [_i2, p2]) => p2 - p1) + .filter(([, p]) => p > 0) + .slice(0, 5) + .map(([id]) => id) + return { userProfits, top5Ids } + }, [contract, bets]) - return { - question, - probability: probPercent, - metadata: contractTextDetails(contract), - creatorName, - creatorUsername, - creatorAvatarUrl, - description, - } + useEffect(() => { + if (top5Ids.length > 0) { + listUsers(top5Ids).then((users) => { + const sortedUsers = sortBy(users, (user) => -userProfits[user.id]) + setUsers(sortedUsers) + }) + } + }, [userProfits, top5Ids]) + + return users && users.length > 0 ? ( + <Leaderboard + title="🏅 Top bettors" + users={users || []} + columns={[ + { + header: 'Total profit', + renderCell: (user) => formatMoney(userProfits[user.id] || 0), + }, + ]} + className="mt-12 max-w-sm" + /> + ) : null +} + +function ContractTopTrades(props: { + contract: Contract + bets: Bet[] + comments: Comment[] + tips: CommentTipMap +}) { + const { contract, bets, comments, tips } = props + const commentsById = keyBy(comments, 'id') + const betsById = keyBy(bets, 'id') + + // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit + // Otherwise, we record the profit at resolution time + const profitById: Record<string, number> = {} + for (const bet of bets) { + if (bet.sale) { + const originalBet = betsById[bet.sale.betId] + const profit = bet.sale.amount - originalBet.amount + profitById[bet.id] = profit + profitById[originalBet.id] = profit + } else { + profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount + } + } + + // Now find the betId with the highest profit + const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id + const topBettor = useUserById(betsById[topBetId]?.userId) + + // And also the commentId of the comment with the highest profit + const topCommentId = sortBy( + comments, + (c) => c.betId && -profitById[c.betId] + )[0]?.id + + return ( + <div className="mt-12 max-w-sm"> + {topCommentId && profitById[topCommentId] > 0 && ( + <> + <Title text="💬 Proven correct" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedComment + contract={contract} + comment={commentsById[topCommentId]} + tips={tips[topCommentId]} + betsBySameUser={[betsById[topCommentId]]} + truncate={false} + smallAvatar={false} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {commentsById[topCommentId].userName} made{' '} + {formatMoney(profitById[topCommentId] || 0)}! + </div> + <Spacer h={16} /> + </> + )} + + {/* If they're the same, only show the comment; otherwise show both */} + {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && ( + <> + <Title text="💸 Smartest money" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedBet + contract={contract} + bet={betsById[topBetId]} + hideOutcome={false} + smallAvatar={false} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}! + </div> + </> + )} + </div> + ) } diff --git a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx new file mode 100644 index 00000000..baf68e2a --- /dev/null +++ b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx @@ -0,0 +1,403 @@ +import React, { useEffect, useState } from 'react' +import Confetti from 'react-confetti' + +import { fromPropz, usePropz } from 'web/hooks/use-propz' +import { contractPath, getContractFromSlug } from 'web/lib/firebase/contracts' +import { useContractWithPreload } from 'web/hooks/use-contract' +import { DOMAIN } from 'common/envs/constants' +import { Col } from 'web/components/layout/col' +import { SiteLink } from 'web/components/site-link' +import { Spacer } from 'web/components/layout/spacer' +import { Row } from 'web/components/layout/row' +import { Challenge } from 'common/challenge' +import { + getChallenge, + getChallengeUrl, + useChallenge, +} from 'web/lib/firebase/challenges' +import { getUserByUsername } from 'web/lib/firebase/users' +import { User } from 'common/user' +import { Page } from 'web/components/page' +import { useUser, useUserById } from 'web/hooks/use-user' +import { AcceptChallengeButton } from 'web/components/challenges/accept-challenge-button' +import { Avatar } from 'web/components/avatar' +import { UserLink } from 'web/components/user-page' +import { BinaryOutcomeLabel } from 'web/components/outcome-label' +import { formatMoney } from 'common/util/format' +import { LoadingIndicator } from 'web/components/loading-indicator' +import { useWindowSize } from 'web/hooks/use-window-size' +import { Bet, listAllBets } from 'web/lib/firebase/bets' +import { SEO } from 'web/components/SEO' +import { getOpenGraphProps } from 'web/components/contract/contract-card-preview' +import Custom404 from 'web/pages/404' +import { useSaveReferral } from 'web/hooks/use-save-referral' +import { BinaryContract } from 'common/contract' +import { Title } from 'web/components/title' + +export const getStaticProps = fromPropz(getStaticPropz) + +export async function getStaticPropz(props: { + params: { username: string; contractSlug: string; challengeSlug: string } +}) { + const { username, contractSlug, challengeSlug } = props.params + const contract = (await getContractFromSlug(contractSlug)) || null + const user = (await getUserByUsername(username)) || null + const bets = contract?.id ? await listAllBets(contract.id) : [] + const challenge = contract?.id + ? await getChallenge(challengeSlug, contract.id) + : null + + return { + props: { + contract, + user, + slug: contractSlug, + challengeSlug, + bets, + challenge, + }, + + revalidate: 60, // regenerate after a minute + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: 'blocking' } +} + +export default function ChallengePage(props: { + contract: BinaryContract | null + user: User + slug: string + bets: Bet[] + challenge: Challenge | null + challengeSlug: string +}) { + props = usePropz(props, getStaticPropz) ?? { + contract: null, + user: null, + challengeSlug: '', + bets: [], + challenge: null, + slug: '', + } + const contract = (useContractWithPreload(props.contract) ?? + props.contract) as BinaryContract + + const challenge = + useChallenge(props.challengeSlug, contract?.id) ?? props.challenge + + const { user, bets } = props + const currentUser = useUser() + + useSaveReferral(currentUser, { + defaultReferrerUsername: challenge?.creatorUsername, + }) + + if (!contract || !challenge) return <Custom404 /> + + const ogCardProps = getOpenGraphProps(contract) + ogCardProps.creatorUsername = challenge.creatorUsername + ogCardProps.creatorName = challenge.creatorName + ogCardProps.creatorAvatarUrl = challenge.creatorAvatarUrl + + return ( + <Page> + <SEO + title={ogCardProps.question} + description={ogCardProps.description} + url={getChallengeUrl(challenge).replace('https://', '')} + ogCardProps={ogCardProps} + challenge={challenge} + /> + {challenge.acceptances.length >= challenge.maxUses ? ( + <ClosedChallengeContent + contract={contract} + challenge={challenge} + creator={user} + /> + ) : ( + <OpenChallengeContent + user={currentUser} + contract={contract} + challenge={challenge} + creator={user} + bets={bets} + /> + )} + + <FAQ /> + </Page> + ) +} + +function FAQ() { + const [toggleWhatIsThis, setToggleWhatIsThis] = useState(false) + const [toggleWhatIsMana, setToggleWhatIsMana] = useState(false) + return ( + <Col className={'items-center gap-4 p-2 md:p-6 lg:items-start'}> + <Row className={'text-xl text-indigo-700'}>FAQ</Row> + <Row className={'text-lg text-indigo-700'}> + <span + className={'mx-2 cursor-pointer'} + onClick={() => setToggleWhatIsThis(!toggleWhatIsThis)} + > + {toggleWhatIsThis ? '-' : '+'} + What is this? + </span> + </Row> + {toggleWhatIsThis && ( + <Row className={'mx-4'}> + <span> + This is a challenge bet, or a bet offered from one person to another + that is only realized if both parties agree. You can agree to the + challenge (if it's open) or create your own from a market page. See + more markets{' '} + <SiteLink className={'font-bold'} href={'/home'}> + here. + </SiteLink> + </span> + </Row> + )} + <Row className={'text-lg text-indigo-700'}> + <span + className={'mx-2 cursor-pointer'} + onClick={() => setToggleWhatIsMana(!toggleWhatIsMana)} + > + {toggleWhatIsMana ? '-' : '+'} + What is M$? + </span> + </Row> + {toggleWhatIsMana && ( + <Row className={'mx-4'}> + Mana (M$) is the play-money used by our platform to keep track of your + bets. It's completely free for you and your friends to get started! + </Row> + )} + </Col> + ) +} + +function ClosedChallengeContent(props: { + contract: BinaryContract + challenge: Challenge + creator: User +}) { + const { contract, challenge, creator } = props + const { resolution, question } = contract + const { + acceptances, + creatorAmount, + creatorOutcome, + acceptorOutcome, + acceptorAmount, + } = challenge + + const user = useUserById(acceptances[0].userId) + + const [showConfetti, setShowConfetti] = useState(false) + const { width, height } = useWindowSize() + useEffect(() => { + if (acceptances.length === 0) return + if (acceptances[0].createdTime > Date.now() - 1000 * 60) + setShowConfetti(true) + }, [acceptances]) + + const creatorWon = resolution === creatorOutcome + + const href = `https://${DOMAIN}${contractPath(contract)}` + + if (!user) return <LoadingIndicator /> + + const winner = (creatorWon ? creator : user).name + + return ( + <> + {showConfetti && ( + <Confetti + width={width ?? 500} + height={height ?? 500} + confettiSource={{ + x: ((width ?? 500) - 200) / 2, + y: 0, + w: 200, + h: 0, + }} + recycle={false} + initialVelocityY={{ min: 1, max: 3 }} + numberOfPieces={200} + /> + )} + <Col className=" w-full items-center justify-center rounded border-0 border-gray-100 bg-white py-6 pl-1 pr-2 sm:px-2 md:px-6 md:py-8 "> + {resolution ? ( + <> + <Title className="!mt-0" text={`🥇 ${winner} wins the bet 🥇`} /> + <SiteLink href={href} className={'mb-8 text-xl'}> + {question} + </SiteLink> + </> + ) : ( + <SiteLink href={href} className={'mb-8'}> + <span className="text-3xl text-indigo-700">{question}</span> + </SiteLink> + )} + <Col + className={'w-full content-between justify-between gap-1 sm:flex-row'} + > + <UserBetColumn + challenger={creator} + outcome={creatorOutcome} + amount={creatorAmount} + isResolved={!!resolution} + /> + + <Col className="items-center justify-center py-8 text-2xl sm:text-4xl"> + VS + </Col> + + <UserBetColumn + challenger={user?.id === creator.id ? undefined : user} + outcome={acceptorOutcome} + amount={acceptorAmount} + isResolved={!!resolution} + /> + </Col> + <Spacer h={3} /> + + {/* <Row className="mt-8 items-center"> + <span className='mr-4'>Share</span> <CopyLinkButton url={window.location.href} /> + </Row> */} + </Col> + </> + ) +} + +function OpenChallengeContent(props: { + contract: BinaryContract + challenge: Challenge + creator: User + user: User | null | undefined + bets: Bet[] +}) { + const { contract, challenge, creator, user } = props + const { question } = contract + const { + creatorAmount, + creatorId, + creatorOutcome, + acceptorAmount, + acceptorOutcome, + } = challenge + + const href = `https://${DOMAIN}${contractPath(contract)}` + + return ( + <Col className="items-center"> + <Col className="h-full items-center justify-center rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md"> + <SiteLink href={href} className={'mb-8'}> + <span className="text-3xl text-indigo-700">{question}</span> + </SiteLink> + + <Col + className={ + 'h-full max-h-[50vh] w-full content-between justify-between gap-1 sm:flex-row' + } + > + <UserBetColumn + challenger={creator} + outcome={creatorOutcome} + amount={creatorAmount} + /> + + <Col className="items-center justify-center py-4 text-2xl sm:py-8 sm:text-4xl"> + VS + </Col> + + <UserBetColumn + challenger={user?.id === creatorId ? undefined : user} + outcome={acceptorOutcome} + amount={acceptorAmount} + /> + </Col> + + <Spacer h={3} /> + <Row className={'my-4 text-center text-gray-500'}> + <span> + {`${creator.name} will bet ${formatMoney( + creatorAmount + )} on ${creatorOutcome} if you bet ${formatMoney( + acceptorAmount + )} on ${acceptorOutcome}. Whoever is right will get `} + <span className="mr-1 font-bold "> + {formatMoney(creatorAmount + acceptorAmount)} + </span> + total. + </span> + </Row> + + <Row className="my-4 w-full items-center justify-center"> + <AcceptChallengeButton + user={user} + contract={contract} + challenge={challenge} + /> + </Row> + </Col> + </Col> + ) +} + +const userCol = (challenger: User) => ( + <Col className={'mb-2 w-full items-center justify-center gap-2'}> + <UserLink + className={'text-2xl'} + name={challenger.name} + username={challenger.username} + /> + <Avatar + size={24} + avatarUrl={challenger.avatarUrl} + username={challenger.username} + /> + </Col> +) + +function UserBetColumn(props: { + challenger: User | null | undefined + outcome: string + amount: number + isResolved?: boolean +}) { + const { challenger, outcome, amount, isResolved } = props + + return ( + <Col className="w-full items-start justify-center gap-1"> + {challenger ? ( + userCol(challenger) + ) : ( + <Col className={'mb-2 w-full items-center justify-center gap-2'}> + <span className={'text-2xl'}>You</span> + <Avatar + className={'h-[7.25rem] w-[7.25rem]'} + avatarUrl={undefined} + username={undefined} + /> + </Col> + )} + <Row className={'w-full items-center justify-center'}> + <span className={'text-lg'}> + {isResolved ? 'had bet' : challenger ? '' : ''} + </span> + </Row> + <Row className={'w-full items-center justify-center'}> + <span className={'text-lg'}> + <span className="bold text-2xl">{formatMoney(amount)}</span> + {' on '} + <span className="bold text-2xl"> + <BinaryOutcomeLabel outcome={outcome as any} /> + </span>{' '} + </span> + </Row> + </Col> + ) +} diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx new file mode 100644 index 00000000..40e00084 --- /dev/null +++ b/web/pages/challenges/index.tsx @@ -0,0 +1,300 @@ +import clsx from 'clsx' +import React from 'react' +import { formatMoney } from 'common/util/format' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' +import { Page } from 'web/components/page' +import { SEO } from 'web/components/SEO' +import { Title } from 'web/components/title' +import { useUser } from 'web/hooks/use-user' +import { fromNow } from 'web/lib/util/time' + +import dayjs from 'dayjs' +import customParseFormat from 'dayjs/plugin/customParseFormat' +import { + getChallengeUrl, + useAcceptedChallenges, + useUserChallenges, +} from 'web/lib/firebase/challenges' +import { Challenge } from 'common/challenge' +import { Tabs } from 'web/components/layout/tabs' +import { SiteLink } from 'web/components/site-link' +import { UserLink } from 'web/components/user-page' +import { Avatar } from 'web/components/avatar' +import Router from 'next/router' +import { contractPathWithoutContract } from 'web/lib/firebase/contracts' +import { Button } from 'web/components/button' +import { ClipboardCopyIcon, QrcodeIcon } from '@heroicons/react/outline' +import { copyToClipboard } from 'web/lib/util/copy' +import toast from 'react-hot-toast' +import { Modal } from 'web/components/layout/modal' +import { QRCode } from 'web/components/qr-code' + +dayjs.extend(customParseFormat) +const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate' +const amountClass = columnClass + ' max-w-[75px] font-bold' + +export default function ChallengesListPage() { + const user = useUser() + const userChallenges = useUserChallenges(user?.id ?? '') + const challenges = useAcceptedChallenges() + + const userTab = user + ? [ + { + content: <YourChallengesTable links={userChallenges} />, + title: 'Your Challenges', + }, + ] + : [] + + const publicTab = [ + { + content: <PublicChallengesTable links={challenges} />, + title: 'Public Challenges', + }, + ] + + return ( + <Page> + <SEO + title="Challenges" + description="Challenge your friends to a bet!" + url="/send" + /> + + <Col className="w-full px-8"> + <Row className="items-center justify-between"> + <Title text="Challenges" /> + </Row> + <p>Find or create a question to challenge someone to a bet.</p> + + <Tabs tabs={[...userTab, ...publicTab]} /> + </Col> + </Page> + ) +} + +function YourChallengesTable(props: { links: Challenge[] }) { + const { links } = props + return links.length == 0 ? ( + <p>There aren't currently any challenges.</p> + ) : ( + <div className="overflow-scroll"> + <table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200"> + <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> + <tr> + <th className={amountClass}>Amount</th> + <th + className={clsx( + columnClass, + 'text-center sm:pl-10 sm:text-start' + )} + > + Link + </th> + <th className={columnClass}>Accepted By</th> + </tr> + </thead> + <tbody className={'divide-y divide-gray-200 bg-white'}> + {links.map((link) => ( + <YourLinkSummaryRow challenge={link} /> + ))} + </tbody> + </table> + </div> + ) +} + +function YourLinkSummaryRow(props: { challenge: Challenge }) { + const { challenge } = props + const { acceptances } = challenge + const [open, setOpen] = React.useState(false) + const className = clsx( + 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white' + ) + return ( + <> + <Modal open={open} setOpen={setOpen} size={'sm'}> + <Col + className={ + 'items-center justify-center gap-4 rounded-md bg-white p-8 py-8 ' + } + > + <span className={'mb-4 text-center text-xl text-indigo-700'}> + Have your friend scan this to accept the challenge! + </span> + <QRCode url={getChallengeUrl(challenge)} /> + </Col> + </Modal> + <tr id={challenge.slug} key={challenge.slug} className={className}> + <td className={amountClass}> + <SiteLink href={getChallengeUrl(challenge)}> + {formatMoney(challenge.creatorAmount)} + </SiteLink> + </td> + <td + className={clsx( + columnClass, + 'text-center sm:max-w-[200px] sm:text-start' + )} + > + <Row className="items-center gap-2"> + <Button + color="gray-white" + size="xs" + onClick={() => { + copyToClipboard(getChallengeUrl(challenge)) + toast('Link copied to clipboard!') + }} + > + <ClipboardCopyIcon className={'h-5 w-5 sm:h-4 sm:w-4'} /> + </Button> + <Button + color="gray-white" + size="xs" + onClick={() => { + setOpen(true) + }} + > + <QrcodeIcon className="h-5 w-5 sm:h-4 sm:w-4" /> + </Button> + <SiteLink + href={getChallengeUrl(challenge)} + className={'mx-1 mb-1 hidden sm:inline-block'} + > + {`...${challenge.contractSlug}/${challenge.slug}`} + </SiteLink> + </Row> + </td> + + <td className={columnClass}> + <Row className={'items-center justify-start gap-1'}> + {acceptances.length > 0 ? ( + <> + <Avatar + username={acceptances[0].userUsername} + avatarUrl={acceptances[0].userAvatarUrl} + size={'sm'} + /> + <UserLink + name={acceptances[0].userName} + username={acceptances[0].userUsername} + /> + </> + ) : ( + <span> + No one - + {challenge.expiresTime && + ` (expires ${fromNow(challenge.expiresTime)})`} + </span> + )} + </Row> + </td> + </tr> + </> + ) +} + +function PublicChallengesTable(props: { links: Challenge[] }) { + const { links } = props + return links.length == 0 ? ( + <p>There aren't currently any challenges.</p> + ) : ( + <div className="overflow-scroll"> + <table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200"> + <thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900"> + <tr> + <th className={amountClass}>Amount</th> + <th className={columnClass}>Creator</th> + <th className={columnClass}>Acceptor</th> + <th className={columnClass}>Market</th> + </tr> + </thead> + <tbody className={'divide-y divide-gray-200 bg-white'}> + {links.map((link) => ( + <PublicLinkSummaryRow challenge={link} /> + ))} + </tbody> + </table> + </div> + ) +} + +function PublicLinkSummaryRow(props: { challenge: Challenge }) { + const { challenge } = props + const { + acceptances, + creatorUsername, + creatorName, + creatorAvatarUrl, + contractCreatorUsername, + contractQuestion, + contractSlug, + } = challenge + + const className = clsx( + 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white' + ) + return ( + <tr + id={challenge.slug + '-public'} + key={challenge.slug + '-public'} + className={className} + onClick={() => Router.push(getChallengeUrl(challenge))} + > + <td className={amountClass}> + <SiteLink href={getChallengeUrl(challenge)}> + {formatMoney(challenge.creatorAmount)} + </SiteLink> + </td> + + <td className={clsx(columnClass)}> + <Row className={'items-center justify-start gap-1'}> + <Avatar + username={creatorUsername} + avatarUrl={creatorAvatarUrl} + size={'sm'} + noLink={true} + /> + <UserLink name={creatorName} username={creatorUsername} /> + </Row> + </td> + + <td className={clsx(columnClass)}> + <Row className={'items-center justify-start gap-1'}> + {acceptances.length > 0 ? ( + <> + <Avatar + username={acceptances[0].userUsername} + avatarUrl={acceptances[0].userAvatarUrl} + size={'sm'} + noLink={true} + /> + <UserLink + name={acceptances[0].userName} + username={acceptances[0].userUsername} + /> + </> + ) : ( + <span> + No one - + {challenge.expiresTime && + ` (expires ${fromNow(challenge.expiresTime)})`} + </span> + )} + </Row> + </td> + <td className={clsx(columnClass, 'font-bold')}> + <SiteLink + href={contractPathWithoutContract( + contractCreatorUsername, + contractSlug + )} + > + {contractQuestion} + </SiteLink> + </td> + </tr> + ) +} diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 57189c0c..d38c6e5b 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -21,8 +21,11 @@ import { useMeasureSize } from 'web/hooks/use-measure-size' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { useWindowSize } from 'web/hooks/use-window-size' import { listAllBets } from 'web/lib/firebase/bets' -import { contractPath, getContractFromSlug } from 'web/lib/firebase/contracts' -import { tradingAllowed } from 'web/lib/firebase/contracts' +import { + contractPath, + getContractFromSlug, + tradingAllowed, +} from 'web/lib/firebase/contracts' import Custom404 from '../../404' export const getStaticProps = fromPropz(getStaticPropz) @@ -76,7 +79,7 @@ export default function ContractEmbedPage(props: { return <ContractEmbed contract={contract} bets={bets} /> } -function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { +export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const { contract, bets } = props const { question, outcomeType } = contract diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 642a2afd..b96d6436 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -160,7 +160,7 @@ export default function GroupPage(props: { const privateUser = usePrivateUser(user?.id) useSaveReferral(user, { - defaultReferrer: creator.username, + defaultReferrerUsername: creator.username, groupId: group?.id, }) diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index c7457f27..d2b12065 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -91,5 +91,5 @@ const useReferral = (user: User | undefined | null, manalink?: Manalink) => { if (manalink?.fromId) getUser(manalink.fromId).then(setCreator) }, [manalink]) - useSaveReferral(user, { defaultReferrer: creator?.username }) + useSaveReferral(user, { defaultReferrerUsername: creator?.username }) } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 9f076c41..625c7c17 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -811,6 +811,7 @@ function getSourceUrl(notification: Notification) { if (sourceType === 'tip' && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}` + if (sourceType === 'challenge') return `${sourceSlug}` if (sourceContractCreatorUsername && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( sourceId ?? '', @@ -913,6 +914,15 @@ function NotificationTextLabel(props: { <span>of your limit order was filled</span> </> ) + } else if (sourceType === 'challenge' && sourceText) { + return ( + <> + <span> for </span> + <span className="text-primary"> + {formatMoney(parseInt(sourceText))} + </span> + </> + ) } return ( <div className={className ? className : 'line-clamp-4 whitespace-pre-line'}> @@ -967,6 +977,9 @@ function getReasonForShowingNotification( case 'bet': reasonText = 'bet against you' break + case 'challenge': + reasonText = 'accepted your challenge' + break default: reasonText = '' } From c93f9c54831eb7d432208f21b309a22640b0bed6 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 4 Aug 2022 15:58:48 -0600 Subject: [PATCH 412/519] See challenges you've accepted too --- web/lib/firebase/challenges.ts | 4 ++-- web/pages/challenges/index.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/lib/firebase/challenges.ts b/web/lib/firebase/challenges.ts index d62d5aac..89da7f80 100644 --- a/web/lib/firebase/challenges.ts +++ b/web/lib/firebase/challenges.ts @@ -134,11 +134,11 @@ export function listenForUserChallenges( return listenForValues<Challenge>(listUserChallenges(fromId), setLinks) } -export const useUserChallenges = (fromId: string) => { +export const useUserChallenges = (fromId?: string) => { const [links, setLinks] = useState<Challenge[]>([]) useEffect(() => { - return listenForUserChallenges(fromId, setLinks) + if (fromId) return listenForUserChallenges(fromId, setLinks) }, [fromId]) return links diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 40e00084..7c68f0bd 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -36,8 +36,12 @@ const amountClass = columnClass + ' max-w-[75px] font-bold' export default function ChallengesListPage() { const user = useUser() - const userChallenges = useUserChallenges(user?.id ?? '') const challenges = useAcceptedChallenges() + const userChallenges = useUserChallenges(user?.id) + .concat( + user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : [] + ) + .sort((a, b) => b.createdTime - a.createdTime) const userTab = user ? [ From 912ccad53053db9647a49ab0310c42d2f874db3d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 4 Aug 2022 16:09:33 -0600 Subject: [PATCH 413/519] Remove max height --- .../challenges/[username]/[contractSlug]/[challengeSlug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx index baf68e2a..0df5b7d7 100644 --- a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx +++ b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx @@ -300,7 +300,7 @@ function OpenChallengeContent(props: { <Col className={ - 'h-full max-h-[50vh] w-full content-between justify-between gap-1 sm:flex-row' + ' w-full content-between justify-between gap-1 sm:flex-row' } > <UserBetColumn From edae709f5f25c192e386dded4838182684c9e6d9 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 4 Aug 2022 15:35:55 -0700 Subject: [PATCH 414/519] Notify mentioned users on market publish (#683) * Add function to parse at mentions * Notify mentioned users on market create - refactor createNotification to accept list of recipients' ids --- common/util/parse.ts | 10 +++ functions/src/create-notification.ts | 66 ++++++++++--------- .../src/on-create-comment-on-contract.ts | 5 +- functions/src/on-create-contract.ts | 9 ++- functions/src/on-create-group.ts | 28 ++++---- functions/src/on-follow-user.ts | 2 +- 6 files changed, 68 insertions(+), 52 deletions(-) diff --git a/common/util/parse.ts b/common/util/parse.ts index cacd0862..f07e4097 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -22,6 +22,7 @@ import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import { Mention } from '@tiptap/extension-mention' import Iframe from './tiptap-iframe' +import { uniq } from 'lodash' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -61,6 +62,15 @@ const checkAgainstQuery = (query: string, corpus: string) => export const searchInAny = (query: string, ...fields: string[]) => fields.some((field) => checkAgainstQuery(query, field)) +/** @return user ids of all \@mentions */ +export function parseMentions(data: JSONContent): string[] { + const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs + if (data.type === 'mention' && data.attrs) { + mentions.push(data.attrs.id as string) + } + return uniq(mentions) +} + // can't just do [StarterKit, Image...] because it doesn't work with cjs imports export const exhibitExts = [ Blockquote, diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 83568535..e16920f7 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -33,7 +33,7 @@ export const createNotification = async ( miscData?: { contract?: Contract relatedSourceType?: notification_source_types - relatedUserId?: string + recipients?: string[] slug?: string title?: string } @@ -41,7 +41,7 @@ export const createNotification = async ( const { contract: sourceContract, relatedSourceType, - relatedUserId, + recipients, slug, title, } = miscData ?? {} @@ -128,7 +128,7 @@ export const createNotification = async ( }) } - const notifyRepliedUsers = async ( + const notifyRepliedUser = ( userToReasonTexts: user_to_reason_texts, relatedUserId: string, relatedSourceType: notification_source_types @@ -145,7 +145,7 @@ export const createNotification = async ( } } - const notifyFollowedUser = async ( + const notifyFollowedUser = ( userToReasonTexts: user_to_reason_texts, followedUserId: string ) => { @@ -155,21 +155,24 @@ export const createNotification = async ( } } - const notifyTaggedUsers = async ( - userToReasonTexts: user_to_reason_texts, - sourceText: string - ) => { - const taggedUsers = sourceText.match(/@\w+/g) - if (!taggedUsers) return - // await all get tagged users: - const users = await Promise.all( - taggedUsers.map(async (username) => { - return await getUserByUsername(username.slice(1)) - }) + /** @deprecated parse from rich text instead */ + const parseMentions = async (source: string) => { + const mentions = source.match(/@\w+/g) + if (!mentions) return [] + return Promise.all( + mentions.map( + async (username) => (await getUserByUsername(username.slice(1)))?.id + ) ) - users.forEach((taggedUser) => { - if (taggedUser && shouldGetNotification(taggedUser.id, userToReasonTexts)) - userToReasonTexts[taggedUser.id] = { + } + + const notifyTaggedUsers = ( + userToReasonTexts: user_to_reason_texts, + userIds: (string | undefined)[] + ) => { + userIds.forEach((id) => { + if (id && shouldGetNotification(id, userToReasonTexts)) + userToReasonTexts[id] = { reason: 'tagged_user', } }) @@ -254,7 +257,7 @@ export const createNotification = async ( }) } - const notifyUserAddedToGroup = async ( + const notifyUserAddedToGroup = ( userToReasonTexts: user_to_reason_texts, relatedUserId: string ) => { @@ -276,11 +279,14 @@ export const createNotification = async ( const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. - if (sourceType === 'follow' && relatedUserId) { - await notifyFollowedUser(userToReasonTexts, relatedUserId) - } else if (sourceType === 'group' && relatedUserId) { - if (sourceUpdateType === 'created') - await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) + if (sourceType === 'follow' && recipients?.[0]) { + notifyFollowedUser(userToReasonTexts, recipients[0]) + } else if ( + sourceType === 'group' && + sourceUpdateType === 'created' && + recipients + ) { + recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r)) } // The following functions need sourceContract to be defined. @@ -293,13 +299,10 @@ export const createNotification = async ( (sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')) ) { if (sourceType === 'comment') { - if (relatedUserId && relatedSourceType) - await notifyRepliedUsers( - userToReasonTexts, - relatedUserId, - relatedSourceType - ) - if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText) + if (recipients?.[0] && relatedSourceType) + notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType) + if (sourceText) + notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText)) } await notifyContractCreator(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) @@ -308,6 +311,7 @@ export const createNotification = async ( await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) } else if (sourceType === 'contract' && sourceUpdateType === 'created') { await notifyUsersFollowers(userToReasonTexts) + notifyTaggedUsers(userToReasonTexts, recipients ?? []) } else if (sourceType === 'contract' && sourceUpdateType === 'closed') { await notifyContractCreator(userToReasonTexts, sourceContract, { force: true, diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 8d841ac0..4719fd08 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -68,9 +68,10 @@ export const onCreateCommentOnContract = functions ? 'answer' : undefined - const relatedUserId = comment.replyToCommentId + const repliedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId + const recipients = repliedUserId ? [repliedUserId] : [] await createNotification( comment.id, @@ -79,7 +80,7 @@ export const onCreateCommentOnContract = functions commentCreator, eventId, comment.text, - { contract, relatedSourceType, relatedUserId } + { contract, relatedSourceType, recipients } ) const recipientUserIds = uniq([ diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index a43beda7..6b57a9a0 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import { getUser } from './utils' import { createNotification } from './create-notification' import { Contract } from '../../common/contract' -import { richTextToString } from '../../common/util/parse' +import { parseMentions, richTextToString } from '../../common/util/parse' import { JSONContent } from '@tiptap/core' export const onCreateContract = functions.firestore @@ -14,13 +14,16 @@ export const onCreateContract = functions.firestore const contractCreator = await getUser(contract.creatorId) if (!contractCreator) throw new Error('Could not find contract creator') + const desc = contract.description as JSONContent + const mentioned = parseMentions(desc) + await createNotification( contract.id, 'contract', 'created', contractCreator, eventId, - richTextToString(contract.description as JSONContent), - { contract } + richTextToString(desc), + { contract, recipients: mentioned } ) }) diff --git a/functions/src/on-create-group.ts b/functions/src/on-create-group.ts index 47618d7a..5209788d 100644 --- a/functions/src/on-create-group.ts +++ b/functions/src/on-create-group.ts @@ -12,19 +12,17 @@ export const onCreateGroup = functions.firestore const groupCreator = await getUser(group.creatorId) if (!groupCreator) throw new Error('Could not find group creator') // create notifications for all members of the group - for (const memberId of group.memberIds) { - await createNotification( - group.id, - 'group', - 'created', - groupCreator, - eventId, - group.about, - { - relatedUserId: memberId, - slug: group.slug, - title: group.name, - } - ) - } + await createNotification( + group.id, + 'group', + 'created', + groupCreator, + eventId, + group.about, + { + recipients: group.memberIds, + slug: group.slug, + title: group.name, + } + ) }) diff --git a/functions/src/on-follow-user.ts b/functions/src/on-follow-user.ts index 9a6e6dce..52042345 100644 --- a/functions/src/on-follow-user.ts +++ b/functions/src/on-follow-user.ts @@ -30,7 +30,7 @@ export const onFollowUser = functions.firestore followingUser, eventId, '', - { relatedUserId: follow.userId } + { recipients: [follow.userId] } ) }) From f52da72115bfacb0af5a4d54c137a936b33d9eee Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 4 Aug 2022 16:34:04 -0700 Subject: [PATCH 415/519] Switch comments/chat to rich text editor (#703) * Switch comments/chat to rich text editor * Remove TruncatedComment * Re-add submit on enter * Insert at mention on reply * Update editor style for send button * only submit on enter in chat * code review: refactor * use more specific type for upload * fix ESlint and errors from merge * fix trigger on every render eslint warning * Notify people mentioned in comment * fix type errors --- common/comment.ts | 6 +- functions/src/create-notification.ts | 19 +- functions/src/emails.ts | 4 +- .../src/on-create-comment-on-contract.ts | 11 +- web/components/comments-list.tsx | 7 +- .../contract/contract-leaderboard.tsx | 1 - web/components/editor.tsx | 30 +-- .../feed/feed-answer-comment-group.tsx | 28 +- web/components/feed/feed-comments.tsx | 242 +++++++----------- web/components/groups/group-chat.tsx | 104 ++++---- web/lib/firebase/comments.ts | 9 +- web/pages/[username]/[contractSlug].tsx | 1 - 12 files changed, 196 insertions(+), 266 deletions(-) diff --git a/common/comment.ts b/common/comment.ts index 0d0c4daf..a217b292 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,3 +1,5 @@ +import type { JSONContent } from '@tiptap/core' + // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. export type Comment = { @@ -9,7 +11,9 @@ export type Comment = { replyToCommentId?: string userId: string - text: string + /** @deprecated - content now stored as JSON in content*/ + text?: string + content: JSONContent createdTime: number // Denormalized, for rendering comments diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index e16920f7..6e312906 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -7,7 +7,7 @@ import { } from '../../common/notification' import { User } from '../../common/user' import { Contract } from '../../common/contract' -import { getUserByUsername, getValues } from './utils' +import { getValues } from './utils' import { Comment } from '../../common/comment' import { uniq } from 'lodash' import { Bet, LimitBet } from '../../common/bet' @@ -17,6 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' import { Group, GROUP_CHAT_SLUG } from '../../common/group' import { Challenge } from '../../common/challenge' +import { richTextToString } from 'common/util/parse' const firestore = admin.firestore() type user_to_reason_texts = { @@ -155,17 +156,6 @@ export const createNotification = async ( } } - /** @deprecated parse from rich text instead */ - const parseMentions = async (source: string) => { - const mentions = source.match(/@\w+/g) - if (!mentions) return [] - return Promise.all( - mentions.map( - async (username) => (await getUserByUsername(username.slice(1)))?.id - ) - ) - } - const notifyTaggedUsers = ( userToReasonTexts: user_to_reason_texts, userIds: (string | undefined)[] @@ -301,8 +291,7 @@ export const createNotification = async ( if (sourceType === 'comment') { if (recipients?.[0] && relatedSourceType) notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType) - if (sourceText) - notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText)) + if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? []) } await notifyContractCreator(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) @@ -427,7 +416,7 @@ export const createGroupCommentNotification = async ( sourceUserName: fromUser.name, sourceUserUsername: fromUser.username, sourceUserAvatarUrl: fromUser.avatarUrl, - sourceText: comment.text, + sourceText: richTextToString(comment.content), sourceSlug, sourceTitle: `${group.name}`, isSeenOnHref: sourceSlug, diff --git a/functions/src/emails.ts b/functions/src/emails.ts index b7469e9f..d594ae65 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -17,6 +17,7 @@ import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' import { getFunctionUrl } from '../../common/api' +import { richTextToString } from 'common/util/parse' const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe') @@ -291,7 +292,8 @@ export const sendNewCommentEmail = async ( const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator - const { text } = comment + const { content } = comment + const text = richTextToString(content) let betDescription = '' if (bet) { diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 4719fd08..a8bc567e 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -1,13 +1,13 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { uniq } from 'lodash' - +import { compact, uniq } from 'lodash' import { getContract, getUser, getValues } from './utils' import { Comment } from '../../common/comment' import { sendNewCommentEmail } from './emails' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' import { createNotification } from './create-notification' +import { parseMentions, richTextToString } from 'common/util/parse' const firestore = admin.firestore() @@ -71,7 +71,10 @@ export const onCreateCommentOnContract = functions const repliedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId - const recipients = repliedUserId ? [repliedUserId] : [] + + const recipients = uniq( + compact([...parseMentions(comment.content), repliedUserId]) + ) await createNotification( comment.id, @@ -79,7 +82,7 @@ export const onCreateCommentOnContract = functions 'created', commentCreator, eventId, - comment.text, + richTextToString(comment.content), { contract, relatedSourceType, recipients } ) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index f8e1d7e1..2a467f6d 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -8,8 +8,8 @@ import { RelativeTimestamp } from './relative-timestamp' import { UserLink } from './user-page' import { User } from 'common/user' import { Col } from './layout/col' -import { Linkify } from './linkify' import { groupBy } from 'lodash' +import { Content } from './editor' export function UserCommentsList(props: { user: User @@ -50,7 +50,8 @@ export function UserCommentsList(props: { function ProfileComment(props: { comment: Comment; className?: string }) { const { comment, className } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const { text, content, userUsername, userName, userAvatarUrl, createdTime } = + comment // TODO: find and attach relevant bets by comment betId at some point return ( <Row className={className}> @@ -64,7 +65,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) { />{' '} <RelativeTimestamp time={createdTime} /> </p> - <Linkify text={text} /> + <Content content={content || text} /> </div> </Row> ) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index deb9b857..6f1a778d 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -107,7 +107,6 @@ export function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} - truncate={false} smallAvatar={false} /> </div> diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 963cea7e..f71e8589 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -41,14 +41,16 @@ export function useTextEditor(props: { max?: number defaultValue?: Content disabled?: boolean + simple?: boolean }) { - const { placeholder, max, defaultValue = '', disabled } = props + const { placeholder, max, defaultValue = '', disabled, simple } = props const users = useUsers() const editorClass = clsx( proseClass, - 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' + !simple && 'min-h-[6em]', + 'outline-none pt-2 px-4' ) const editor = useEditor( @@ -56,7 +58,8 @@ export function useTextEditor(props: { editorProps: { attributes: { class: editorClass } }, extensions: [ StarterKit.configure({ - heading: { levels: [1, 2, 3] }, + heading: simple ? false : { levels: [1, 2, 3] }, + horizontalRule: simple ? false : {}, }), Placeholder.configure({ placeholder, @@ -120,8 +123,9 @@ function isValidIframe(text: string) { export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> + children?: React.ReactNode // additional toolbar buttons }) { - const { editor, upload } = props + const { editor, upload, children } = props const [iframeOpen, setIframeOpen] = useState(false) return ( @@ -143,20 +147,10 @@ export function TextEditor(props: { images! </FloatingMenu> )} - <div className="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> <EditorContent editor={editor} /> - {/* Spacer element to match the height of the toolbar */} - <div className="py-2" aria-hidden="true"> - {/* Matches height of button in toolbar (1px border + 36px content height) */} - <div className="py-px"> - <div className="h-9" /> - </div> - </div> - </div> - - {/* Toolbar, with buttons for image and embeds */} - <div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> - <div className="flex items-center space-x-5"> + {/* Toolbar, with buttons for images and embeds */} + <div className="flex h-9 items-center gap-5 pl-4 pr-1"> <div className="flex items-center"> <FileUploadButton onFiles={upload.mutate} @@ -181,6 +175,8 @@ export function TextEditor(props: { <span className="sr-only">Embed an iframe</span> </button> </div> + <div className="ml-auto" /> + {children} </div> </div> </div> diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index aabb1081..edaf1fe5 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -31,9 +31,9 @@ export function FeedAnswerCommentGroup(props: { const { answer, contract, comments, tips, bets, user } = props const { username, avatarUrl, name, text } = answer - const [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState<Pick<User, 'id' | 'username'>>() const [showReply, setShowReply] = useState(false) - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [highlighted, setHighlighted] = useState(false) const router = useRouter() @@ -70,9 +70,14 @@ export function FeedAnswerCommentGroup(props: { const scrollAndOpenReplyInput = useEvent( (comment?: Comment, answer?: Answer) => { - setReplyToUsername(comment?.userUsername ?? answer?.username ?? '') + setReplyToUser( + comment + ? { id: comment.userId, username: comment.userUsername } + : answer + ? { id: answer.userId, username: answer.username } + : undefined + ) setShowReply(true) - inputRef?.focus() } ) @@ -80,7 +85,7 @@ export function FeedAnswerCommentGroup(props: { // Only show one comment input for a bet at a time if ( betsByCurrentUser.length > 1 && - inputRef?.textContent?.length === 0 && + // inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0] ?.outcome !== answer.number.toString() ) @@ -89,10 +94,6 @@ export function FeedAnswerCommentGroup(props: { // eslint-disable-next-line react-hooks/exhaustive-deps }, [betsByCurrentUser.length, user, answer.number]) - useEffect(() => { - if (showReply && inputRef) inputRef.focus() - }, [inputRef, showReply]) - useEffect(() => { if (router.asPath.endsWith(`#${answerElementId}`)) { setHighlighted(true) @@ -154,7 +155,6 @@ export function FeedAnswerCommentGroup(props: { commentsList={commentsList} betsByUserId={betsByUserId} smallAvatar={true} - truncate={false} bets={bets} tips={tips} scrollAndOpenReplyInput={scrollAndOpenReplyInput} @@ -172,12 +172,8 @@ export function FeedAnswerCommentGroup(props: { betsByCurrentUser={betsByCurrentUser} commentsByCurrentUser={commentsByCurrentUser} parentAnswerOutcome={answer.number.toString()} - replyToUsername={replyToUsername} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + replyToUser={replyToUser} + onSubmitComment={() => setShowReply(false)} /> </div> )} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index f4c6eb74..fd2dbde2 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -13,25 +13,22 @@ import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { OutcomeLabel } from 'web/components/outcome-label' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' -import { contractPath } from 'web/lib/firebase/contracts' import { firebaseLogin } from 'web/lib/firebase/users' import { createCommentOnContract, MAX_COMMENT_LENGTH, } from 'web/lib/firebase/comments' -import Textarea from 'react-expanding-textarea' -import { Linkify } from 'web/components/linkify' -import { SiteLink } from 'web/components/site-link' import { BetStatusText } from 'web/components/feed/feed-bets' import { Col } from 'web/components/layout/col' import { getProbability } from 'common/calculate' import { LoadingIndicator } from 'web/components/loading-indicator' import { PaperAirplaneIcon } from '@heroicons/react/outline' import { track } from 'web/lib/service/analytics' -import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { useWindowSize } from 'web/hooks/use-window-size' +import { Content, TextEditor, useTextEditor } from '../editor' +import { Editor } from '@tiptap/react' export function FeedCommentThread(props: { contract: Contract @@ -39,20 +36,12 @@ export function FeedCommentThread(props: { tips: CommentTipMap parentComment: Comment bets: Bet[] - truncate?: boolean smallAvatar?: boolean }) { - const { - contract, - comments, - bets, - tips, - truncate, - smallAvatar, - parentComment, - } = props + const { contract, comments, bets, tips, smallAvatar, parentComment } = props const [showReply, setShowReply] = useState(false) - const [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState<{ id: string; username: string }>() const betsByUserId = groupBy(bets, (bet) => bet.userId) const user = useUser() const commentsList = comments.filter( @@ -60,15 +49,12 @@ export function FeedCommentThread(props: { parentComment.id && comment.replyToCommentId === parentComment.id ) commentsList.unshift(parentComment) - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) + function scrollAndOpenReplyInput(comment: Comment) { - setReplyToUsername(comment.userUsername) + setReplyToUser({ id: comment.userId, username: comment.userUsername }) setShowReply(true) - inputRef?.focus() } - useEffect(() => { - if (showReply && inputRef) inputRef.focus() - }, [inputRef, showReply]) + return ( <Col className={'w-full gap-3 pr-1'}> <span @@ -81,7 +67,6 @@ export function FeedCommentThread(props: { betsByUserId={betsByUserId} tips={tips} smallAvatar={smallAvatar} - truncate={truncate} bets={bets} scrollAndOpenReplyInput={scrollAndOpenReplyInput} /> @@ -98,13 +83,9 @@ export function FeedCommentThread(props: { (c) => c.userId === user?.id )} parentCommentId={parentComment.id} - replyToUsername={replyToUsername} + replyToUser={replyToUser} parentAnswerOutcome={comments[0].answerOutcome} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + onSubmitComment={() => setShowReply(false)} /> </Col> )} @@ -121,14 +102,12 @@ export function CommentRepliesList(props: { bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean - truncate?: boolean }) { const { contract, commentsList, betsByUserId, tips, - truncate, smallAvatar, bets, scrollAndOpenReplyInput, @@ -168,7 +147,6 @@ export function CommentRepliesList(props: { : undefined } smallAvatar={smallAvatar} - truncate={truncate} /> </div> ))} @@ -182,7 +160,6 @@ export function FeedComment(props: { tips: CommentTips betsBySameUser: Bet[] probAtCreatedTime?: number - truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { @@ -192,10 +169,10 @@ export function FeedComment(props: { tips, betsBySameUser, probAtCreatedTime, - truncate, onReplyClick, } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const { text, content, userUsername, userName, userAvatarUrl, createdTime } = + comment let betOutcome: string | undefined, bought: string | undefined, money: string | undefined @@ -276,11 +253,9 @@ export function FeedComment(props: { elementId={comment.id} /> </div> - <TruncatedComment - comment={text} - moreHref={contractPath(contract)} - shouldTruncate={truncate} - /> + <div className="mt-2 text-[15px] text-gray-700"> + <Content content={content || text} /> + </div> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> {onReplyClick && ( @@ -345,8 +320,7 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: Comment[] - replyToUsername?: string - setRef?: (ref: HTMLTextAreaElement) => void + replyToUser?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment @@ -359,12 +333,18 @@ export function CommentInput(props: { commentsByCurrentUser, parentAnswerOutcome, parentCommentId, - replyToUsername, + replyToUser, onSubmitComment, - setRef, } = props const user = useUser() - const [comment, setComment] = useState('') + const { editor, upload } = useTextEditor({ + simple: true, + max: MAX_COMMENT_LENGTH, + placeholder: + !!parentCommentId || !!parentAnswerOutcome + ? 'Write a reply...' + : 'Write a comment...', + }) const [isSubmitting, setIsSubmitting] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( @@ -380,18 +360,17 @@ export function CommentInput(props: { track('sign in to comment') return await firebaseLogin() } - if (!comment || isSubmitting) return + if (!editor || editor.isEmpty || isSubmitting) return setIsSubmitting(true) await createCommentOnContract( contract.id, - comment, + editor.getJSON(), user, betId, parentAnswerOutcome, parentCommentId ) onSubmitComment?.() - setComment('') setIsSubmitting(false) } @@ -446,14 +425,12 @@ export function CommentInput(props: { )} </div> <CommentInputTextArea - commentText={comment} - setComment={setComment} - isReply={!!parentCommentId || !!parentAnswerOutcome} - replyToUsername={replyToUsername ?? ''} + editor={editor} + upload={upload} + replyToUser={replyToUser} user={user} submitComment={submitComment} isSubmitting={isSubmitting} - setRef={setRef} presetId={id} /> </div> @@ -465,94 +442,89 @@ export function CommentInput(props: { export function CommentInputTextArea(props: { user: User | undefined | null - isReply: boolean - replyToUsername: string - commentText: string - setComment: (text: string) => void + replyToUser?: { id: string; username: string } + editor: Editor | null + upload: Parameters<typeof TextEditor>[0]['upload'] submitComment: (id?: string) => void isSubmitting: boolean - setRef?: (ref: HTMLTextAreaElement) => void + submitOnEnter?: boolean presetId?: string - enterToSubmitOnDesktop?: boolean }) { const { - isReply, - setRef, user, - commentText, - setComment, + editor, + upload, submitComment, presetId, isSubmitting, - replyToUsername, - enterToSubmitOnDesktop, + submitOnEnter, + replyToUser, } = props - const { width } = useWindowSize() - const memoizedSetComment = useEvent(setComment) + const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch) + useEffect(() => { - if (!replyToUsername || !user || replyToUsername === user.username) return - const replacement = `@${replyToUsername} ` - memoizedSetComment(replacement + commentText.replace(replacement, '')) + editor?.setEditable(!isSubmitting) + }, [isSubmitting, editor]) + + const submit = () => { + submitComment(presetId) + editor?.commands?.clearContent() + } + + useEffect(() => { + if (!editor) { + return + } + // submit on Enter key + editor.setOptions({ + editorProps: { + handleKeyDown: (view, event) => { + if ( + submitOnEnter && + event.key === 'Enter' && + !event.shiftKey && + (!isMobile || event.ctrlKey || event.metaKey) && + // mention list is closed + !(view.state as any).mention$.active + ) { + submit() + event.preventDefault() + return true + } + return false + }, + }, + }) + // insert at mention + if (replyToUser) { + editor.commands.insertContentAt(0, { + type: 'mention', + attrs: { label: replyToUser.username, id: replyToUser.id }, + }) + editor.commands.focus() + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, replyToUsername, memoizedSetComment]) + }, [editor]) + return ( <> - <Row className="gap-1.5 text-gray-700"> - <Textarea - ref={setRef} - value={commentText} - onChange={(e) => setComment(e.target.value)} - className={clsx('textarea textarea-bordered w-full resize-none')} - // Make room for floating submit button. - style={{ paddingRight: 48 }} - placeholder={ - isReply - ? 'Write a reply... ' - : enterToSubmitOnDesktop - ? 'Send a message' - : 'Write a comment...' - } - autoFocus={false} - maxLength={MAX_COMMENT_LENGTH} - disabled={isSubmitting} - onKeyDown={(e) => { - if ( - (enterToSubmitOnDesktop && - e.key === 'Enter' && - !e.shiftKey && - width && - width > 768) || - (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) - ) { - e.preventDefault() - submitComment(presetId) - e.currentTarget.blur() - } - }} - /> - - <Col className={clsx('relative justify-end')}> + <div> + <TextEditor editor={editor} upload={upload}> {user && !isSubmitting && ( <button - className={clsx( - 'btn btn-ghost btn-sm absolute right-2 bottom-2 flex-row pl-2 capitalize', - !commentText && 'pointer-events-none text-gray-500' - )} - onClick={() => { - submitComment(presetId) - }} + className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" + disabled={!editor || editor.isEmpty} + onClick={submit} > - <PaperAirplaneIcon - className={'m-0 min-w-[22px] rotate-90 p-0 '} - height={25} - /> + <PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" /> </button> )} + {isSubmitting && ( <LoadingIndicator spinnerClassName={'border-gray-500'} /> )} - </Col> - </Row> + </TextEditor> + </div> <Row> {!user && ( <button @@ -567,38 +539,6 @@ export function CommentInputTextArea(props: { ) } -export function TruncatedComment(props: { - comment: string - moreHref: string - shouldTruncate?: boolean -}) { - const { comment, moreHref, shouldTruncate } = props - let truncated = comment - - // Keep descriptions to at most 400 characters - const MAX_CHARS = 400 - if (shouldTruncate && truncated.length > MAX_CHARS) { - truncated = truncated.slice(0, MAX_CHARS) - // Make sure to end on a space - const i = truncated.lastIndexOf(' ') - truncated = truncated.slice(0, i) - } - - return ( - <div - className="mt-2 whitespace-pre-line break-words text-gray-700" - style={{ fontSize: 15 }} - > - <Linkify text={truncated} /> - {truncated != comment && ( - <SiteLink href={moreHref} className="text-indigo-700"> - ... (show more) - </SiteLink> - )} - </div> - ) -} - function getBettorsLargestPositionBeforeTime( contract: Contract, createdTime: number, diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 91de63c6..db7e558b 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -5,24 +5,19 @@ import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments' -import { - CommentInputTextArea, - TruncatedComment, -} from 'web/components/feed/feed-comments' +import { CommentInputTextArea } from 'web/components/feed/feed-comments' import { track } from 'web/lib/service/analytics' import { firebaseLogin } from 'web/lib/firebase/users' - import { useRouter } from 'next/router' import clsx from 'clsx' import { UserLink } from 'web/components/user-page' - -import { groupPath } from 'web/lib/firebase/groups' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' +import { Content, useTextEditor } from 'web/components/editor' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { ChatIcon, ChevronDownIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -34,16 +29,18 @@ export function GroupChat(props: { tips: CommentTipMap }) { const { messages, user, group, tips } = props - const [messageText, setMessageText] = useState('') + const { editor, upload } = useTextEditor({ + simple: true, + placeholder: 'Send a message', + }) const [isSubmitting, setIsSubmitting] = useState(false) const [scrollToBottomRef, setScrollToBottomRef] = useState<HTMLDivElement | null>(null) const [scrollToMessageId, setScrollToMessageId] = useState('') const [scrollToMessageRef, setScrollToMessageRef] = useState<HTMLDivElement | null>(null) - const [replyToUsername, setReplyToUsername] = useState('') - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) - const [groupedMessages, setGroupedMessages] = useState<Comment[]>([]) + const [replyToUser, setReplyToUser] = useState<any>() + const router = useRouter() const isMember = user && group.memberIds.includes(user?.id) @@ -54,25 +51,26 @@ export function GroupChat(props: { const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight - useMemo(() => { + // array of groups, where each group is an array of messages that are displayed as one + const groupedMessages = useMemo(() => { // Group messages with createdTime within 2 minutes of each other. - const tempMessages = [] + const tempGrouped: Comment[][] = [] for (let i = 0; i < messages.length; i++) { const message = messages[i] - if (i === 0) tempMessages.push({ ...message }) + if (i === 0) tempGrouped.push([message]) else { const prevMessage = messages[i - 1] const diff = message.createdTime - prevMessage.createdTime const creatorsMatch = message.userId === prevMessage.userId if (diff < 2 * 60 * 1000 && creatorsMatch) { - tempMessages[tempMessages.length - 1].text += `\n${message.text}` + tempGrouped.at(-1)?.push(message) } else { - tempMessages.push({ ...message }) + tempGrouped.push([message]) } } } - setGroupedMessages(tempMessages) + return tempGrouped }, [messages]) useEffect(() => { @@ -94,11 +92,12 @@ export function GroupChat(props: { useEffect(() => { // is mobile? - if (inputRef && width && width > 720) inputRef.focus() - }, [inputRef, width]) + if (width && width > 720) focusInput() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [width]) function onReplyClick(comment: Comment) { - setReplyToUsername(comment.userUsername) + setReplyToUser({ id: comment.userId, username: comment.userUsername }) } async function submitMessage() { @@ -106,13 +105,16 @@ export function GroupChat(props: { track('sign in to comment') return await firebaseLogin() } - if (!messageText || isSubmitting) return + if (!editor || editor.isEmpty || isSubmitting) return setIsSubmitting(true) - await createCommentOnGroup(group.id, messageText, user) - setMessageText('') + await createCommentOnGroup(group.id, editor.getJSON(), user) + editor.commands.clearContent() setIsSubmitting(false) - setReplyToUsername('') - inputRef?.focus() + setReplyToUser(undefined) + focusInput() + } + function focusInput() { + editor?.commands.focus() } return ( @@ -123,20 +125,20 @@ export function GroupChat(props: { } ref={setScrollToBottomRef} > - {groupedMessages.map((message) => ( + {groupedMessages.map((messages) => ( <GroupMessage user={user} - key={message.id} - comment={message} + key={`group ${messages[0].id}`} + comments={messages} group={group} onReplyClick={onReplyClick} - highlight={message.id === scrollToMessageId} + highlight={messages[0].id === scrollToMessageId} setRef={ - scrollToMessageId === message.id + scrollToMessageId === messages[0].id ? setScrollToMessageRef : undefined } - tips={tips[message.id] ?? {}} + tips={tips[messages[0].id] ?? {}} /> ))} {messages.length === 0 && ( @@ -144,7 +146,7 @@ export function GroupChat(props: { No messages yet. Why not{isMember ? ` ` : ' join and '} <button className={'cursor-pointer font-bold text-gray-700'} - onClick={() => inputRef?.focus()} + onClick={focusInput} > add one? </button> @@ -162,15 +164,13 @@ export function GroupChat(props: { </div> <div className={'flex-1'}> <CommentInputTextArea - commentText={messageText} - setComment={setMessageText} - isReply={false} + editor={editor} + upload={upload} user={user} - replyToUsername={replyToUsername} + replyToUser={replyToUser} submitComment={submitMessage} isSubmitting={isSubmitting} - enterToSubmitOnDesktop={true} - setRef={setInputRef} + submitOnEnter /> </div> </div> @@ -292,16 +292,18 @@ function GroupChatNotificationsIcon(props: { const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined - comment: Comment + comments: Comment[] group: Group onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void highlight?: boolean tips: CommentTips }) { - const { comment, onReplyClick, group, setRef, highlight, user, tips } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment - const isCreatorsComment = user && comment.userId === user.id + const { comments, onReplyClick, group, setRef, highlight, user, tips } = props + const first = comments[0] + const { id, userUsername, userName, userAvatarUrl, createdTime } = first + + const isCreatorsComment = user && first.userId === user.id return ( <Col ref={setRef} @@ -331,23 +333,21 @@ const GroupMessage = memo(function GroupMessage_(props: { prefix={'group'} slug={group.slug} createdTime={createdTime} - elementId={comment.id} - /> - </Row> - <Row className={'text-black'}> - <TruncatedComment - comment={text} - moreHref={groupPath(group.slug)} - shouldTruncate={false} + elementId={id} /> </Row> + <div className="mt-2 text-black"> + {comments.map((comment) => ( + <Content content={comment.content || comment.text} /> + ))} + </div> <Row> {!isCreatorsComment && onReplyClick && ( <button className={ 'self-start py-1 text-xs font-bold text-gray-500 hover:underline' } - onClick={() => onReplyClick(comment)} + onClick={() => onReplyClick(first)} > Reply </button> @@ -357,7 +357,7 @@ const GroupMessage = memo(function GroupMessage_(props: { {formatMoney(sum(Object.values(tips)))} </span> )} - {!isCreatorsComment && <Tipper comment={comment} tips={tips} />} + {!isCreatorsComment && <Tipper comment={first} tips={tips} />} </Row> </Col> ) diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index 5775a2bb..e82c6d45 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -14,6 +14,7 @@ import { User } from 'common/user' import { Comment } from 'common/comment' import { removeUndefinedProps } from 'common/util/object' import { track } from '@amplitude/analytics-browser' +import { JSONContent } from '@tiptap/react' export type { Comment } @@ -21,7 +22,7 @@ export const MAX_COMMENT_LENGTH = 10000 export async function createCommentOnContract( contractId: string, - text: string, + content: JSONContent, commenter: User, betId?: string, answerOutcome?: string, @@ -34,7 +35,7 @@ export async function createCommentOnContract( id: ref.id, contractId, userId: commenter.id, - text: text.slice(0, MAX_COMMENT_LENGTH), + content: content, createdTime: Date.now(), userName: commenter.name, userUsername: commenter.username, @@ -53,7 +54,7 @@ export async function createCommentOnContract( } export async function createCommentOnGroup( groupId: string, - text: string, + content: JSONContent, user: User, replyToCommentId?: string ) { @@ -62,7 +63,7 @@ export async function createCommentOnGroup( id: ref.id, groupId, userId: user.id, - text: text.slice(0, MAX_COMMENT_LENGTH), + content: content, createdTime: Date.now(), userName: user.name, userUsername: user.username, diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 0da6c994..5866f899 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -354,7 +354,6 @@ function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} - truncate={false} smallAvatar={false} /> </div> From 33906adfe489cbc3489bc014c4a3253d96660ec0 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 4 Aug 2022 16:49:59 -0700 Subject: [PATCH 416/519] Revert "Switch comments/chat to rich text editor (#703)" This reverts commit f52da72115bfacb0af5a4d54c137a936b33d9eee. --- common/comment.ts | 6 +- functions/src/create-notification.ts | 19 +- functions/src/emails.ts | 4 +- .../src/on-create-comment-on-contract.ts | 11 +- web/components/comments-list.tsx | 7 +- .../contract/contract-leaderboard.tsx | 1 + web/components/editor.tsx | 30 ++- .../feed/feed-answer-comment-group.tsx | 28 +- web/components/feed/feed-comments.tsx | 242 +++++++++++------- web/components/groups/group-chat.tsx | 104 ++++---- web/lib/firebase/comments.ts | 9 +- web/pages/[username]/[contractSlug].tsx | 1 + 12 files changed, 266 insertions(+), 196 deletions(-) diff --git a/common/comment.ts b/common/comment.ts index a217b292..0d0c4daf 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,5 +1,3 @@ -import type { JSONContent } from '@tiptap/core' - // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. export type Comment = { @@ -11,9 +9,7 @@ export type Comment = { replyToCommentId?: string userId: string - /** @deprecated - content now stored as JSON in content*/ - text?: string - content: JSONContent + text: string createdTime: number // Denormalized, for rendering comments diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 6e312906..e16920f7 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -7,7 +7,7 @@ import { } from '../../common/notification' import { User } from '../../common/user' import { Contract } from '../../common/contract' -import { getValues } from './utils' +import { getUserByUsername, getValues } from './utils' import { Comment } from '../../common/comment' import { uniq } from 'lodash' import { Bet, LimitBet } from '../../common/bet' @@ -17,7 +17,6 @@ import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' import { Group, GROUP_CHAT_SLUG } from '../../common/group' import { Challenge } from '../../common/challenge' -import { richTextToString } from 'common/util/parse' const firestore = admin.firestore() type user_to_reason_texts = { @@ -156,6 +155,17 @@ export const createNotification = async ( } } + /** @deprecated parse from rich text instead */ + const parseMentions = async (source: string) => { + const mentions = source.match(/@\w+/g) + if (!mentions) return [] + return Promise.all( + mentions.map( + async (username) => (await getUserByUsername(username.slice(1)))?.id + ) + ) + } + const notifyTaggedUsers = ( userToReasonTexts: user_to_reason_texts, userIds: (string | undefined)[] @@ -291,7 +301,8 @@ export const createNotification = async ( if (sourceType === 'comment') { if (recipients?.[0] && relatedSourceType) notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType) - if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? []) + if (sourceText) + notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText)) } await notifyContractCreator(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) @@ -416,7 +427,7 @@ export const createGroupCommentNotification = async ( sourceUserName: fromUser.name, sourceUserUsername: fromUser.username, sourceUserAvatarUrl: fromUser.avatarUrl, - sourceText: richTextToString(comment.content), + sourceText: comment.text, sourceSlug, sourceTitle: `${group.name}`, isSeenOnHref: sourceSlug, diff --git a/functions/src/emails.ts b/functions/src/emails.ts index d594ae65..b7469e9f 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -17,7 +17,6 @@ import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' import { getFunctionUrl } from '../../common/api' -import { richTextToString } from 'common/util/parse' const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe') @@ -292,8 +291,7 @@ export const sendNewCommentEmail = async ( const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator - const { content } = comment - const text = richTextToString(content) + const { text } = comment let betDescription = '' if (bet) { diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index a8bc567e..4719fd08 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -1,13 +1,13 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { compact, uniq } from 'lodash' +import { uniq } from 'lodash' + import { getContract, getUser, getValues } from './utils' import { Comment } from '../../common/comment' import { sendNewCommentEmail } from './emails' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' import { createNotification } from './create-notification' -import { parseMentions, richTextToString } from 'common/util/parse' const firestore = admin.firestore() @@ -71,10 +71,7 @@ export const onCreateCommentOnContract = functions const repliedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId - - const recipients = uniq( - compact([...parseMentions(comment.content), repliedUserId]) - ) + const recipients = repliedUserId ? [repliedUserId] : [] await createNotification( comment.id, @@ -82,7 +79,7 @@ export const onCreateCommentOnContract = functions 'created', commentCreator, eventId, - richTextToString(comment.content), + comment.text, { contract, relatedSourceType, recipients } ) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index 2a467f6d..f8e1d7e1 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -8,8 +8,8 @@ import { RelativeTimestamp } from './relative-timestamp' import { UserLink } from './user-page' import { User } from 'common/user' import { Col } from './layout/col' +import { Linkify } from './linkify' import { groupBy } from 'lodash' -import { Content } from './editor' export function UserCommentsList(props: { user: User @@ -50,8 +50,7 @@ export function UserCommentsList(props: { function ProfileComment(props: { comment: Comment; className?: string }) { const { comment, className } = props - const { text, content, userUsername, userName, userAvatarUrl, createdTime } = - comment + const { text, userUsername, userName, userAvatarUrl, createdTime } = comment // TODO: find and attach relevant bets by comment betId at some point return ( <Row className={className}> @@ -65,7 +64,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) { />{' '} <RelativeTimestamp time={createdTime} /> </p> - <Content content={content || text} /> + <Linkify text={text} /> </div> </Row> ) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 6f1a778d..deb9b857 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -107,6 +107,7 @@ export function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} + truncate={false} smallAvatar={false} /> </div> diff --git a/web/components/editor.tsx b/web/components/editor.tsx index f71e8589..963cea7e 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -41,16 +41,14 @@ export function useTextEditor(props: { max?: number defaultValue?: Content disabled?: boolean - simple?: boolean }) { - const { placeholder, max, defaultValue = '', disabled, simple } = props + const { placeholder, max, defaultValue = '', disabled } = props const users = useUsers() const editorClass = clsx( proseClass, - !simple && 'min-h-[6em]', - 'outline-none pt-2 px-4' + 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' ) const editor = useEditor( @@ -58,8 +56,7 @@ export function useTextEditor(props: { editorProps: { attributes: { class: editorClass } }, extensions: [ StarterKit.configure({ - heading: simple ? false : { levels: [1, 2, 3] }, - horizontalRule: simple ? false : {}, + heading: { levels: [1, 2, 3] }, }), Placeholder.configure({ placeholder, @@ -123,9 +120,8 @@ function isValidIframe(text: string) { export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> - children?: React.ReactNode // additional toolbar buttons }) { - const { editor, upload, children } = props + const { editor, upload } = props const [iframeOpen, setIframeOpen] = useState(false) return ( @@ -147,10 +143,20 @@ export function TextEditor(props: { images! </FloatingMenu> )} - <div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <div className="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> <EditorContent editor={editor} /> - {/* Toolbar, with buttons for images and embeds */} - <div className="flex h-9 items-center gap-5 pl-4 pr-1"> + {/* Spacer element to match the height of the toolbar */} + <div className="py-2" aria-hidden="true"> + {/* Matches height of button in toolbar (1px border + 36px content height) */} + <div className="py-px"> + <div className="h-9" /> + </div> + </div> + </div> + + {/* Toolbar, with buttons for image and embeds */} + <div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> + <div className="flex items-center space-x-5"> <div className="flex items-center"> <FileUploadButton onFiles={upload.mutate} @@ -175,8 +181,6 @@ export function TextEditor(props: { <span className="sr-only">Embed an iframe</span> </button> </div> - <div className="ml-auto" /> - {children} </div> </div> </div> diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index edaf1fe5..aabb1081 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -31,9 +31,9 @@ export function FeedAnswerCommentGroup(props: { const { answer, contract, comments, tips, bets, user } = props const { username, avatarUrl, name, text } = answer - const [replyToUser, setReplyToUser] = - useState<Pick<User, 'id' | 'username'>>() + const [replyToUsername, setReplyToUsername] = useState('') const [showReply, setShowReply] = useState(false) + const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [highlighted, setHighlighted] = useState(false) const router = useRouter() @@ -70,14 +70,9 @@ export function FeedAnswerCommentGroup(props: { const scrollAndOpenReplyInput = useEvent( (comment?: Comment, answer?: Answer) => { - setReplyToUser( - comment - ? { id: comment.userId, username: comment.userUsername } - : answer - ? { id: answer.userId, username: answer.username } - : undefined - ) + setReplyToUsername(comment?.userUsername ?? answer?.username ?? '') setShowReply(true) + inputRef?.focus() } ) @@ -85,7 +80,7 @@ export function FeedAnswerCommentGroup(props: { // Only show one comment input for a bet at a time if ( betsByCurrentUser.length > 1 && - // inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty + inputRef?.textContent?.length === 0 && betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0] ?.outcome !== answer.number.toString() ) @@ -94,6 +89,10 @@ export function FeedAnswerCommentGroup(props: { // eslint-disable-next-line react-hooks/exhaustive-deps }, [betsByCurrentUser.length, user, answer.number]) + useEffect(() => { + if (showReply && inputRef) inputRef.focus() + }, [inputRef, showReply]) + useEffect(() => { if (router.asPath.endsWith(`#${answerElementId}`)) { setHighlighted(true) @@ -155,6 +154,7 @@ export function FeedAnswerCommentGroup(props: { commentsList={commentsList} betsByUserId={betsByUserId} smallAvatar={true} + truncate={false} bets={bets} tips={tips} scrollAndOpenReplyInput={scrollAndOpenReplyInput} @@ -172,8 +172,12 @@ export function FeedAnswerCommentGroup(props: { betsByCurrentUser={betsByCurrentUser} commentsByCurrentUser={commentsByCurrentUser} parentAnswerOutcome={answer.number.toString()} - replyToUser={replyToUser} - onSubmitComment={() => setShowReply(false)} + replyToUsername={replyToUsername} + setRef={setInputRef} + onSubmitComment={() => { + setShowReply(false) + setReplyToUsername('') + }} /> </div> )} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index fd2dbde2..f4c6eb74 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -13,22 +13,25 @@ import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { OutcomeLabel } from 'web/components/outcome-label' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' +import { contractPath } from 'web/lib/firebase/contracts' import { firebaseLogin } from 'web/lib/firebase/users' import { createCommentOnContract, MAX_COMMENT_LENGTH, } from 'web/lib/firebase/comments' +import Textarea from 'react-expanding-textarea' +import { Linkify } from 'web/components/linkify' +import { SiteLink } from 'web/components/site-link' import { BetStatusText } from 'web/components/feed/feed-bets' import { Col } from 'web/components/layout/col' import { getProbability } from 'common/calculate' import { LoadingIndicator } from 'web/components/loading-indicator' import { PaperAirplaneIcon } from '@heroicons/react/outline' import { track } from 'web/lib/service/analytics' +import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { useWindowSize } from 'web/hooks/use-window-size' -import { Content, TextEditor, useTextEditor } from '../editor' -import { Editor } from '@tiptap/react' export function FeedCommentThread(props: { contract: Contract @@ -36,12 +39,20 @@ export function FeedCommentThread(props: { tips: CommentTipMap parentComment: Comment bets: Bet[] + truncate?: boolean smallAvatar?: boolean }) { - const { contract, comments, bets, tips, smallAvatar, parentComment } = props + const { + contract, + comments, + bets, + tips, + truncate, + smallAvatar, + parentComment, + } = props const [showReply, setShowReply] = useState(false) - const [replyToUser, setReplyToUser] = - useState<{ id: string; username: string }>() + const [replyToUsername, setReplyToUsername] = useState('') const betsByUserId = groupBy(bets, (bet) => bet.userId) const user = useUser() const commentsList = comments.filter( @@ -49,12 +60,15 @@ export function FeedCommentThread(props: { parentComment.id && comment.replyToCommentId === parentComment.id ) commentsList.unshift(parentComment) - + const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) function scrollAndOpenReplyInput(comment: Comment) { - setReplyToUser({ id: comment.userId, username: comment.userUsername }) + setReplyToUsername(comment.userUsername) setShowReply(true) + inputRef?.focus() } - + useEffect(() => { + if (showReply && inputRef) inputRef.focus() + }, [inputRef, showReply]) return ( <Col className={'w-full gap-3 pr-1'}> <span @@ -67,6 +81,7 @@ export function FeedCommentThread(props: { betsByUserId={betsByUserId} tips={tips} smallAvatar={smallAvatar} + truncate={truncate} bets={bets} scrollAndOpenReplyInput={scrollAndOpenReplyInput} /> @@ -83,9 +98,13 @@ export function FeedCommentThread(props: { (c) => c.userId === user?.id )} parentCommentId={parentComment.id} - replyToUser={replyToUser} + replyToUsername={replyToUsername} parentAnswerOutcome={comments[0].answerOutcome} - onSubmitComment={() => setShowReply(false)} + setRef={setInputRef} + onSubmitComment={() => { + setShowReply(false) + setReplyToUsername('') + }} /> </Col> )} @@ -102,12 +121,14 @@ export function CommentRepliesList(props: { bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean + truncate?: boolean }) { const { contract, commentsList, betsByUserId, tips, + truncate, smallAvatar, bets, scrollAndOpenReplyInput, @@ -147,6 +168,7 @@ export function CommentRepliesList(props: { : undefined } smallAvatar={smallAvatar} + truncate={truncate} /> </div> ))} @@ -160,6 +182,7 @@ export function FeedComment(props: { tips: CommentTips betsBySameUser: Bet[] probAtCreatedTime?: number + truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { @@ -169,10 +192,10 @@ export function FeedComment(props: { tips, betsBySameUser, probAtCreatedTime, + truncate, onReplyClick, } = props - const { text, content, userUsername, userName, userAvatarUrl, createdTime } = - comment + const { text, userUsername, userName, userAvatarUrl, createdTime } = comment let betOutcome: string | undefined, bought: string | undefined, money: string | undefined @@ -253,9 +276,11 @@ export function FeedComment(props: { elementId={comment.id} /> </div> - <div className="mt-2 text-[15px] text-gray-700"> - <Content content={content || text} /> - </div> + <TruncatedComment + comment={text} + moreHref={contractPath(contract)} + shouldTruncate={truncate} + /> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> {onReplyClick && ( @@ -320,7 +345,8 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: Comment[] - replyToUser?: { id: string; username: string } + replyToUsername?: string + setRef?: (ref: HTMLTextAreaElement) => void // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment @@ -333,18 +359,12 @@ export function CommentInput(props: { commentsByCurrentUser, parentAnswerOutcome, parentCommentId, - replyToUser, + replyToUsername, onSubmitComment, + setRef, } = props const user = useUser() - const { editor, upload } = useTextEditor({ - simple: true, - max: MAX_COMMENT_LENGTH, - placeholder: - !!parentCommentId || !!parentAnswerOutcome - ? 'Write a reply...' - : 'Write a comment...', - }) + const [comment, setComment] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( @@ -360,17 +380,18 @@ export function CommentInput(props: { track('sign in to comment') return await firebaseLogin() } - if (!editor || editor.isEmpty || isSubmitting) return + if (!comment || isSubmitting) return setIsSubmitting(true) await createCommentOnContract( contract.id, - editor.getJSON(), + comment, user, betId, parentAnswerOutcome, parentCommentId ) onSubmitComment?.() + setComment('') setIsSubmitting(false) } @@ -425,12 +446,14 @@ export function CommentInput(props: { )} </div> <CommentInputTextArea - editor={editor} - upload={upload} - replyToUser={replyToUser} + commentText={comment} + setComment={setComment} + isReply={!!parentCommentId || !!parentAnswerOutcome} + replyToUsername={replyToUsername ?? ''} user={user} submitComment={submitComment} isSubmitting={isSubmitting} + setRef={setRef} presetId={id} /> </div> @@ -442,89 +465,94 @@ export function CommentInput(props: { export function CommentInputTextArea(props: { user: User | undefined | null - replyToUser?: { id: string; username: string } - editor: Editor | null - upload: Parameters<typeof TextEditor>[0]['upload'] + isReply: boolean + replyToUsername: string + commentText: string + setComment: (text: string) => void submitComment: (id?: string) => void isSubmitting: boolean - submitOnEnter?: boolean + setRef?: (ref: HTMLTextAreaElement) => void presetId?: string + enterToSubmitOnDesktop?: boolean }) { const { + isReply, + setRef, user, - editor, - upload, + commentText, + setComment, submitComment, presetId, isSubmitting, - submitOnEnter, - replyToUser, + replyToUsername, + enterToSubmitOnDesktop, } = props - const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch) - + const { width } = useWindowSize() + const memoizedSetComment = useEvent(setComment) useEffect(() => { - editor?.setEditable(!isSubmitting) - }, [isSubmitting, editor]) - - const submit = () => { - submitComment(presetId) - editor?.commands?.clearContent() - } - - useEffect(() => { - if (!editor) { - return - } - // submit on Enter key - editor.setOptions({ - editorProps: { - handleKeyDown: (view, event) => { - if ( - submitOnEnter && - event.key === 'Enter' && - !event.shiftKey && - (!isMobile || event.ctrlKey || event.metaKey) && - // mention list is closed - !(view.state as any).mention$.active - ) { - submit() - event.preventDefault() - return true - } - return false - }, - }, - }) - // insert at mention - if (replyToUser) { - editor.commands.insertContentAt(0, { - type: 'mention', - attrs: { label: replyToUser.username, id: replyToUser.id }, - }) - editor.commands.focus() - } + if (!replyToUsername || !user || replyToUsername === user.username) return + const replacement = `@${replyToUsername} ` + memoizedSetComment(replacement + commentText.replace(replacement, '')) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editor]) - + }, [user, replyToUsername, memoizedSetComment]) return ( <> - <div> - <TextEditor editor={editor} upload={upload}> + <Row className="gap-1.5 text-gray-700"> + <Textarea + ref={setRef} + value={commentText} + onChange={(e) => setComment(e.target.value)} + className={clsx('textarea textarea-bordered w-full resize-none')} + // Make room for floating submit button. + style={{ paddingRight: 48 }} + placeholder={ + isReply + ? 'Write a reply... ' + : enterToSubmitOnDesktop + ? 'Send a message' + : 'Write a comment...' + } + autoFocus={false} + maxLength={MAX_COMMENT_LENGTH} + disabled={isSubmitting} + onKeyDown={(e) => { + if ( + (enterToSubmitOnDesktop && + e.key === 'Enter' && + !e.shiftKey && + width && + width > 768) || + (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) + ) { + e.preventDefault() + submitComment(presetId) + e.currentTarget.blur() + } + }} + /> + + <Col className={clsx('relative justify-end')}> {user && !isSubmitting && ( <button - className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" - disabled={!editor || editor.isEmpty} - onClick={submit} + className={clsx( + 'btn btn-ghost btn-sm absolute right-2 bottom-2 flex-row pl-2 capitalize', + !commentText && 'pointer-events-none text-gray-500' + )} + onClick={() => { + submitComment(presetId) + }} > - <PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" /> + <PaperAirplaneIcon + className={'m-0 min-w-[22px] rotate-90 p-0 '} + height={25} + /> </button> )} - {isSubmitting && ( <LoadingIndicator spinnerClassName={'border-gray-500'} /> )} - </TextEditor> - </div> + </Col> + </Row> <Row> {!user && ( <button @@ -539,6 +567,38 @@ export function CommentInputTextArea(props: { ) } +export function TruncatedComment(props: { + comment: string + moreHref: string + shouldTruncate?: boolean +}) { + const { comment, moreHref, shouldTruncate } = props + let truncated = comment + + // Keep descriptions to at most 400 characters + const MAX_CHARS = 400 + if (shouldTruncate && truncated.length > MAX_CHARS) { + truncated = truncated.slice(0, MAX_CHARS) + // Make sure to end on a space + const i = truncated.lastIndexOf(' ') + truncated = truncated.slice(0, i) + } + + return ( + <div + className="mt-2 whitespace-pre-line break-words text-gray-700" + style={{ fontSize: 15 }} + > + <Linkify text={truncated} /> + {truncated != comment && ( + <SiteLink href={moreHref} className="text-indigo-700"> + ... (show more) + </SiteLink> + )} + </div> + ) +} + function getBettorsLargestPositionBeforeTime( contract: Contract, createdTime: number, diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index db7e558b..91de63c6 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -5,19 +5,24 @@ import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments' -import { CommentInputTextArea } from 'web/components/feed/feed-comments' +import { + CommentInputTextArea, + TruncatedComment, +} from 'web/components/feed/feed-comments' import { track } from 'web/lib/service/analytics' import { firebaseLogin } from 'web/lib/firebase/users' + import { useRouter } from 'next/router' import clsx from 'clsx' import { UserLink } from 'web/components/user-page' + +import { groupPath } from 'web/lib/firebase/groups' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' -import { Content, useTextEditor } from 'web/components/editor' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { ChatIcon, ChevronDownIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -29,18 +34,16 @@ export function GroupChat(props: { tips: CommentTipMap }) { const { messages, user, group, tips } = props - const { editor, upload } = useTextEditor({ - simple: true, - placeholder: 'Send a message', - }) + const [messageText, setMessageText] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [scrollToBottomRef, setScrollToBottomRef] = useState<HTMLDivElement | null>(null) const [scrollToMessageId, setScrollToMessageId] = useState('') const [scrollToMessageRef, setScrollToMessageRef] = useState<HTMLDivElement | null>(null) - const [replyToUser, setReplyToUser] = useState<any>() - + const [replyToUsername, setReplyToUsername] = useState('') + const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) + const [groupedMessages, setGroupedMessages] = useState<Comment[]>([]) const router = useRouter() const isMember = user && group.memberIds.includes(user?.id) @@ -51,26 +54,25 @@ export function GroupChat(props: { const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight - // array of groups, where each group is an array of messages that are displayed as one - const groupedMessages = useMemo(() => { + useMemo(() => { // Group messages with createdTime within 2 minutes of each other. - const tempGrouped: Comment[][] = [] + const tempMessages = [] for (let i = 0; i < messages.length; i++) { const message = messages[i] - if (i === 0) tempGrouped.push([message]) + if (i === 0) tempMessages.push({ ...message }) else { const prevMessage = messages[i - 1] const diff = message.createdTime - prevMessage.createdTime const creatorsMatch = message.userId === prevMessage.userId if (diff < 2 * 60 * 1000 && creatorsMatch) { - tempGrouped.at(-1)?.push(message) + tempMessages[tempMessages.length - 1].text += `\n${message.text}` } else { - tempGrouped.push([message]) + tempMessages.push({ ...message }) } } } - return tempGrouped + setGroupedMessages(tempMessages) }, [messages]) useEffect(() => { @@ -92,12 +94,11 @@ export function GroupChat(props: { useEffect(() => { // is mobile? - if (width && width > 720) focusInput() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [width]) + if (inputRef && width && width > 720) inputRef.focus() + }, [inputRef, width]) function onReplyClick(comment: Comment) { - setReplyToUser({ id: comment.userId, username: comment.userUsername }) + setReplyToUsername(comment.userUsername) } async function submitMessage() { @@ -105,16 +106,13 @@ export function GroupChat(props: { track('sign in to comment') return await firebaseLogin() } - if (!editor || editor.isEmpty || isSubmitting) return + if (!messageText || isSubmitting) return setIsSubmitting(true) - await createCommentOnGroup(group.id, editor.getJSON(), user) - editor.commands.clearContent() + await createCommentOnGroup(group.id, messageText, user) + setMessageText('') setIsSubmitting(false) - setReplyToUser(undefined) - focusInput() - } - function focusInput() { - editor?.commands.focus() + setReplyToUsername('') + inputRef?.focus() } return ( @@ -125,20 +123,20 @@ export function GroupChat(props: { } ref={setScrollToBottomRef} > - {groupedMessages.map((messages) => ( + {groupedMessages.map((message) => ( <GroupMessage user={user} - key={`group ${messages[0].id}`} - comments={messages} + key={message.id} + comment={message} group={group} onReplyClick={onReplyClick} - highlight={messages[0].id === scrollToMessageId} + highlight={message.id === scrollToMessageId} setRef={ - scrollToMessageId === messages[0].id + scrollToMessageId === message.id ? setScrollToMessageRef : undefined } - tips={tips[messages[0].id] ?? {}} + tips={tips[message.id] ?? {}} /> ))} {messages.length === 0 && ( @@ -146,7 +144,7 @@ export function GroupChat(props: { No messages yet. Why not{isMember ? ` ` : ' join and '} <button className={'cursor-pointer font-bold text-gray-700'} - onClick={focusInput} + onClick={() => inputRef?.focus()} > add one? </button> @@ -164,13 +162,15 @@ export function GroupChat(props: { </div> <div className={'flex-1'}> <CommentInputTextArea - editor={editor} - upload={upload} + commentText={messageText} + setComment={setMessageText} + isReply={false} user={user} - replyToUser={replyToUser} + replyToUsername={replyToUsername} submitComment={submitMessage} isSubmitting={isSubmitting} - submitOnEnter + enterToSubmitOnDesktop={true} + setRef={setInputRef} /> </div> </div> @@ -292,18 +292,16 @@ function GroupChatNotificationsIcon(props: { const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined - comments: Comment[] + comment: Comment group: Group onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void highlight?: boolean tips: CommentTips }) { - const { comments, onReplyClick, group, setRef, highlight, user, tips } = props - const first = comments[0] - const { id, userUsername, userName, userAvatarUrl, createdTime } = first - - const isCreatorsComment = user && first.userId === user.id + const { comment, onReplyClick, group, setRef, highlight, user, tips } = props + const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const isCreatorsComment = user && comment.userId === user.id return ( <Col ref={setRef} @@ -333,21 +331,23 @@ const GroupMessage = memo(function GroupMessage_(props: { prefix={'group'} slug={group.slug} createdTime={createdTime} - elementId={id} + elementId={comment.id} + /> + </Row> + <Row className={'text-black'}> + <TruncatedComment + comment={text} + moreHref={groupPath(group.slug)} + shouldTruncate={false} /> </Row> - <div className="mt-2 text-black"> - {comments.map((comment) => ( - <Content content={comment.content || comment.text} /> - ))} - </div> <Row> {!isCreatorsComment && onReplyClick && ( <button className={ 'self-start py-1 text-xs font-bold text-gray-500 hover:underline' } - onClick={() => onReplyClick(first)} + onClick={() => onReplyClick(comment)} > Reply </button> @@ -357,7 +357,7 @@ const GroupMessage = memo(function GroupMessage_(props: { {formatMoney(sum(Object.values(tips)))} </span> )} - {!isCreatorsComment && <Tipper comment={first} tips={tips} />} + {!isCreatorsComment && <Tipper comment={comment} tips={tips} />} </Row> </Col> ) diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index e82c6d45..5775a2bb 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -14,7 +14,6 @@ import { User } from 'common/user' import { Comment } from 'common/comment' import { removeUndefinedProps } from 'common/util/object' import { track } from '@amplitude/analytics-browser' -import { JSONContent } from '@tiptap/react' export type { Comment } @@ -22,7 +21,7 @@ export const MAX_COMMENT_LENGTH = 10000 export async function createCommentOnContract( contractId: string, - content: JSONContent, + text: string, commenter: User, betId?: string, answerOutcome?: string, @@ -35,7 +34,7 @@ export async function createCommentOnContract( id: ref.id, contractId, userId: commenter.id, - content: content, + text: text.slice(0, MAX_COMMENT_LENGTH), createdTime: Date.now(), userName: commenter.name, userUsername: commenter.username, @@ -54,7 +53,7 @@ export async function createCommentOnContract( } export async function createCommentOnGroup( groupId: string, - content: JSONContent, + text: string, user: User, replyToCommentId?: string ) { @@ -63,7 +62,7 @@ export async function createCommentOnGroup( id: ref.id, groupId, userId: user.id, - content: content, + text: text.slice(0, MAX_COMMENT_LENGTH), createdTime: Date.now(), userName: user.name, userUsername: user.username, diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 5866f899..0da6c994 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -354,6 +354,7 @@ function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} + truncate={false} smallAvatar={false} /> </div> From 1e66f4d1402618b641d8fbefc3d01d9f16c0cee5 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:22:45 -0500 Subject: [PATCH 417/519] Share row (#715) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * market share row * Add lite market endpoint * 500 mana email (#687) * Create 500-mana.html * Update 500-mana.html Fixed typos and links not working * Added "create a good market" guide added page creating-market.html For Stephen to set up condition (email 3 days after signing up) * Update 500-mana.html updated 500 Mana email (still need to make changes to create market guide) * email changes * sendOneWeekBonusEmail logic * add dayjs as dependency * don't use mailgun scheduling Co-authored-by: mantikoros <sgrugett@gmail.com> * Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros <sgrugett@gmail.com> * See challenges you've accepted too * Remove max height * Notify mentioned users on market publish (#683) * Add function to parse at mentions * Notify mentioned users on market create - refactor createNotification to accept list of recipients' ids * Switch comments/chat to rich text editor (#703) * Switch comments/chat to rich text editor * Remove TruncatedComment * Re-add submit on enter * Insert at mention on reply * Update editor style for send button * only submit on enter in chat * code review: refactor * use more specific type for upload * fix ESlint and errors from merge * fix trigger on every render eslint warning * Notify people mentioned in comment * fix type errors * Revert "Switch comments/chat to rich text editor (#703)" This reverts commit f52da72115bfacb0af5a4d54c137a936b33d9eee. * merge conflict * share modal * merge issue * eslint * bigger link icion Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: James Grugett <jahooma@gmail.com> Co-authored-by: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com> --- web/components/button.tsx | 2 +- .../challenges/create-challenge-modal.tsx | 248 ++++++++++++++++++ web/components/contract/contract-details.tsx | 12 +- .../contract/contract-info-dialog.tsx | 31 +-- web/components/contract/contract-overview.tsx | 52 +--- web/components/contract/share-modal.tsx | 77 ++++++ web/components/contract/share-row.tsx | 59 +++++ web/pages/challenges/index.tsx | 1 + 8 files changed, 394 insertions(+), 88 deletions(-) create mode 100644 web/components/challenges/create-challenge-modal.tsx create mode 100644 web/components/contract/share-modal.tsx create mode 100644 web/components/contract/share-row.tsx diff --git a/web/components/button.tsx b/web/components/button.tsx index 5c1e15f8..462670bd 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -52,7 +52,7 @@ export function Button(props: { color === 'gradient' && 'bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700', color === 'gray-white' && - 'text-greyscale-6 hover:bg-greyscale-2 bg-white', + 'border-none bg-white text-gray-500 shadow-none hover:bg-gray-200', className )} disabled={disabled} diff --git a/web/components/challenges/create-challenge-modal.tsx b/web/components/challenges/create-challenge-modal.tsx new file mode 100644 index 00000000..3a0e857a --- /dev/null +++ b/web/components/challenges/create-challenge-modal.tsx @@ -0,0 +1,248 @@ +import clsx from 'clsx' +import dayjs from 'dayjs' +import React, { useEffect, useState } from 'react' +import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline' +import toast from 'react-hot-toast' + +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { Title } from '../title' +import { User } from 'common/user' +import { Modal } from 'web/components/layout/modal' +import { Button } from '../button' +import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' +import { BinaryContract } from 'common/contract' +import { SiteLink } from 'web/components/site-link' +import { formatMoney } from 'common/util/format' +import { NoLabel, YesLabel } from '../outcome-label' +import { QRCode } from '../qr-code' +import { copyToClipboard } from 'web/lib/util/copy' + +type challengeInfo = { + amount: number + expiresTime: number | null + message: string + outcome: 'YES' | 'NO' | number + acceptorAmount: number +} + +export function CreateChallengeModal(props: { + user: User | null | undefined + contract: BinaryContract + isOpen: boolean + setOpen: (open: boolean) => void +}) { + const { user, contract, isOpen, setOpen } = props + const [challengeSlug, setChallengeSlug] = useState('') + + return ( + <Modal open={isOpen} setOpen={setOpen} size={'sm'}> + <Col className="gap-4 rounded-md bg-white px-8 py-6"> + {/*// add a sign up to challenge button?*/} + {user && ( + <CreateChallengeForm + user={user} + contract={contract} + onCreate={async (newChallenge) => { + const challenge = await createChallenge({ + creator: user, + creatorAmount: newChallenge.amount, + expiresTime: newChallenge.expiresTime, + message: newChallenge.message, + acceptorAmount: newChallenge.acceptorAmount, + outcome: newChallenge.outcome, + contract: contract, + }) + challenge && setChallengeSlug(getChallengeUrl(challenge)) + }} + challengeSlug={challengeSlug} + /> + )} + </Col> + </Modal> + ) +} + +function CreateChallengeForm(props: { + user: User + contract: BinaryContract + onCreate: (m: challengeInfo) => Promise<void> + challengeSlug: string +}) { + const { user, onCreate, contract, challengeSlug } = props + const [isCreating, setIsCreating] = useState(false) + const [finishedCreating, setFinishedCreating] = useState(false) + const [error, setError] = useState<string>('') + const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false) + const defaultExpire = 'week' + + const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}` + + const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({ + expiresTime: dayjs().add(2, defaultExpire).valueOf(), + outcome: 'YES', + amount: 100, + acceptorAmount: 100, + message: defaultMessage, + }) + useEffect(() => { + setError('') + }, [challengeInfo]) + + return ( + <> + {!finishedCreating && ( + <form + onSubmit={(e) => { + e.preventDefault() + if (user.balance < challengeInfo.amount) { + setError('You do not have enough mana to create this challenge') + return + } + setIsCreating(true) + onCreate(challengeInfo).finally(() => setIsCreating(false)) + setFinishedCreating(true) + }} + > + <Title className="!mt-2" text="Challenge a friend to bet " /> + <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> + <div>You'll bet:</div> + <Row + className={ + 'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' + } + > + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.amount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + amount: parseInt(e.target.value), + acceptorAmount: editingAcceptorAmount + ? m.acceptorAmount + : parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + <span className={''}>on</span> + {challengeInfo.outcome === 'YES' ? <YesLabel /> : <NoLabel />} + </Row> + <Row className={'mt-3 max-w-xs justify-end'}> + <Button + color={'gradient'} + className={'opacity-80'} + onClick={() => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + outcome: m.outcome === 'YES' ? 'NO' : 'YES', + } + }) + } + > + <SwitchVerticalIcon className={'h-4 w-4'} /> + </Button> + </Row> + <Row className={'items-center'}>If they bet:</Row> + <Row className={'max-w-xs items-center justify-between gap-4 pr-3'}> + <div className={'w-32 sm:mr-1'}> + {editingAcceptorAmount ? ( + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.acceptorAmount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + acceptorAmount: parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + ) : ( + <span className="ml-1 font-bold"> + {formatMoney(challengeInfo.acceptorAmount)} + </span> + )} + </div> + <span>on</span> + {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} + </Row> + </div> + <Row + className={clsx( + 'mt-8', + !editingAcceptorAmount ? 'justify-between' : 'justify-end' + )} + > + {!editingAcceptorAmount && ( + <Button + color={'gray-white'} + onClick={() => setEditingAcceptorAmount(!editingAcceptorAmount)} + > + Edit + </Button> + )} + <Button + type="submit" + color={'indigo'} + className={clsx( + 'whitespace-nowrap drop-shadow-md', + isCreating ? 'disabled' : '' + )} + > + Continue + </Button> + </Row> + <Row className={'text-error'}>{error} </Row> + </form> + )} + {finishedCreating && ( + <> + <Title className="!my-0" text="Challenge Created!" /> + + <div>Share the challenge using the link.</div> + <button + onClick={() => { + copyToClipboard(challengeSlug) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + <LinkIcon className={'mr-2 h-5 w-5'} /> + Copy link + </button> + + <QRCode url={challengeSlug} className="self-center" /> + <Row className={'gap-1 text-gray-500'}> + See your other + <SiteLink className={'underline'} href={'/challenges'}> + challenges + </SiteLink> + </Row> + </> + )} + </> + ) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 7a7242a0..9d12496d 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -5,13 +5,13 @@ import { TrendingUpIcon, UserGroupIcon, } from '@heroicons/react/outline' + import { Row } from '../layout/row' import { formatMoney } from 'common/util/format' import { UserLink } from '../user-page' import { Contract, contractMetrics, - contractPath, updateContract, } from 'web/lib/firebase/contracts' import dayjs from 'dayjs' @@ -24,11 +24,9 @@ import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' import { UserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' -import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' import { exhibitExts } from 'common/util/parse' -import { ENV_CONFIG } from 'common/envs/constants' import { Button } from 'web/components/button' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' @@ -228,14 +226,6 @@ export function ContractDetails(props: { <div className="whitespace-nowrap">{volumeLabel}</div> </Row> - <ShareIconButton - copyPayload={`https://${ENV_CONFIG.domain}${contractPath(contract)}${ - user?.username && contract.creatorUsername !== user?.username - ? '?referrer=' + user?.username - : '' - }`} - toastClassName={'sm:-left-40 -left-24 min-w-[250%]'} - /> {!disabled && <ContractInfoDialog contract={contract} bets={bets} />} </Row> diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index a1f79479..168ada50 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -7,16 +7,12 @@ import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' -import { contractPath, contractPool } from 'web/lib/firebase/contracts' +import { contractPool } from 'web/lib/firebase/contracts' import { LiquidityPanel } from '../liquidity-panel' import { Col } from '../layout/col' import { Modal } from '../layout/modal' -import { Row } from '../layout/row' -import { ShareEmbedButton } from '../share-embed-button' import { Title } from '../title' -import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' -import { DuplicateContractButton } from '../copy-contract-button' export const contractDetailsButtonClassName = 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' @@ -61,20 +57,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { <Col className="gap-4 rounded bg-white p-6"> <Title className="!mt-0 !mb-0" text="Market info" /> - <div>Share</div> - - <Row className="justify-start gap-4"> - <TweetButton - className="self-start" - tweetText={getTweetText(contract)} - /> - <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> - <DuplicateContractButton contract={contract} /> - </Row> - <div /> - - <div>Stats</div> - <table className="table-compact table-zebra table w-full text-gray-500"> <tbody> <tr> @@ -150,14 +132,3 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { </> ) } - -const getTweetText = (contract: Contract) => { - const { question, resolution } = contract - - const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' - - const timeParam = `${Date.now()}`.substring(7) - const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` - - return `${question}\n\n${url}${tweetDescription}` -} diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 28eabb04..b95bb02b 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -1,12 +1,13 @@ -import { contractUrl, tradingAllowed } from 'web/lib/firebase/contracts' +import React from 'react' +import clsx from 'clsx' + +import { tradingAllowed } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { ContractProbGraph } from './contract-prob-graph' import { useUser } from 'web/hooks/use-user' import { Row } from '../layout/row' import { Linkify } from '../linkify' -import clsx from 'clsx' - import { BinaryResolutionOrChance, FreeResponseResolutionOrChance, @@ -20,12 +21,7 @@ import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' import { NumericGraph } from './numeric-graph' -import { CreateChallengeButton } from 'web/components/challenges/create-challenge-button' -import React from 'react' -import { copyToClipboard } from 'web/lib/util/copy' -import toast from 'react-hot-toast' -import { LinkIcon } from '@heroicons/react/outline' -import { CHALLENGES_ENABLED } from 'common/challenge' +import { ShareRow } from './share-row' export const ContractOverview = (props: { contract: Contract @@ -40,7 +36,6 @@ export const ContractOverview = (props: { const isBinary = outcomeType === 'BINARY' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const showChallenge = user && isBinary && !resolution && CHALLENGES_ENABLED return ( <Col className={clsx('mb-6', className)}> @@ -123,47 +118,12 @@ export const ContractOverview = (props: { <AnswersGraph contract={contract} bets={bets} /> )} {outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />} - {/* {(contract.description || isCreator) && <Spacer h={6} />} */} + <ShareRow user={user} contract={contract} /> <ContractDescription className="px-2" contract={contract} isCreator={isCreator} /> - {/*<Row className="mx-4 mt-4 hidden justify-around sm:block">*/} - {/* {showChallenge && (*/} - {/* <Col className="gap-3">*/} - {/* <div className="text-lg">⚔️ Challenge a friend ⚔️</div>*/} - {/* <CreateChallengeButton user={user} contract={contract} />*/} - {/* </Col>*/} - {/* )}*/} - {/* {isCreator && (*/} - {/* <Col className="gap-3">*/} - {/* <div className="text-lg">Share your market</div>*/} - {/* <ShareMarketButton contract={contract} />*/} - {/* </Col>*/} - {/* )}*/} - {/*</Row>*/} - <Row className="mx-4 mt-6 block justify-around"> - {showChallenge && ( - <Col className="gap-3"> - <CreateChallengeButton user={user} contract={contract} /> - </Col> - )} - {isCreator && ( - <Col className="gap-3"> - <button - onClick={() => { - copyToClipboard(contractUrl(contract)) - toast('Link copied to clipboard!') - }} - className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} - > - <LinkIcon className={'mr-2 h-5 w-5'} /> - Share market - </button> - </Col> - )} - </Row> </Col> ) } diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx new file mode 100644 index 00000000..017d3174 --- /dev/null +++ b/web/components/contract/share-modal.tsx @@ -0,0 +1,77 @@ +import { LinkIcon } from '@heroicons/react/outline' +import toast from 'react-hot-toast' + +import { Contract } from 'common/contract' +import { contractPath } from 'web/lib/firebase/contracts' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { ShareEmbedButton } from '../share-embed-button' +import { Title } from '../title' +import { TweetButton } from '../tweet-button' +import { DuplicateContractButton } from '../copy-contract-button' +import { Button } from '../button' +import { copyToClipboard } from 'web/lib/util/copy' +import { track } from 'web/lib/service/analytics' +import { ENV_CONFIG } from 'common/envs/constants' +import { User } from 'common/user' + +export function ShareModal(props: { + contract: Contract + user: User | undefined | null + isOpen: boolean + setOpen: (open: boolean) => void +}) { + const { contract, user, isOpen, setOpen } = props + + const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" /> + + const copyPayload = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ + user?.username && contract.creatorUsername !== user?.username + ? '?referrer=' + user?.username + : '' + }` + + return ( + <Modal open={isOpen} setOpen={setOpen}> + <Col className="gap-4 rounded bg-white p-4"> + <Title className="!mt-0 mb-2" text="Share this market" /> + + <Button + size="2xl" + color="gradient" + className={'mb-2 flex max-w-xs self-center'} + onClick={() => { + copyToClipboard(copyPayload) + track('copy share link') + toast.success('Link copied!', { + icon: linkIcon, + }) + }} + > + {linkIcon} Copy link + </Button> + + <Row className="justify-start gap-4 self-center"> + <TweetButton + className="self-start" + tweetText={getTweetText(contract)} + /> + <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> + <DuplicateContractButton contract={contract} /> + </Row> + </Col> + </Modal> + ) +} + +const getTweetText = (contract: Contract) => { + const { question, resolution } = contract + + const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' + + const timeParam = `${Date.now()}`.substring(7) + const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` + + return `${question}\n\n${url}${tweetDescription}` +} diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx new file mode 100644 index 00000000..fd872c5a --- /dev/null +++ b/web/components/contract/share-row.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx' +import { ShareIcon } from '@heroicons/react/outline' + +import { Row } from '../layout/row' +import { Contract } from 'web/lib/firebase/contracts' +import { useState } from 'react' +import { Button } from 'web/components/button' +import { CreateChallengeModal } from '../challenges/create-challenge-modal' +import { User } from 'common/user' +import { CHALLENGES_ENABLED } from 'common/challenge' +import { ShareModal } from './share-modal' + +export function ShareRow(props: { + contract: Contract + user: User | undefined | null +}) { + const { user, contract } = props + const { outcomeType, resolution } = contract + + const showChallenge = + user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED + + const [isOpen, setIsOpen] = useState(false) + const [isShareOpen, setShareOpen] = useState(false) + + return ( + <Row className="mt-2"> + <Button + size="lg" + color="gray-white" + className={'flex'} + onClick={() => { + setShareOpen(true) + }} + > + <ShareIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" /> + Share + <ShareModal + isOpen={isShareOpen} + setOpen={setShareOpen} + contract={contract} + user={user} + /> + </Button> + + {showChallenge && ( + <Button size="lg" color="gray-white" onClick={() => setIsOpen(true)}> + ⚔️ Challenge + <CreateChallengeModal + isOpen={isOpen} + setOpen={setIsOpen} + user={user} + contract={contract} + /> + </Button> + )} + </Row> + ) +} diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 7c68f0bd..e548e56f 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -113,6 +113,7 @@ function YourChallengesTable(props: { links: Challenge[] }) { function YourLinkSummaryRow(props: { challenge: Challenge }) { const { challenge } = props const { acceptances } = challenge + const [open, setOpen] = React.useState(false) const className = clsx( 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white' From 4d153755c1f2a53a15d4e9af1f0344be176985a6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 4 Aug 2022 22:33:56 -0700 Subject: [PATCH 418/519] delete challenge button --- .../challenges/create-challenge-button.tsx | 255 ------------------ 1 file changed, 255 deletions(-) delete mode 100644 web/components/challenges/create-challenge-button.tsx diff --git a/web/components/challenges/create-challenge-button.tsx b/web/components/challenges/create-challenge-button.tsx deleted file mode 100644 index 6eab9bc5..00000000 --- a/web/components/challenges/create-challenge-button.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import clsx from 'clsx' -import dayjs from 'dayjs' -import React, { useEffect, useState } from 'react' -import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline' - -import { Col } from '../layout/col' -import { Row } from '../layout/row' -import { Title } from '../title' -import { User } from 'common/user' -import { Modal } from 'web/components/layout/modal' -import { Button } from '../button' -import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' -import { BinaryContract } from 'common/contract' -import { SiteLink } from 'web/components/site-link' -import { formatMoney } from 'common/util/format' -import { NoLabel, YesLabel } from '../outcome-label' -import { QRCode } from '../qr-code' -import { copyToClipboard } from 'web/lib/util/copy' -import toast from 'react-hot-toast' - -type challengeInfo = { - amount: number - expiresTime: number | null - message: string - outcome: 'YES' | 'NO' | number - acceptorAmount: number -} -export function CreateChallengeButton(props: { - user: User | null | undefined - contract: BinaryContract -}) { - const { user, contract } = props - const [open, setOpen] = useState(false) - const [challengeSlug, setChallengeSlug] = useState('') - - return ( - <> - <Modal open={open} setOpen={(newOpen) => setOpen(newOpen)} size={'sm'}> - <Col className="gap-4 rounded-md bg-white px-8 py-6"> - {/*// add a sign up to challenge button?*/} - {user && ( - <CreateChallengeForm - user={user} - contract={contract} - onCreate={async (newChallenge) => { - const challenge = await createChallenge({ - creator: user, - creatorAmount: newChallenge.amount, - expiresTime: newChallenge.expiresTime, - message: newChallenge.message, - acceptorAmount: newChallenge.acceptorAmount, - outcome: newChallenge.outcome, - contract: contract, - }) - challenge && setChallengeSlug(getChallengeUrl(challenge)) - }} - challengeSlug={challengeSlug} - /> - )} - </Col> - </Modal> - - <button - onClick={() => setOpen(true)} - className="btn btn-outline mb-4 max-w-xs whitespace-nowrap normal-case" - > - Challenge a friend - </button> - </> - ) -} - -function CreateChallengeForm(props: { - user: User - contract: BinaryContract - onCreate: (m: challengeInfo) => Promise<void> - challengeSlug: string -}) { - const { user, onCreate, contract, challengeSlug } = props - const [isCreating, setIsCreating] = useState(false) - const [finishedCreating, setFinishedCreating] = useState(false) - const [error, setError] = useState<string>('') - const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false) - const defaultExpire = 'week' - - const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}` - - const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({ - expiresTime: dayjs().add(2, defaultExpire).valueOf(), - outcome: 'YES', - amount: 100, - acceptorAmount: 100, - message: defaultMessage, - }) - useEffect(() => { - setError('') - }, [challengeInfo]) - - return ( - <> - {!finishedCreating && ( - <form - onSubmit={(e) => { - e.preventDefault() - if (user.balance < challengeInfo.amount) { - setError('You do not have enough mana to create this challenge') - return - } - setIsCreating(true) - onCreate(challengeInfo).finally(() => setIsCreating(false)) - setFinishedCreating(true) - }} - > - <Title className="!mt-2" text="Challenge a friend to bet " /> - <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> - <div>You'll bet:</div> - <Row - className={ - 'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' - } - > - <Col> - <div className="relative"> - <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> - M$ - </span> - <input - className="input input-bordered w-32 pl-10" - type="number" - min={1} - value={challengeInfo.amount} - onChange={(e) => - setChallengeInfo((m: challengeInfo) => { - return { - ...m, - amount: parseInt(e.target.value), - acceptorAmount: editingAcceptorAmount - ? m.acceptorAmount - : parseInt(e.target.value), - } - }) - } - /> - </div> - </Col> - <span className={''}>on</span> - {challengeInfo.outcome === 'YES' ? <YesLabel /> : <NoLabel />} - </Row> - <Row className={'mt-3 max-w-xs justify-end'}> - <Button - color={'gradient'} - className={'opacity-80'} - onClick={() => - setChallengeInfo((m: challengeInfo) => { - return { - ...m, - outcome: m.outcome === 'YES' ? 'NO' : 'YES', - } - }) - } - > - <SwitchVerticalIcon className={'h-4 w-4'} /> - </Button> - </Row> - <Row className={'items-center'}>If they bet:</Row> - <Row className={'max-w-xs items-center justify-between gap-4 pr-3'}> - <div className={'w-32 sm:mr-1'}> - {editingAcceptorAmount ? ( - <Col> - <div className="relative"> - <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> - M$ - </span> - <input - className="input input-bordered w-32 pl-10" - type="number" - min={1} - value={challengeInfo.acceptorAmount} - onChange={(e) => - setChallengeInfo((m: challengeInfo) => { - return { - ...m, - acceptorAmount: parseInt(e.target.value), - } - }) - } - /> - </div> - </Col> - ) : ( - <span className="ml-1 font-bold"> - {formatMoney(challengeInfo.acceptorAmount)} - </span> - )} - </div> - <span>on</span> - {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} - </Row> - </div> - <Row - className={clsx( - 'mt-8', - !editingAcceptorAmount ? 'justify-between' : 'justify-end' - )} - > - {!editingAcceptorAmount && ( - <Button - color={'gray-white'} - onClick={() => setEditingAcceptorAmount(!editingAcceptorAmount)} - > - Edit - </Button> - )} - <Button - type="submit" - color={'indigo'} - className={clsx( - 'whitespace-nowrap drop-shadow-md', - isCreating ? 'disabled' : '' - )} - > - Continue - </Button> - </Row> - <Row className={'text-error'}>{error} </Row> - </form> - )} - {finishedCreating && ( - <> - <Title className="!my-0" text="Challenge Created!" /> - - <div>Share the challenge using the link.</div> - <button - onClick={() => { - copyToClipboard(challengeSlug) - toast('Link copied to clipboard!') - }} - className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} - > - <LinkIcon className={'mr-2 h-5 w-5'} /> - Copy link - </button> - - <QRCode url={challengeSlug} className="self-center" /> - <Row className={'gap-1 text-gray-500'}> - See your other - <SiteLink className={'underline'} href={'/challenges'}> - challenges - </SiteLink> - </Row> - </> - )} - </> - ) -} From 16f4fb94900fc14cababb79a486d44790a85c1e4 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 4 Aug 2022 22:47:59 -0700 Subject: [PATCH 419/519] disable clicking on group in embed --- web/components/contract/contract-details.tsx | 34 ++++++++++++-------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 9d12496d..936f5e24 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -145,6 +145,15 @@ export function ContractDetails(props: { const user = useUser() const [open, setOpen] = useState(false) + const groupInfo = ( + <Row> + <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> + <span className={'line-clamp-1'}> + {groupToDisplay ? groupToDisplay.name : 'No group'} + </span> + </Row> + ) + return ( <Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500"> <Row className="items-center gap-2"> @@ -166,19 +175,18 @@ export function ContractDetails(props: { {!disabled && <UserFollowButton userId={creatorId} small />} </Row> <Row> - <Button - size={'xs'} - className={'max-w-[200px]'} - color={'gray-white'} - onClick={() => setOpen(!open)} - > - <Row> - <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> - <span className={'line-clamp-1'}> - {groupToDisplay ? groupToDisplay.name : 'No group'} - </span> - </Row> - </Button> + {disabled ? ( + groupInfo + ) : ( + <Button + size={'xs'} + className={'max-w-[200px]'} + color={'gray-white'} + onClick={() => setOpen(!open)} + > + {groupInfo} + </Button> + )} </Row> <Modal open={open} setOpen={setOpen} size={'md'}> <Col From 5988dd1e484e9d390eb7de42de9192b6fcd6a8f5 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 4 Aug 2022 23:42:35 -0700 Subject: [PATCH 420/519] improved create challenge modal; 2xs button --- web/components/button.tsx | 3 +- .../challenges/create-challenge-modal.tsx | 143 +++++++++--------- 2 files changed, 75 insertions(+), 71 deletions(-) diff --git a/web/components/button.tsx b/web/components/button.tsx index 462670bd..57b2add9 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -5,7 +5,7 @@ export function Button(props: { className?: string onClick?: () => void children?: ReactNode - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' + size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' color?: | 'green' | 'red' @@ -29,6 +29,7 @@ export function Button(props: { } = props const sizeClasses = { + '2xs': 'px-2 py-1 text-xs', xs: 'px-2.5 py-1.5 text-sm', sm: 'px-3 py-2 text-sm', md: 'px-4 py-2 text-sm', diff --git a/web/components/challenges/create-challenge-modal.tsx b/web/components/challenges/create-challenge-modal.tsx index 3a0e857a..eca50f27 100644 --- a/web/components/challenges/create-challenge-modal.tsx +++ b/web/components/challenges/create-challenge-modal.tsx @@ -17,6 +17,8 @@ import { formatMoney } from 'common/util/format' import { NoLabel, YesLabel } from '../outcome-label' import { QRCode } from '../qr-code' import { copyToClipboard } from 'web/lib/util/copy' +import { AmountInput } from '../amount-input' +import { getProbability } from 'common/calculate' type challengeInfo = { amount: number @@ -36,7 +38,7 @@ export function CreateChallengeModal(props: { const [challengeSlug, setChallengeSlug] = useState('') return ( - <Modal open={isOpen} setOpen={setOpen} size={'sm'}> + <Modal open={isOpen} setOpen={setOpen}> <Col className="gap-4 rounded-md bg-white px-8 py-6"> {/*// add a sign up to challenge button?*/} {user && ( @@ -104,7 +106,13 @@ function CreateChallengeForm(props: { setFinishedCreating(true) }} > - <Title className="!mt-2" text="Challenge a friend to bet " /> + <Title className="!mt-2" text="Challenge bet " /> + + <div className="mb-8"> + Challenge a friend to bet on{' '} + <span className="underline">{contract.question}</span> + </div> + <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> <div>You'll bet:</div> <Row @@ -112,37 +120,29 @@ function CreateChallengeForm(props: { 'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' } > - <Col> - <div className="relative"> - <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> - M$ - </span> - <input - className="input input-bordered w-32 pl-10" - type="number" - min={1} - value={challengeInfo.amount} - onChange={(e) => - setChallengeInfo((m: challengeInfo) => { - return { - ...m, - amount: parseInt(e.target.value), - acceptorAmount: editingAcceptorAmount - ? m.acceptorAmount - : parseInt(e.target.value), - } - }) + <AmountInput + amount={challengeInfo.amount || undefined} + onChange={(newAmount) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + amount: newAmount ?? 0, + acceptorAmount: editingAcceptorAmount + ? m.acceptorAmount + : newAmount ?? 0, } - /> - </div> - </Col> + }) + } + error={undefined} + label={'M$'} + inputClassName="w-24" + /> <span className={''}>on</span> {challengeInfo.outcome === 'YES' ? <YesLabel /> : <NoLabel />} </Row> <Row className={'mt-3 max-w-xs justify-end'}> <Button - color={'gradient'} - className={'opacity-80'} + color={'gray-white'} onClick={() => setChallengeInfo((m: challengeInfo) => { return { @@ -152,67 +152,70 @@ function CreateChallengeForm(props: { }) } > - <SwitchVerticalIcon className={'h-4 w-4'} /> + <SwitchVerticalIcon className={'h-6 w-6'} /> </Button> </Row> <Row className={'items-center'}>If they bet:</Row> <Row className={'max-w-xs items-center justify-between gap-4 pr-3'}> <div className={'w-32 sm:mr-1'}> - {editingAcceptorAmount ? ( - <Col> - <div className="relative"> - <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> - M$ - </span> - <input - className="input input-bordered w-32 pl-10" - type="number" - min={1} - value={challengeInfo.acceptorAmount} - onChange={(e) => - setChallengeInfo((m: challengeInfo) => { - return { - ...m, - acceptorAmount: parseInt(e.target.value), - } - }) - } - /> - </div> - </Col> - ) : ( - <span className="ml-1 font-bold"> - {formatMoney(challengeInfo.acceptorAmount)} - </span> - )} + <AmountInput + amount={challengeInfo.acceptorAmount || undefined} + onChange={(newAmount) => { + setEditingAcceptorAmount(true) + + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + acceptorAmount: newAmount ?? 0, + } + }) + }} + error={undefined} + label={'M$'} + inputClassName="w-24" + /> </div> <span>on</span> {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} </Row> </div> - <Row - className={clsx( - 'mt-8', - !editingAcceptorAmount ? 'justify-between' : 'justify-end' - )} + <Button + size="2xs" + color="gray" + onClick={() => { + setEditingAcceptorAmount(true) + + const p = getProbability(contract) + const prob = challengeInfo.outcome === 'YES' ? p : 1 - p + const { amount } = challengeInfo + const acceptorAmount = Math.round(amount / prob - amount) + setChallengeInfo({ ...challengeInfo, acceptorAmount }) + }} > - {!editingAcceptorAmount && ( - <Button - color={'gray-white'} - onClick={() => setEditingAcceptorAmount(!editingAcceptorAmount)} - > - Edit - </Button> - )} + Use market odds + </Button> + + <div className="mt-8"> + If the challenge is accepted, whoever is right will earn{' '} + <span className="font-semibold"> + {formatMoney( + challengeInfo.acceptorAmount + challengeInfo.amount || 0 + )} + </span>{' '} + in total. + </div> + + <Row className="mt-8 items-center"> <Button type="submit" - color={'indigo'} + color={'gradient'} + size="xl" className={clsx( 'whitespace-nowrap drop-shadow-md', isCreating ? 'disabled' : '' )} > - Continue + Create challenge bet </Button> </Row> <Row className={'text-error'}>{error} </Row> From f3704633ee16346ead7d903c7b85bde965a34031 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 5 Aug 2022 00:03:38 -0700 Subject: [PATCH 421/519] liquidity panel styling --- web/components/liquidity-panel.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/components/liquidity-panel.tsx b/web/components/liquidity-panel.tsx index 7ecadeb7..94cf63b5 100644 --- a/web/components/liquidity-panel.tsx +++ b/web/components/liquidity-panel.tsx @@ -11,7 +11,6 @@ import { useUserLiquidity } from 'web/hooks/use-liquidity' import { Tabs } from './layout/tabs' import { NoLabel, YesLabel } from './outcome-label' import { Col } from './layout/col' -import { InfoTooltip } from './info-tooltip' import { track } from 'web/lib/service/analytics' export function LiquidityPanel(props: { contract: CPMMContract }) { @@ -103,8 +102,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) { return ( <> <div className="align-center mb-4 text-gray-500"> - Subsidize this market by adding M$ to the liquidity pool.{' '} - <InfoTooltip text="The greater the M$ subsidy, the greater the incentive for traders to participate, the more accurate the market will be." /> + Subsidize this market by adding M$ to the liquidity pool. </div> <Row> @@ -114,6 +112,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) { label="M$" error={error} disabled={isLoading} + inputClassName="w-28" /> <button className={clsx('btn btn-primary ml-2', isLoading && 'btn-disabled')} From d90901b4e325a314d73a59c21f15caec2577de40 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 05:03:47 -0600 Subject: [PATCH 422/519] Check creator balance again upon acceptance --- functions/src/accept-challenge.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions/src/accept-challenge.ts b/functions/src/accept-challenge.ts index fa98c8c6..eae6ab55 100644 --- a/functions/src/accept-challenge.ts +++ b/functions/src/accept-challenge.ts @@ -47,7 +47,7 @@ export const acceptchallenge = newEndpoint({}, async (req, auth) => { const creatorDoc = firestore.doc(`users/${challenge.creatorId}`) const creatorSnap = await trans.get(creatorDoc) - if (!creatorSnap.exists) throw new APIError(400, 'User not found.') + if (!creatorSnap.exists) throw new APIError(400, 'Creator not found.') const creator = creatorSnap.data() as User const { @@ -61,6 +61,9 @@ export const acceptchallenge = newEndpoint({}, async (req, auth) => { if (user.balance < acceptorAmount) throw new APIError(400, 'Insufficient balance.') + if (creator.balance < creatorAmount) + throw new APIError(400, 'Creator has insufficient balance.') + const contract = anyContract as CPMMBinaryContract const shares = (1 / creatorOutcomeProb) * creatorAmount const createdTime = Date.now() From 97e3de4e0fc4d7d07b68af98204981d500d6b325 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 06:56:10 -0600 Subject: [PATCH 423/519] Show numeric values in card preview --- og-image/api/_lib/parser.ts | 2 ++ og-image/api/_lib/template.ts | 9 ++++++++- og-image/api/_lib/types.ts | 1 + web/components/SEO.tsx | 8 ++++++++ web/components/contract/contract-card-preview.tsx | 8 ++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/og-image/api/_lib/parser.ts b/og-image/api/_lib/parser.ts index 1a0863bd..6d5c9b3d 100644 --- a/og-image/api/_lib/parser.ts +++ b/og-image/api/_lib/parser.ts @@ -16,6 +16,7 @@ export function parseRequest(req: IncomingMessage) { // Attributes for Manifold card: question, probability, + numericValue, metadata, creatorName, creatorUsername, @@ -71,6 +72,7 @@ export function parseRequest(req: IncomingMessage) { question: getString(question) || 'Will you create a prediction market on Manifold?', probability: getString(probability), + numericValue: getString(numericValue) || '', metadata: getString(metadata) || 'Jan 1  •  M$ 123 pool', creatorName: getString(creatorName) || 'Manifold Markets', creatorUsername: getString(creatorUsername) || 'ManifoldMarkets', diff --git a/og-image/api/_lib/template.ts b/og-image/api/_lib/template.ts index 1fe54554..e7b4fcaf 100644 --- a/og-image/api/_lib/template.ts +++ b/og-image/api/_lib/template.ts @@ -91,6 +91,7 @@ export function getHtml(parsedReq: ParsedRequest) { creatorName, creatorUsername, creatorAvatarUrl, + numericValue, } = parsedReq const MAX_QUESTION_CHARS = 100 const truncatedQuestion = @@ -147,8 +148,14 @@ export function getHtml(parsedReq: ParsedRequest) { <div class="text-indigo-700 text-6xl leading-tight"> ${truncatedQuestion} </div> - <div class="flex flex-col text-primary"> + <div class="flex flex-col text-primary text-center"> <div class="text-8xl">${probability}</div> + <span class='text-blue-400'> + <div class="text-8xl ">${ + numericValue !== '' && probability === '' ? numericValue : '' + }</div> + <div class="text-4xl">${numericValue !== '' ? 'expected' : ''}</div> + </span> <div class="text-4xl">${probability !== '' ? 'chance' : ''}</div> </div> </div> diff --git a/og-image/api/_lib/types.ts b/og-image/api/_lib/types.ts index 3ade016a..ef0a8135 100644 --- a/og-image/api/_lib/types.ts +++ b/og-image/api/_lib/types.ts @@ -14,6 +14,7 @@ export interface ParsedRequest { // Attributes for Manifold card: question: string probability: string + numericValue: string metadata: string creatorName: string creatorUsername: string diff --git a/web/components/SEO.tsx b/web/components/SEO.tsx index b1e0ca5f..08dee31e 100644 --- a/web/components/SEO.tsx +++ b/web/components/SEO.tsx @@ -9,6 +9,7 @@ export type OgCardProps = { creatorName: string creatorUsername: string creatorAvatarUrl?: string + numericValue?: string } function buildCardUrl(props: OgCardProps, challenge?: Challenge) { @@ -25,6 +26,12 @@ function buildCardUrl(props: OgCardProps, challenge?: Challenge) { props.probability === undefined ? '' : `&probability=${encodeURIComponent(props.probability ?? '')}` + + const numericValueParam = + props.numericValue === undefined + ? '' + : `&numericValue=${encodeURIComponent(props.numericValue ?? '')}` + const creatorAvatarUrlParam = props.creatorAvatarUrl === undefined ? '' @@ -41,6 +48,7 @@ function buildCardUrl(props: OgCardProps, challenge?: Challenge) { `https://manifold-og-image.vercel.app/m.png` + `?question=${encodeURIComponent(props.question)}` + probabilityParam + + numericValueParam + `&metadata=${encodeURIComponent(props.metadata)}` + `&creatorName=${encodeURIComponent(props.creatorName)}` + creatorAvatarUrlParam + diff --git a/web/components/contract/contract-card-preview.tsx b/web/components/contract/contract-card-preview.tsx index 06a7f7f6..354fe308 100644 --- a/web/components/contract/contract-card-preview.tsx +++ b/web/components/contract/contract-card-preview.tsx @@ -2,6 +2,8 @@ import { Contract } from 'common/contract' import { getBinaryProbPercent } from 'web/lib/firebase/contracts' import { richTextToString } from 'common/util/parse' import { contractTextDetails } from 'web/components/contract/contract-details' +import { getFormattedMappedValue } from 'common/pseudo-numeric' +import { getProbability } from 'common/calculate' export const getOpenGraphProps = (contract: Contract) => { const { @@ -16,6 +18,11 @@ export const getOpenGraphProps = (contract: Contract) => { const probPercent = outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined + const numericValue = + outcomeType === 'PSEUDO_NUMERIC' + ? getFormattedMappedValue(contract)(getProbability(contract)) + : undefined + const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) const description = resolution @@ -32,5 +39,6 @@ export const getOpenGraphProps = (contract: Contract) => { creatorUsername, creatorAvatarUrl, description, + numericValue, } } From 1c80bf1fafaba307f3845f4aedf72e1e5a18890d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 06:58:29 -0600 Subject: [PATCH 424/519] Chat icon => users icon --- web/components/groups/group-chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 91de63c6..70605556 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -24,7 +24,7 @@ import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' -import { ChatIcon, ChevronDownIcon } from '@heroicons/react/outline' +import { ChatIcon, ChevronDownIcon, UsersIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' export function GroupChat(props: { @@ -239,7 +239,7 @@ export function GroupChatInBubble(props: { }} > {!shouldShowChat ? ( - <ChatIcon className="h-10 w-10" aria-hidden="true" /> + <UsersIcon className="h-10 w-10" aria-hidden="true" /> ) : ( <ChevronDownIcon className={'h-10 w-10'} aria-hidden={'true'} /> )} From de6d5b388a00f8332dee15aa0fb6954f90ebc9a0 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 06:58:39 -0600 Subject: [PATCH 425/519] Lint --- web/components/groups/group-chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 70605556..47258b09 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -24,7 +24,7 @@ import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' -import { ChatIcon, ChevronDownIcon, UsersIcon } from '@heroicons/react/outline' +import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' export function GroupChat(props: { From f47b70dd3c4ba7462a444a3681bc2ab4cd717e1d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 07:08:41 -0600 Subject: [PATCH 426/519] Darken numeric preview text --- og-image/api/_lib/template.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/og-image/api/_lib/template.ts b/og-image/api/_lib/template.ts index e7b4fcaf..f59740c5 100644 --- a/og-image/api/_lib/template.ts +++ b/og-image/api/_lib/template.ts @@ -148,15 +148,15 @@ export function getHtml(parsedReq: ParsedRequest) { <div class="text-indigo-700 text-6xl leading-tight"> ${truncatedQuestion} </div> - <div class="flex flex-col text-primary text-center"> + <div class="flex flex-col text-primary"> <div class="text-8xl">${probability}</div> - <span class='text-blue-400'> + <div class="text-4xl">${probability !== '' ? 'chance' : ''}</div> + <span class='text-blue-500 text-center'> <div class="text-8xl ">${ numericValue !== '' && probability === '' ? numericValue : '' }</div> <div class="text-4xl">${numericValue !== '' ? 'expected' : ''}</div> </span> - <div class="text-4xl">${probability !== '' ? 'chance' : ''}</div> </div> </div> From 60ebadbbe53788641b9060b164f0d12f8390d56e Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Fri, 5 Aug 2022 09:58:02 -0600 Subject: [PATCH 427/519] Add not about donating winnings to charity --- .../challenges/[username]/[contractSlug]/[challengeSlug].tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx index 0df5b7d7..55e78616 100644 --- a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx +++ b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx @@ -92,6 +92,7 @@ export default function ChallengePage(props: { useSaveReferral(currentUser, { defaultReferrerUsername: challenge?.creatorUsername, + contractId: challenge?.contractId, }) if (!contract || !challenge) return <Custom404 /> @@ -171,7 +172,8 @@ function FAQ() { {toggleWhatIsMana && ( <Row className={'mx-4'}> Mana (M$) is the play-money used by our platform to keep track of your - bets. It's completely free for you and your friends to get started! + bets. It's completely free to get started, and you can donate your + winnings to charity! </Row> )} </Col> From ced404eb74993207ebcbef835f718db60c713c7c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 5 Aug 2022 12:01:16 -0700 Subject: [PATCH 428/519] Local search filters on groups, exclude contractIds --- web/pages/contract-search-firestore.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index ea42b38a..9039aa50 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -20,6 +20,8 @@ export default function ContractSearchFirestore(props: { additionalFilter?: { creatorId?: string tag?: string + excludeContractIds?: string[] + groupSlug?: string } }) { const contracts = useContracts() @@ -63,7 +65,7 @@ export default function ContractSearchFirestore(props: { } if (additionalFilter) { - const { creatorId, tag } = additionalFilter + const { creatorId, tag, groupSlug, excludeContractIds } = additionalFilter if (creatorId) { matches = matches.filter((c) => c.creatorId === creatorId) @@ -74,6 +76,14 @@ export default function ContractSearchFirestore(props: { c.lowercaseTags.includes(tag.toLowerCase()) ) } + + if (groupSlug) { + matches = matches.filter((c) => c.groupSlugs?.includes(groupSlug)) + } + + if (excludeContractIds) { + matches = matches.filter((c) => !excludeContractIds.includes(c.id)) + } } matches = matches.slice(0, MAX_CONTRACTS_RENDERED) From f11c9a334122993778ecc09e14c43705f95c5583 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 5 Aug 2022 13:38:12 -0700 Subject: [PATCH 429/519] bouncing challenge button (temporary gimmick) --- web/components/contract/share-row.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx index fd872c5a..fa86094f 100644 --- a/web/components/contract/share-row.tsx +++ b/web/components/contract/share-row.tsx @@ -44,7 +44,12 @@ export function ShareRow(props: { </Button> {showChallenge && ( - <Button size="lg" color="gray-white" onClick={() => setIsOpen(true)}> + <Button + size="lg" + color="gray-white" + onClick={() => setIsOpen(true)} + className="animate-bounce" + > ⚔️ Challenge <CreateChallengeModal isOpen={isOpen} From 5e896285934c3e49724cf07b69e8f5242e59805a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 5 Aug 2022 13:42:02 -0700 Subject: [PATCH 430/519] challenge bet tracking --- web/components/challenges/create-challenge-modal.tsx | 10 +++++++++- web/components/contract/share-row.tsx | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/web/components/challenges/create-challenge-modal.tsx b/web/components/challenges/create-challenge-modal.tsx index eca50f27..e93ec314 100644 --- a/web/components/challenges/create-challenge-modal.tsx +++ b/web/components/challenges/create-challenge-modal.tsx @@ -19,6 +19,7 @@ import { QRCode } from '../qr-code' import { copyToClipboard } from 'web/lib/util/copy' import { AmountInput } from '../amount-input' import { getProbability } from 'common/calculate' +import { track } from 'web/lib/service/analytics' type challengeInfo = { amount: number @@ -55,7 +56,14 @@ export function CreateChallengeModal(props: { outcome: newChallenge.outcome, contract: contract, }) - challenge && setChallengeSlug(getChallengeUrl(challenge)) + if (challenge) { + setChallengeSlug(getChallengeUrl(challenge)) + track('challenge created', { + creator: user.username, + amount: newChallenge.amount, + contractId: contract.id, + }) + } }} challengeSlug={challengeSlug} /> diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx index fa86094f..9c8c1573 100644 --- a/web/components/contract/share-row.tsx +++ b/web/components/contract/share-row.tsx @@ -9,6 +9,7 @@ import { CreateChallengeModal } from '../challenges/create-challenge-modal' import { User } from 'common/user' import { CHALLENGES_ENABLED } from 'common/challenge' import { ShareModal } from './share-modal' +import { withTracking } from 'web/lib/service/analytics' export function ShareRow(props: { contract: Contract @@ -47,7 +48,10 @@ export function ShareRow(props: { <Button size="lg" color="gray-white" - onClick={() => setIsOpen(true)} + onClick={withTracking( + () => setIsOpen(true), + 'click challenge button' + )} className="animate-bounce" > ⚔️ Challenge From 67139b99f904a91f7741511754b22c86882efe02 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 15:33:34 -0700 Subject: [PATCH 431/519] Add workflow to automate prettier running on main (#720) --- .github/workflows/format.yml | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 00000000..feee8758 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,38 @@ +name: Reformat main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + +env: + FORCE_COLOR: 3 + NEXT_TELEMETRY_DISABLED: 1 + +jobs: + prettify: + name: Auto-prettify + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Restore cached node_modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} + - name: Install missing dependencies + run: yarn install --prefer-offline --frozen-lockfile + - name: Run Prettier on web client + working-directory: web + run: yarn format + - name: Commit any Prettier changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Auto-prettification + branch: ${{ github.head_ref }} From d9ddabcfd4b8ce0619ec00d1af1f5da2807f40e6 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 15:35:57 -0700 Subject: [PATCH 432/519] Commit some un-pretty code --- web/test.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/test.js diff --git a/web/test.js b/web/test.js new file mode 100644 index 00000000..ef4a8a0f --- /dev/null +++ b/web/test.js @@ -0,0 +1 @@ + // this comment isn't very pretty From db3b0c2cf5b5691598df2ba2436f986288286ce4 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 15:38:22 -0700 Subject: [PATCH 433/519] Give repo write permission to formatting workflow --- .github/workflows/format.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index feee8758..8be333a3 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,6 +12,9 @@ env: FORCE_COLOR: 3 NEXT_TELEMETRY_DISABLED: 1 +permissions: + contents: write + jobs: prettify: name: Auto-prettify From f05db0ef0f3c03a43ccd7ec32001a88e7b92093e Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 15:56:10 -0700 Subject: [PATCH 434/519] Give formatting workflow even more permissions... --- .github/workflows/format.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 8be333a3..815c7732 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,8 +12,7 @@ env: FORCE_COLOR: 3 NEXT_TELEMETRY_DISABLED: 1 -permissions: - contents: write +permissions: write-all jobs: prettify: From 7e0634aee07cdc2b49ddf04ec3b9c75f9c9b5bfb Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 16:02:46 -0700 Subject: [PATCH 435/519] Try using a personal access token for formatter job --- .github/workflows/format.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 815c7732..fb850606 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,8 +12,6 @@ env: FORCE_COLOR: 3 NEXT_TELEMETRY_DISABLED: 1 -permissions: write-all - jobs: prettify: name: Auto-prettify @@ -23,6 +21,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + token: ${{ secrets.FORMATTER_ACCESS_TOKEN }} - name: Restore cached node_modules uses: actions/cache@v2 with: From bba9f9a5551c4a0e001fe9954fd213c3a0c220ec Mon Sep 17 00:00:00 2001 From: mqp <mqp@users.noreply.github.com> Date: Fri, 5 Aug 2022 23:03:25 +0000 Subject: [PATCH 436/519] Auto-prettification --- web/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/test.js b/web/test.js index ef4a8a0f..219046be 100644 --- a/web/test.js +++ b/web/test.js @@ -1 +1 @@ - // this comment isn't very pretty +// this comment isn't very pretty From bf3ba8ac3f27a6ed40837e0318399437ed678579 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 16:07:02 -0700 Subject: [PATCH 437/519] Remove test file --- web/test.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 web/test.js diff --git a/web/test.js b/web/test.js deleted file mode 100644 index 219046be..00000000 --- a/web/test.js +++ /dev/null @@ -1 +0,0 @@ -// this comment isn't very pretty From 48ac21ffad65882f8abe8e537cb5d98fa16ec509 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 16:08:30 -0700 Subject: [PATCH 438/519] Add comment explaining fishy token --- .github/workflows/format.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index fb850606..2aa95e44 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,6 +12,9 @@ env: FORCE_COLOR: 3 NEXT_TELEMETRY_DISABLED: 1 +# mqp - i generated a personal token to use for these writes -- it's unclear +# why, but the default token didn't work, even when i gave it max permissions + jobs: prettify: name: Auto-prettify From b3b06896bec8e3f324516dcf18c23b124aa78e96 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 5 Aug 2022 17:44:55 -0700 Subject: [PATCH 439/519] Add loading indicator when algolia search is initially loading --- web/components/contract-search.tsx | 2 +- web/components/contract/contracts-list.tsx | 7 ++++++- web/pages/contract-search-firestore.tsx | 17 ++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index c1e63175..607a4668 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -343,7 +343,7 @@ export function ContractSearch(props: { <>You're not following anyone, nor in any of your own groups yet.</> ) : ( <ContractsGrid - contracts={contracts} + contracts={hitsByPage[0] === undefined ? undefined : contracts} loadMore={loadMore} hasMore={true} showTime={showTime} diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-list.tsx index c733bd76..31a564d3 100644 --- a/web/components/contract/contracts-list.tsx +++ b/web/components/contract/contracts-list.tsx @@ -8,6 +8,7 @@ import { ContractSearch } from '../contract-search' import { useIsVisible } from 'web/hooks/use-is-visible' import { useEffect, useState } from 'react' import clsx from 'clsx' +import { LoadingIndicator } from '../loading-indicator' export type ContractHighlightOptions = { contractIds?: string[] @@ -15,7 +16,7 @@ export type ContractHighlightOptions = { } export function ContractsGrid(props: { - contracts: Contract[] + contracts: Contract[] | undefined loadMore: () => void hasMore: boolean showTime?: ShowTime @@ -49,6 +50,10 @@ export function ContractsGrid(props: { } }, [isBottomVisible, hasMore, loadMore]) + if (contracts === undefined) { + return <LoadingIndicator /> + } + if (contracts.length === 0) { return ( <p className="mx-2 text-gray-500"> diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 9039aa50..7bb42a05 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -3,7 +3,6 @@ import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { useState } from 'react' import { ContractsGrid } from 'web/components/contract/contracts-list' -import { LoadingIndicator } from 'web/components/loading-indicator' import { useContracts } from 'web/hooks/use-contracts' import { Sort, @@ -118,16 +117,12 @@ export default function ContractSearchFirestore(props: { <option value="close-date">Closing soon</option> </select> </div> - {contracts === undefined ? ( - <LoadingIndicator /> - ) : ( - <ContractsGrid - contracts={matches} - loadMore={() => {}} - hasMore={false} - showTime={showTime} - /> - )} + <ContractsGrid + contracts={matches} + loadMore={() => {}} + hasMore={false} + showTime={showTime} + /> </div> ) } From e0196f7107435978ff2a37341f725242bf6ffb91 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 5 Aug 2022 17:46:32 -0700 Subject: [PATCH 440/519] Rename file contracts-list to contracts-group --- web/components/contract-search.tsx | 2 +- .../contract/{contracts-list.tsx => contracts-grid.tsx} | 0 web/components/landing-page-panel.tsx | 2 +- web/components/user-page.tsx | 2 +- web/pages/contract-search-firestore.tsx | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename web/components/contract/{contracts-list.tsx => contracts-grid.tsx} (100%) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 607a4668..265b25c6 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -6,7 +6,7 @@ import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, ContractsGrid, -} from './contract/contracts-list' +} from './contract/contracts-grid' import { Row } from './layout/row' import { useEffect, useMemo, useState } from 'react' import { Spacer } from './layout/spacer' diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-grid.tsx similarity index 100% rename from web/components/contract/contracts-list.tsx rename to web/components/contract/contracts-grid.tsx diff --git a/web/components/landing-page-panel.tsx b/web/components/landing-page-panel.tsx index bcfdaf1e..4b436442 100644 --- a/web/components/landing-page-panel.tsx +++ b/web/components/landing-page-panel.tsx @@ -4,7 +4,7 @@ import { Contract } from 'common/contract' import { Spacer } from './layout/spacer' import { firebaseLogin } from 'web/lib/firebase/users' -import { ContractsGrid } from './contract/contracts-list' +import { ContractsGrid } from './contract/contracts-grid' import { Col } from './layout/col' import { Row } from './layout/row' import { withTracking } from 'web/lib/service/analytics' diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index d628e92d..fb349149 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -12,7 +12,7 @@ import { unfollow, User, } from 'web/lib/firebase/users' -import { CreatorContractsList } from './contract/contracts-list' +import { CreatorContractsList } from './contract/contracts-grid' import { SEO } from './SEO' import { Page } from './page' import { SiteLink } from './site-link' diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 7bb42a05..9a09b101 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -2,7 +2,7 @@ import { Answer } from 'common/answer' import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { useState } from 'react' -import { ContractsGrid } from 'web/components/contract/contracts-list' +import { ContractsGrid } from 'web/components/contract/contracts-grid' import { useContracts } from 'web/hooks/use-contracts' import { Sort, From d43b9e1836be524a956bf5dc19486f75d34be855 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 5 Aug 2022 20:49:29 -0700 Subject: [PATCH 441/519] Vercel auth phase 2 (#714) * Add cloud function to get custom token from API auth * Use custom token to authenticate Firebase SDK on server * Make sure getcustomtoken cloud function is fast * Make server auth code maximally robust * Refactor cookie code, make set and delete more flexible --- functions/src/api.ts | 24 +++-- functions/src/get-custom-token.ts | 33 +++++++ functions/src/index.ts | 3 + functions/src/serve.ts | 2 + web/components/auth-context.tsx | 9 +- web/lib/firebase/auth.ts | 98 +++++++++++-------- web/lib/firebase/server-auth.ts | 153 ++++++++++++++++++++++++------ web/pages/notifications.tsx | 13 +-- 8 files changed, 245 insertions(+), 90 deletions(-) create mode 100644 functions/src/get-custom-token.ts diff --git a/functions/src/api.ts b/functions/src/api.ts index fdda0ad5..e9a488c2 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -78,6 +78,19 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => { } } +export const writeResponseError = (e: unknown, res: Response) => { + if (e instanceof APIError) { + const output: { [k: string]: unknown } = { message: e.message } + if (e.details != null) { + output.details = e.details + } + res.status(e.code).json(output) + } else { + error(e) + res.status(500).json({ message: 'An unknown error occurred.' }) + } +} + export const zTimestamp = () => { return z.preprocess((arg) => { return typeof arg == 'number' ? new Date(arg) : undefined @@ -131,16 +144,7 @@ export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { const authedUser = await lookupUser(await parseCredentials(req)) res.status(200).json(await fn(req, authedUser)) } catch (e) { - if (e instanceof APIError) { - const output: { [k: string]: unknown } = { message: e.message } - if (e.details != null) { - output.details = e.details - } - res.status(e.code).json(output) - } else { - error(e) - res.status(500).json({ message: 'An unknown error occurred.' }) - } + writeResponseError(e, res) } }, } as EndpointDefinition diff --git a/functions/src/get-custom-token.ts b/functions/src/get-custom-token.ts new file mode 100644 index 00000000..4aaaac11 --- /dev/null +++ b/functions/src/get-custom-token.ts @@ -0,0 +1,33 @@ +import * as admin from 'firebase-admin' +import { + APIError, + EndpointDefinition, + lookupUser, + parseCredentials, + writeResponseError, +} from './api' + +const opts = { + method: 'GET', + minInstances: 1, + concurrency: 100, + memory: '2GiB', + cpu: 1, +} as const + +export const getcustomtoken: EndpointDefinition = { + opts, + handler: async (req, res) => { + try { + const credentials = await parseCredentials(req) + if (credentials.kind != 'jwt') { + throw new APIError(403, 'API keys cannot mint custom tokens.') + } + const user = await lookupUser(credentials) + const token = await admin.auth().createCustomToken(user.uid) + res.status(200).json({ token: token }) + } catch (e) { + writeResponseError(e, res) + } + }, +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 125cdea4..07b37648 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -65,6 +65,7 @@ import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' import { acceptchallenge } from './accept-challenge' +import { getcustomtoken } from './get-custom-token' const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { return onRequest(opts, handler as any) @@ -89,6 +90,7 @@ const stripeWebhookFunction = toCloudFunction(stripewebhook) const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) const getCurrentUserFunction = toCloudFunction(getcurrentuser) const acceptChallenge = toCloudFunction(acceptchallenge) +const getCustomTokenFunction = toCloudFunction(getcustomtoken) export { healthFunction as health, @@ -111,4 +113,5 @@ export { createCheckoutSessionFunction as createcheckoutsession, getCurrentUserFunction as getcurrentuser, acceptChallenge as acceptchallenge, + getCustomTokenFunction as getcustomtoken, } diff --git a/functions/src/serve.ts b/functions/src/serve.ts index 0064b69f..bf96db20 100644 --- a/functions/src/serve.ts +++ b/functions/src/serve.ts @@ -26,6 +26,7 @@ import { resolvemarket } from './resolve-market' import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' +import { getcustomtoken } from './get-custom-token' type Middleware = (req: Request, res: Response, next: NextFunction) => void const app = express() @@ -64,6 +65,7 @@ addJsonEndpointRoute('/resolvemarket', resolvemarket) addJsonEndpointRoute('/unsubscribe', unsubscribe) addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession) addJsonEndpointRoute('/getcurrentuser', getcurrentuser) +addEndpointRoute('/getcustomtoken', getcustomtoken) addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) app.listen(PORT) diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 653368b6..24adde25 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -7,7 +7,7 @@ import { getUser, setCachedReferralInfoForUser, } from 'web/lib/firebase/users' -import { deleteAuthCookies, setAuthCookies } from 'web/lib/firebase/auth' +import { deleteTokenCookies, setTokenCookies } from 'web/lib/firebase/auth' import { createUser } from 'web/lib/firebase/api' import { randomString } from 'common/util/random' import { identifyUser, setUserProperty } from 'web/lib/service/analytics' @@ -41,7 +41,10 @@ export function AuthProvider({ children }: any) { useEffect(() => { return onIdTokenChanged(auth, async (fbUser) => { if (fbUser) { - setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken) + setTokenCookies({ + id: await fbUser.getIdToken(), + refresh: fbUser.refreshToken, + }) let user = await getUser(fbUser.uid) if (!user) { const deviceToken = ensureDeviceToken() @@ -54,7 +57,7 @@ export function AuthProvider({ children }: any) { setCachedReferralInfoForUser(user) } else { // User logged out; reset to null - deleteAuthCookies() + deleteTokenCookies() setAuthUser(null) localStorage.removeItem(CACHED_USER_KEY) } diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index b6daea6e..b363189c 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -2,53 +2,73 @@ import { PROJECT_ID } from 'common/envs/constants' import { setCookie, getCookies } from '../util/cookie' import { IncomingMessage, ServerResponse } from 'http' -const TOKEN_KINDS = ['refresh', 'id'] as const -type TokenKind = typeof TOKEN_KINDS[number] +const ONE_HOUR_SECS = 60 * 60 +const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10 +const TOKEN_KINDS = ['refresh', 'id', 'custom'] as const +const TOKEN_AGES = { + id: ONE_HOUR_SECS, + refresh: ONE_HOUR_SECS, + custom: TEN_YEARS_SECS, +} as const +export type TokenKind = typeof TOKEN_KINDS[number] const getAuthCookieName = (kind: TokenKind) => { const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replace(/-/g, '_') return `FIREBASE_TOKEN_${suffix}` } -const ID_COOKIE_NAME = getAuthCookieName('id') -const REFRESH_COOKIE_NAME = getAuthCookieName('refresh') +const COOKIE_NAMES = Object.fromEntries( + TOKEN_KINDS.map((k) => [k, getAuthCookieName(k)]) +) as Record<TokenKind, string> -export const getAuthCookies = (request?: IncomingMessage) => { - const data = request != null ? request.headers.cookie ?? '' : document.cookie - const cookies = getCookies(data) - return { - idToken: cookies[ID_COOKIE_NAME] as string | undefined, - refreshToken: cookies[REFRESH_COOKIE_NAME] as string | undefined, - } -} - -export const setAuthCookies = ( - idToken?: string, - refreshToken?: string, - response?: ServerResponse -) => { - // these tokens last an hour - const idMaxAge = idToken != null ? 60 * 60 : 0 - const idCookie = setCookie(ID_COOKIE_NAME, idToken ?? '', [ - ['path', '/'], - ['max-age', idMaxAge.toString()], - ['samesite', 'lax'], - ['secure'], - ]) - // these tokens don't expire - const refreshMaxAge = refreshToken != null ? 60 * 60 * 24 * 365 * 10 : 0 - const refreshCookie = setCookie(REFRESH_COOKIE_NAME, refreshToken ?? '', [ - ['path', '/'], - ['max-age', refreshMaxAge.toString()], - ['samesite', 'lax'], - ['secure'], - ]) - if (response != null) { - response.setHeader('Set-Cookie', [idCookie, refreshCookie]) +const getCookieDataIsomorphic = (req?: IncomingMessage) => { + if (req != null) { + return req.headers.cookie ?? '' + } else if (document != null) { + return document.cookie } else { - document.cookie = idCookie - document.cookie = refreshCookie + throw new Error( + 'Neither request nor document is available; no way to get cookies.' + ) } } -export const deleteAuthCookies = () => setAuthCookies() +const setCookieDataIsomorphic = (cookies: string[], res?: ServerResponse) => { + if (res != null) { + res.setHeader('Set-Cookie', cookies) + } else if (document != null) { + for (const ck of cookies) { + document.cookie = ck + } + } else { + throw new Error( + 'Neither response nor document is available; no way to set cookies.' + ) + } +} + +export const getTokensFromCookies = (req?: IncomingMessage) => { + const cookies = getCookies(getCookieDataIsomorphic(req)) + return Object.fromEntries( + TOKEN_KINDS.map((k) => [k, cookies[COOKIE_NAMES[k]]]) + ) as Partial<Record<TokenKind, string>> +} + +export const setTokenCookies = ( + cookies: Partial<Record<TokenKind, string | undefined>>, + res?: ServerResponse +) => { + const data = TOKEN_KINDS.filter((k) => k in cookies).map((k) => { + const maxAge = cookies[k] ? TOKEN_AGES[k as TokenKind] : 0 + return setCookie(COOKIE_NAMES[k], cookies[k] ?? '', [ + ['path', '/'], + ['max-age', maxAge.toString()], + ['samesite', 'lax'], + ['secure'], + ]) + }) + setCookieDataIsomorphic(data, res) +} + +export const deleteTokenCookies = (res?: ServerResponse) => + setTokenCookies({ id: undefined, refresh: undefined, custom: undefined }, res) diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts index 47eadb45..b0d225f1 100644 --- a/web/lib/firebase/server-auth.ts +++ b/web/lib/firebase/server-auth.ts @@ -1,9 +1,25 @@ -import * as admin from 'firebase-admin' import fetch from 'node-fetch' import { IncomingMessage, ServerResponse } from 'http' import { FIREBASE_CONFIG, PROJECT_ID } from 'common/envs/constants' -import { getAuthCookies, setAuthCookies } from './auth' -import { GetServerSideProps, GetServerSidePropsContext } from 'next' +import { getFunctionUrl } from 'common/api' +import { UserCredential } from 'firebase/auth' +import { + getTokensFromCookies, + setTokenCookies, + deleteTokenCookies, +} from './auth' +import { + GetServerSideProps, + GetServerSidePropsContext, + GetServerSidePropsResult, +} from 'next' + +// server firebase SDK +import * as admin from 'firebase-admin' + +// client firebase SDK +import { app as clientApp } from './init' +import { getAuth, signInWithCustomToken } from 'firebase/auth' const ensureApp = async () => { // Note: firebase-admin can only be imported from a server context, @@ -33,7 +49,21 @@ const requestFirebaseIdToken = async (refreshToken: string) => { if (!result.ok) { throw new Error(`Could not refresh ID token: ${await result.text()}`) } - return (await result.json()) as any + return (await result.json()) as { id_token: string; refresh_token: string } +} + +const requestManifoldCustomToken = async (idToken: string) => { + const functionUrl = getFunctionUrl('getcustomtoken') + const result = await fetch(functionUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${idToken}`, + }, + }) + if (!result.ok) { + throw new Error(`Could not get custom token: ${await result.text()}`) + } + return (await result.json()) as { token: string } } type RequestContext = { @@ -41,39 +71,103 @@ type RequestContext = { res: ServerResponse } -export const getServerAuthenticatedUid = async (ctx: RequestContext) => { - const app = await ensureApp() - const auth = app.auth() - const { idToken, refreshToken } = getAuthCookies(ctx.req) +const authAndRefreshTokens = async (ctx: RequestContext) => { + const adminAuth = (await ensureApp()).auth() + const clientAuth = getAuth(clientApp) + let { id, refresh, custom } = getTokensFromCookies(ctx.req) - // If we have a valid ID token, verify the user immediately with no network trips. - // If the ID token doesn't verify, we'll have to refresh it to see who they are. - // If they don't have any tokens, then we have no idea who they are. - if (idToken != null) { + // step 0: if you have no refresh token you are logged out + if (refresh == null) { + return undefined + } + + // step 1: given a valid refresh token, ensure a valid ID token + if (id != null) { + // if they have an ID token, throw it out if it's invalid/expired try { - return (await auth.verifyIdToken(idToken))?.uid + await adminAuth.verifyIdToken(id) } catch { - // plausibly expired; try the refresh token, if it's present + id = undefined } } - if (refreshToken != null) { + if (id == null) { + // ask for a new one from google using the refresh token try { - const resp = await requestFirebaseIdToken(refreshToken) - setAuthCookies(resp.id_token, resp.refresh_token, ctx.res) - return (await auth.verifyIdToken(resp.id_token))?.uid + const resp = await requestFirebaseIdToken(refresh) + id = resp.id_token + refresh = resp.refresh_token } catch (e) { - // this is a big unexpected problem -- either their cookies are corrupt - // or the refresh token API is down. functionally, they are not logged in + // big unexpected problem -- functionally, they are not logged in console.error(e) + return undefined + } + } + + // step 2: given a valid ID token, ensure a valid custom token, and sign in + // to the client SDK with the custom token + if (custom != null) { + // sign in with this token, or throw it out if it's invalid/expired + try { + return { + creds: await signInWithCustomToken(clientAuth, custom), + id, + refresh, + custom, + } + } catch { + custom = undefined + } + } + if (custom == null) { + // ask for a new one from our cloud functions using the ID token, then sign in + try { + const resp = await requestManifoldCustomToken(id) + custom = resp.token + return { + creds: await signInWithCustomToken(clientAuth, custom), + id, + refresh, + custom, + } + } catch (e) { + // big unexpected problem -- functionally, they are not logged in + console.error(e) + return undefined } } - return undefined } -export const redirectIfLoggedIn = (dest: string, fn?: GetServerSideProps) => { +export const authenticateOnServer = async (ctx: RequestContext) => { + const tokens = await authAndRefreshTokens(ctx) + const creds = tokens?.creds + try { + if (tokens == null) { + deleteTokenCookies(ctx.res) + } else { + setTokenCookies(tokens, ctx.res) + } + } catch (e) { + // definitely not supposed to happen, but let's be maximally robust + console.error(e) + } + return creds +} + +// note that we might want to define these types more generically if we want better +// type safety on next.js stuff... see the definition of GetServerSideProps + +type GetServerSidePropsAuthed<P> = ( + context: GetServerSidePropsContext, + creds: UserCredential +) => Promise<GetServerSidePropsResult<P>> + +export const redirectIfLoggedIn = <P>( + dest: string, + fn?: GetServerSideProps<P> +) => { return async (ctx: GetServerSidePropsContext) => { - const uid = await getServerAuthenticatedUid(ctx) - if (uid == null) { + const creds = await authenticateOnServer(ctx) + if (creds == null) { return fn != null ? await fn(ctx) : { props: {} } } else { return { redirect: { destination: dest, permanent: false } } @@ -81,13 +175,16 @@ export const redirectIfLoggedIn = (dest: string, fn?: GetServerSideProps) => { } } -export const redirectIfLoggedOut = (dest: string, fn?: GetServerSideProps) => { +export const redirectIfLoggedOut = <P>( + dest: string, + fn?: GetServerSidePropsAuthed<P> +) => { return async (ctx: GetServerSidePropsContext) => { - const uid = await getServerAuthenticatedUid(ctx) - if (uid == null) { + const creds = await authenticateOnServer(ctx) + if (creds == null) { return { redirect: { destination: dest, permanent: false } } } else { - return fn != null ? await fn(ctx) : { props: {} } + return fn != null ? await fn(ctx, creds) : { props: {} } } } } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 625c7c17..89ffb5d9 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -40,10 +40,7 @@ import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' import { safeLocalStorage } from 'web/lib/util/local' -import { - getServerAuthenticatedUid, - redirectIfLoggedOut, -} from 'web/lib/firebase/server-auth' +import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { SiteLink } from 'web/components/site-link' import { NotificationSettings } from 'web/components/NotificationSettings' @@ -51,12 +48,8 @@ export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' const HIGHLIGHT_CLASS = 'bg-indigo-50' -export const getServerSideProps = redirectIfLoggedOut('/', async (ctx) => { - const uid = await getServerAuthenticatedUid(ctx) - if (!uid) { - return { props: { user: null } } - } - const user = await getUser(uid) +export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { + const user = await getUser(creds.user.uid) return { props: { user } } }) From 5892ccee977decd3644c66cfda22db02defa21ca Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Sat, 6 Aug 2022 13:39:52 -0700 Subject: [PATCH 442/519] Rich text in comments, fixed (#719) * Revert "Revert "Switch comments/chat to rich text editor (#703)"" This reverts commit 33906adfe489cbc3489bc014c4a3253d96660ec0. * Fix typing after being weird on Android Issue: character from mention gets deleted. Most weird on Swiftkey: mention gets duplicated instead of deleting. See https://github.com/ProseMirror/prosemirror/issues/565 https://bugs.chromium.org/p/chromium/issues/detail?id=612446 The keyboard still closes unexpectedly when you delete :( * On reply, put space instead of \n after mention * Remove image upload from placeholder text - Redundant with image upload button - Too long on mobile * fix dependency --- common/comment.ts | 6 +- functions/src/create-notification.ts | 19 +- functions/src/emails.ts | 4 +- .../src/on-create-comment-on-contract.ts | 11 +- web/components/comments-list.tsx | 7 +- .../contract/contract-leaderboard.tsx | 1 - web/components/editor.tsx | 39 +-- web/components/editor/mention.tsx | 5 +- .../feed/feed-answer-comment-group.tsx | 28 +- web/components/feed/feed-comments.tsx | 246 +++++++----------- web/components/groups/group-chat.tsx | 104 ++++---- web/lib/firebase/comments.ts | 9 +- web/pages/[username]/[contractSlug].tsx | 1 - 13 files changed, 204 insertions(+), 276 deletions(-) diff --git a/common/comment.ts b/common/comment.ts index 0d0c4daf..a217b292 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,3 +1,5 @@ +import type { JSONContent } from '@tiptap/core' + // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. export type Comment = { @@ -9,7 +11,9 @@ export type Comment = { replyToCommentId?: string userId: string - text: string + /** @deprecated - content now stored as JSON in content*/ + text?: string + content: JSONContent createdTime: number // Denormalized, for rendering comments diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index e16920f7..51b884ad 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -7,7 +7,7 @@ import { } from '../../common/notification' import { User } from '../../common/user' import { Contract } from '../../common/contract' -import { getUserByUsername, getValues } from './utils' +import { getValues } from './utils' import { Comment } from '../../common/comment' import { uniq } from 'lodash' import { Bet, LimitBet } from '../../common/bet' @@ -17,6 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' import { Group, GROUP_CHAT_SLUG } from '../../common/group' import { Challenge } from '../../common/challenge' +import { richTextToString } from '../../common/util/parse' const firestore = admin.firestore() type user_to_reason_texts = { @@ -155,17 +156,6 @@ export const createNotification = async ( } } - /** @deprecated parse from rich text instead */ - const parseMentions = async (source: string) => { - const mentions = source.match(/@\w+/g) - if (!mentions) return [] - return Promise.all( - mentions.map( - async (username) => (await getUserByUsername(username.slice(1)))?.id - ) - ) - } - const notifyTaggedUsers = ( userToReasonTexts: user_to_reason_texts, userIds: (string | undefined)[] @@ -301,8 +291,7 @@ export const createNotification = async ( if (sourceType === 'comment') { if (recipients?.[0] && relatedSourceType) notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType) - if (sourceText) - notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText)) + if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? []) } await notifyContractCreator(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) @@ -427,7 +416,7 @@ export const createGroupCommentNotification = async ( sourceUserName: fromUser.name, sourceUserUsername: fromUser.username, sourceUserAvatarUrl: fromUser.avatarUrl, - sourceText: comment.text, + sourceText: richTextToString(comment.content), sourceSlug, sourceTitle: `${group.name}`, isSeenOnHref: sourceSlug, diff --git a/functions/src/emails.ts b/functions/src/emails.ts index b7469e9f..a097393e 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -17,6 +17,7 @@ import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail } from './send-email' import { getPrivateUser, getUser } from './utils' import { getFunctionUrl } from '../../common/api' +import { richTextToString } from '../../common/util/parse' const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe') @@ -291,7 +292,8 @@ export const sendNewCommentEmail = async ( const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator - const { text } = comment + const { content } = comment + const text = richTextToString(content) let betDescription = '' if (bet) { diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 4719fd08..d7aa0c5e 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -1,13 +1,13 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { uniq } from 'lodash' - +import { compact, uniq } from 'lodash' import { getContract, getUser, getValues } from './utils' import { Comment } from '../../common/comment' import { sendNewCommentEmail } from './emails' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' import { createNotification } from './create-notification' +import { parseMentions, richTextToString } from '../../common/util/parse' const firestore = admin.firestore() @@ -71,7 +71,10 @@ export const onCreateCommentOnContract = functions const repliedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId - const recipients = repliedUserId ? [repliedUserId] : [] + + const recipients = uniq( + compact([...parseMentions(comment.content), repliedUserId]) + ) await createNotification( comment.id, @@ -79,7 +82,7 @@ export const onCreateCommentOnContract = functions 'created', commentCreator, eventId, - comment.text, + richTextToString(comment.content), { contract, relatedSourceType, recipients } ) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index f8e1d7e1..2a467f6d 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -8,8 +8,8 @@ import { RelativeTimestamp } from './relative-timestamp' import { UserLink } from './user-page' import { User } from 'common/user' import { Col } from './layout/col' -import { Linkify } from './linkify' import { groupBy } from 'lodash' +import { Content } from './editor' export function UserCommentsList(props: { user: User @@ -50,7 +50,8 @@ export function UserCommentsList(props: { function ProfileComment(props: { comment: Comment; className?: string }) { const { comment, className } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const { text, content, userUsername, userName, userAvatarUrl, createdTime } = + comment // TODO: find and attach relevant bets by comment betId at some point return ( <Row className={className}> @@ -64,7 +65,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) { />{' '} <RelativeTimestamp time={createdTime} /> </p> - <Linkify text={text} /> + <Content content={content || text} /> </div> </Row> ) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index deb9b857..6f1a778d 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -107,7 +107,6 @@ export function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} - truncate={false} smallAvatar={false} /> </div> diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 963cea7e..2371bbf8 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -41,14 +41,16 @@ export function useTextEditor(props: { max?: number defaultValue?: Content disabled?: boolean + simple?: boolean }) { - const { placeholder, max, defaultValue = '', disabled } = props + const { placeholder, max, defaultValue = '', disabled, simple } = props const users = useUsers() const editorClass = clsx( proseClass, - 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' + !simple && 'min-h-[6em]', + 'outline-none pt-2 px-4' ) const editor = useEditor( @@ -56,7 +58,8 @@ export function useTextEditor(props: { editorProps: { attributes: { class: editorClass } }, extensions: [ StarterKit.configure({ - heading: { levels: [1, 2, 3] }, + heading: simple ? false : { levels: [1, 2, 3] }, + horizontalRule: simple ? false : {}, }), Placeholder.configure({ placeholder, @@ -120,8 +123,9 @@ function isValidIframe(text: string) { export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> + children?: React.ReactNode // additional toolbar buttons }) { - const { editor, upload } = props + const { editor, upload, children } = props const [iframeOpen, setIframeOpen] = useState(false) return ( @@ -133,30 +137,13 @@ export function TextEditor(props: { editor={editor} className={clsx(proseClass, '-ml-2 mr-2 w-full text-slate-300 ')} > - Type <em>*markdown*</em>. Paste or{' '} - <FileUploadButton - className="link text-blue-300" - onFiles={upload.mutate} - > - upload - </FileUploadButton>{' '} - images! + Type <em>*markdown*</em> </FloatingMenu> )} - <div className="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> <EditorContent editor={editor} /> - {/* Spacer element to match the height of the toolbar */} - <div className="py-2" aria-hidden="true"> - {/* Matches height of button in toolbar (1px border + 36px content height) */} - <div className="py-px"> - <div className="h-9" /> - </div> - </div> - </div> - - {/* Toolbar, with buttons for image and embeds */} - <div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> - <div className="flex items-center space-x-5"> + {/* Toolbar, with buttons for images and embeds */} + <div className="flex h-9 items-center gap-5 pl-4 pr-1"> <div className="flex items-center"> <FileUploadButton onFiles={upload.mutate} @@ -181,6 +168,8 @@ export function TextEditor(props: { <span className="sr-only">Embed an iframe</span> </button> </div> + <div className="ml-auto" /> + {children} </div> </div> </div> diff --git a/web/components/editor/mention.tsx b/web/components/editor/mention.tsx index 3ad5de39..5ccea6f5 100644 --- a/web/components/editor/mention.tsx +++ b/web/components/editor/mention.tsx @@ -11,7 +11,7 @@ const name = 'mention-component' const MentionComponent = (props: any) => { return ( - <NodeViewWrapper className={clsx(name, 'not-prose inline text-indigo-700')}> + <NodeViewWrapper className={clsx(name, 'not-prose text-indigo-700')}> <Linkify text={'@' + props.node.attrs.label} /> </NodeViewWrapper> ) @@ -25,5 +25,6 @@ const MentionComponent = (props: any) => { export const DisplayMention = Mention.extend({ parseHTML: () => [{ tag: name }], renderHTML: ({ HTMLAttributes }) => [name, mergeAttributes(HTMLAttributes)], - addNodeView: () => ReactNodeViewRenderer(MentionComponent), + addNodeView: () => + ReactNodeViewRenderer(MentionComponent, { className: 'inline-block' }), }) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index aabb1081..edaf1fe5 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -31,9 +31,9 @@ export function FeedAnswerCommentGroup(props: { const { answer, contract, comments, tips, bets, user } = props const { username, avatarUrl, name, text } = answer - const [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState<Pick<User, 'id' | 'username'>>() const [showReply, setShowReply] = useState(false) - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [highlighted, setHighlighted] = useState(false) const router = useRouter() @@ -70,9 +70,14 @@ export function FeedAnswerCommentGroup(props: { const scrollAndOpenReplyInput = useEvent( (comment?: Comment, answer?: Answer) => { - setReplyToUsername(comment?.userUsername ?? answer?.username ?? '') + setReplyToUser( + comment + ? { id: comment.userId, username: comment.userUsername } + : answer + ? { id: answer.userId, username: answer.username } + : undefined + ) setShowReply(true) - inputRef?.focus() } ) @@ -80,7 +85,7 @@ export function FeedAnswerCommentGroup(props: { // Only show one comment input for a bet at a time if ( betsByCurrentUser.length > 1 && - inputRef?.textContent?.length === 0 && + // inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0] ?.outcome !== answer.number.toString() ) @@ -89,10 +94,6 @@ export function FeedAnswerCommentGroup(props: { // eslint-disable-next-line react-hooks/exhaustive-deps }, [betsByCurrentUser.length, user, answer.number]) - useEffect(() => { - if (showReply && inputRef) inputRef.focus() - }, [inputRef, showReply]) - useEffect(() => { if (router.asPath.endsWith(`#${answerElementId}`)) { setHighlighted(true) @@ -154,7 +155,6 @@ export function FeedAnswerCommentGroup(props: { commentsList={commentsList} betsByUserId={betsByUserId} smallAvatar={true} - truncate={false} bets={bets} tips={tips} scrollAndOpenReplyInput={scrollAndOpenReplyInput} @@ -172,12 +172,8 @@ export function FeedAnswerCommentGroup(props: { betsByCurrentUser={betsByCurrentUser} commentsByCurrentUser={commentsByCurrentUser} parentAnswerOutcome={answer.number.toString()} - replyToUsername={replyToUsername} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + replyToUser={replyToUser} + onSubmitComment={() => setShowReply(false)} /> </div> )} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index f4c6eb74..8c84039e 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -13,25 +13,22 @@ import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { OutcomeLabel } from 'web/components/outcome-label' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' -import { contractPath } from 'web/lib/firebase/contracts' import { firebaseLogin } from 'web/lib/firebase/users' import { createCommentOnContract, MAX_COMMENT_LENGTH, } from 'web/lib/firebase/comments' -import Textarea from 'react-expanding-textarea' -import { Linkify } from 'web/components/linkify' -import { SiteLink } from 'web/components/site-link' import { BetStatusText } from 'web/components/feed/feed-bets' import { Col } from 'web/components/layout/col' import { getProbability } from 'common/calculate' import { LoadingIndicator } from 'web/components/loading-indicator' import { PaperAirplaneIcon } from '@heroicons/react/outline' import { track } from 'web/lib/service/analytics' -import { useEvent } from 'web/hooks/use-event' import { Tipper } from '../tipper' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { useWindowSize } from 'web/hooks/use-window-size' +import { Content, TextEditor, useTextEditor } from '../editor' +import { Editor } from '@tiptap/react' export function FeedCommentThread(props: { contract: Contract @@ -39,20 +36,12 @@ export function FeedCommentThread(props: { tips: CommentTipMap parentComment: Comment bets: Bet[] - truncate?: boolean smallAvatar?: boolean }) { - const { - contract, - comments, - bets, - tips, - truncate, - smallAvatar, - parentComment, - } = props + const { contract, comments, bets, tips, smallAvatar, parentComment } = props const [showReply, setShowReply] = useState(false) - const [replyToUsername, setReplyToUsername] = useState('') + const [replyToUser, setReplyToUser] = + useState<{ id: string; username: string }>() const betsByUserId = groupBy(bets, (bet) => bet.userId) const user = useUser() const commentsList = comments.filter( @@ -60,15 +49,12 @@ export function FeedCommentThread(props: { parentComment.id && comment.replyToCommentId === parentComment.id ) commentsList.unshift(parentComment) - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) + function scrollAndOpenReplyInput(comment: Comment) { - setReplyToUsername(comment.userUsername) + setReplyToUser({ id: comment.userId, username: comment.userUsername }) setShowReply(true) - inputRef?.focus() } - useEffect(() => { - if (showReply && inputRef) inputRef.focus() - }, [inputRef, showReply]) + return ( <Col className={'w-full gap-3 pr-1'}> <span @@ -81,7 +67,6 @@ export function FeedCommentThread(props: { betsByUserId={betsByUserId} tips={tips} smallAvatar={smallAvatar} - truncate={truncate} bets={bets} scrollAndOpenReplyInput={scrollAndOpenReplyInput} /> @@ -98,13 +83,9 @@ export function FeedCommentThread(props: { (c) => c.userId === user?.id )} parentCommentId={parentComment.id} - replyToUsername={replyToUsername} + replyToUser={replyToUser} parentAnswerOutcome={comments[0].answerOutcome} - setRef={setInputRef} - onSubmitComment={() => { - setShowReply(false) - setReplyToUsername('') - }} + onSubmitComment={() => setShowReply(false)} /> </Col> )} @@ -121,14 +102,12 @@ export function CommentRepliesList(props: { bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean - truncate?: boolean }) { const { contract, commentsList, betsByUserId, tips, - truncate, smallAvatar, bets, scrollAndOpenReplyInput, @@ -168,7 +147,6 @@ export function CommentRepliesList(props: { : undefined } smallAvatar={smallAvatar} - truncate={truncate} /> </div> ))} @@ -182,7 +160,6 @@ export function FeedComment(props: { tips: CommentTips betsBySameUser: Bet[] probAtCreatedTime?: number - truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { @@ -192,10 +169,10 @@ export function FeedComment(props: { tips, betsBySameUser, probAtCreatedTime, - truncate, onReplyClick, } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + const { text, content, userUsername, userName, userAvatarUrl, createdTime } = + comment let betOutcome: string | undefined, bought: string | undefined, money: string | undefined @@ -276,11 +253,9 @@ export function FeedComment(props: { elementId={comment.id} /> </div> - <TruncatedComment - comment={text} - moreHref={contractPath(contract)} - shouldTruncate={truncate} - /> + <div className="mt-2 text-[15px] text-gray-700"> + <Content content={content || text} /> + </div> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> {onReplyClick && ( @@ -345,8 +320,7 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: Comment[] - replyToUsername?: string - setRef?: (ref: HTMLTextAreaElement) => void + replyToUser?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment @@ -359,12 +333,18 @@ export function CommentInput(props: { commentsByCurrentUser, parentAnswerOutcome, parentCommentId, - replyToUsername, + replyToUser, onSubmitComment, - setRef, } = props const user = useUser() - const [comment, setComment] = useState('') + const { editor, upload } = useTextEditor({ + simple: true, + max: MAX_COMMENT_LENGTH, + placeholder: + !!parentCommentId || !!parentAnswerOutcome + ? 'Write a reply...' + : 'Write a comment...', + }) const [isSubmitting, setIsSubmitting] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( @@ -380,18 +360,17 @@ export function CommentInput(props: { track('sign in to comment') return await firebaseLogin() } - if (!comment || isSubmitting) return + if (!editor || editor.isEmpty || isSubmitting) return setIsSubmitting(true) await createCommentOnContract( contract.id, - comment, + editor.getJSON(), user, betId, parentAnswerOutcome, parentCommentId ) onSubmitComment?.() - setComment('') setIsSubmitting(false) } @@ -446,14 +425,12 @@ export function CommentInput(props: { )} </div> <CommentInputTextArea - commentText={comment} - setComment={setComment} - isReply={!!parentCommentId || !!parentAnswerOutcome} - replyToUsername={replyToUsername ?? ''} + editor={editor} + upload={upload} + replyToUser={replyToUser} user={user} submitComment={submitComment} isSubmitting={isSubmitting} - setRef={setRef} presetId={id} /> </div> @@ -465,94 +442,93 @@ export function CommentInput(props: { export function CommentInputTextArea(props: { user: User | undefined | null - isReply: boolean - replyToUsername: string - commentText: string - setComment: (text: string) => void + replyToUser?: { id: string; username: string } + editor: Editor | null + upload: Parameters<typeof TextEditor>[0]['upload'] submitComment: (id?: string) => void isSubmitting: boolean - setRef?: (ref: HTMLTextAreaElement) => void + submitOnEnter?: boolean presetId?: string - enterToSubmitOnDesktop?: boolean }) { const { - isReply, - setRef, user, - commentText, - setComment, + editor, + upload, submitComment, presetId, isSubmitting, - replyToUsername, - enterToSubmitOnDesktop, + submitOnEnter, + replyToUser, } = props - const { width } = useWindowSize() - const memoizedSetComment = useEvent(setComment) + const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch) + useEffect(() => { - if (!replyToUsername || !user || replyToUsername === user.username) return - const replacement = `@${replyToUsername} ` - memoizedSetComment(replacement + commentText.replace(replacement, '')) + editor?.setEditable(!isSubmitting) + }, [isSubmitting, editor]) + + const submit = () => { + submitComment(presetId) + editor?.commands?.clearContent() + } + + useEffect(() => { + if (!editor) { + return + } + // submit on Enter key + editor.setOptions({ + editorProps: { + handleKeyDown: (view, event) => { + if ( + submitOnEnter && + event.key === 'Enter' && + !event.shiftKey && + (!isMobile || event.ctrlKey || event.metaKey) && + // mention list is closed + !(view.state as any).mention$.active + ) { + submit() + event.preventDefault() + return true + } + return false + }, + }, + }) + // insert at mention and focus + if (replyToUser) { + editor + .chain() + .setContent({ + type: 'mention', + attrs: { label: replyToUser.username, id: replyToUser.id }, + }) + .insertContent(' ') + .focus() + .run() + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, replyToUsername, memoizedSetComment]) + }, [editor]) + return ( <> - <Row className="gap-1.5 text-gray-700"> - <Textarea - ref={setRef} - value={commentText} - onChange={(e) => setComment(e.target.value)} - className={clsx('textarea textarea-bordered w-full resize-none')} - // Make room for floating submit button. - style={{ paddingRight: 48 }} - placeholder={ - isReply - ? 'Write a reply... ' - : enterToSubmitOnDesktop - ? 'Send a message' - : 'Write a comment...' - } - autoFocus={false} - maxLength={MAX_COMMENT_LENGTH} - disabled={isSubmitting} - onKeyDown={(e) => { - if ( - (enterToSubmitOnDesktop && - e.key === 'Enter' && - !e.shiftKey && - width && - width > 768) || - (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) - ) { - e.preventDefault() - submitComment(presetId) - e.currentTarget.blur() - } - }} - /> - - <Col className={clsx('relative justify-end')}> + <div> + <TextEditor editor={editor} upload={upload}> {user && !isSubmitting && ( <button - className={clsx( - 'btn btn-ghost btn-sm absolute right-2 bottom-2 flex-row pl-2 capitalize', - !commentText && 'pointer-events-none text-gray-500' - )} - onClick={() => { - submitComment(presetId) - }} + className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" + disabled={!editor || editor.isEmpty} + onClick={submit} > - <PaperAirplaneIcon - className={'m-0 min-w-[22px] rotate-90 p-0 '} - height={25} - /> + <PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" /> </button> )} + {isSubmitting && ( <LoadingIndicator spinnerClassName={'border-gray-500'} /> )} - </Col> - </Row> + </TextEditor> + </div> <Row> {!user && ( <button @@ -567,38 +543,6 @@ export function CommentInputTextArea(props: { ) } -export function TruncatedComment(props: { - comment: string - moreHref: string - shouldTruncate?: boolean -}) { - const { comment, moreHref, shouldTruncate } = props - let truncated = comment - - // Keep descriptions to at most 400 characters - const MAX_CHARS = 400 - if (shouldTruncate && truncated.length > MAX_CHARS) { - truncated = truncated.slice(0, MAX_CHARS) - // Make sure to end on a space - const i = truncated.lastIndexOf(' ') - truncated = truncated.slice(0, i) - } - - return ( - <div - className="mt-2 whitespace-pre-line break-words text-gray-700" - style={{ fontSize: 15 }} - > - <Linkify text={truncated} /> - {truncated != comment && ( - <SiteLink href={moreHref} className="text-indigo-700"> - ... (show more) - </SiteLink> - )} - </div> - ) -} - function getBettorsLargestPositionBeforeTime( contract: Contract, createdTime: number, diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 47258b09..2d25351a 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -5,24 +5,19 @@ import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments' -import { - CommentInputTextArea, - TruncatedComment, -} from 'web/components/feed/feed-comments' +import { CommentInputTextArea } from 'web/components/feed/feed-comments' import { track } from 'web/lib/service/analytics' import { firebaseLogin } from 'web/lib/firebase/users' - import { useRouter } from 'next/router' import clsx from 'clsx' import { UserLink } from 'web/components/user-page' - -import { groupPath } from 'web/lib/firebase/groups' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' +import { Content, useTextEditor } from 'web/components/editor' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -34,16 +29,18 @@ export function GroupChat(props: { tips: CommentTipMap }) { const { messages, user, group, tips } = props - const [messageText, setMessageText] = useState('') + const { editor, upload } = useTextEditor({ + simple: true, + placeholder: 'Send a message', + }) const [isSubmitting, setIsSubmitting] = useState(false) const [scrollToBottomRef, setScrollToBottomRef] = useState<HTMLDivElement | null>(null) const [scrollToMessageId, setScrollToMessageId] = useState('') const [scrollToMessageRef, setScrollToMessageRef] = useState<HTMLDivElement | null>(null) - const [replyToUsername, setReplyToUsername] = useState('') - const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) - const [groupedMessages, setGroupedMessages] = useState<Comment[]>([]) + const [replyToUser, setReplyToUser] = useState<any>() + const router = useRouter() const isMember = user && group.memberIds.includes(user?.id) @@ -54,25 +51,26 @@ export function GroupChat(props: { const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight - useMemo(() => { + // array of groups, where each group is an array of messages that are displayed as one + const groupedMessages = useMemo(() => { // Group messages with createdTime within 2 minutes of each other. - const tempMessages = [] + const tempGrouped: Comment[][] = [] for (let i = 0; i < messages.length; i++) { const message = messages[i] - if (i === 0) tempMessages.push({ ...message }) + if (i === 0) tempGrouped.push([message]) else { const prevMessage = messages[i - 1] const diff = message.createdTime - prevMessage.createdTime const creatorsMatch = message.userId === prevMessage.userId if (diff < 2 * 60 * 1000 && creatorsMatch) { - tempMessages[tempMessages.length - 1].text += `\n${message.text}` + tempGrouped.at(-1)?.push(message) } else { - tempMessages.push({ ...message }) + tempGrouped.push([message]) } } } - setGroupedMessages(tempMessages) + return tempGrouped }, [messages]) useEffect(() => { @@ -94,11 +92,12 @@ export function GroupChat(props: { useEffect(() => { // is mobile? - if (inputRef && width && width > 720) inputRef.focus() - }, [inputRef, width]) + if (width && width > 720) focusInput() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [width]) function onReplyClick(comment: Comment) { - setReplyToUsername(comment.userUsername) + setReplyToUser({ id: comment.userId, username: comment.userUsername }) } async function submitMessage() { @@ -106,13 +105,16 @@ export function GroupChat(props: { track('sign in to comment') return await firebaseLogin() } - if (!messageText || isSubmitting) return + if (!editor || editor.isEmpty || isSubmitting) return setIsSubmitting(true) - await createCommentOnGroup(group.id, messageText, user) - setMessageText('') + await createCommentOnGroup(group.id, editor.getJSON(), user) + editor.commands.clearContent() setIsSubmitting(false) - setReplyToUsername('') - inputRef?.focus() + setReplyToUser(undefined) + focusInput() + } + function focusInput() { + editor?.commands.focus() } return ( @@ -123,20 +125,20 @@ export function GroupChat(props: { } ref={setScrollToBottomRef} > - {groupedMessages.map((message) => ( + {groupedMessages.map((messages) => ( <GroupMessage user={user} - key={message.id} - comment={message} + key={`group ${messages[0].id}`} + comments={messages} group={group} onReplyClick={onReplyClick} - highlight={message.id === scrollToMessageId} + highlight={messages[0].id === scrollToMessageId} setRef={ - scrollToMessageId === message.id + scrollToMessageId === messages[0].id ? setScrollToMessageRef : undefined } - tips={tips[message.id] ?? {}} + tips={tips[messages[0].id] ?? {}} /> ))} {messages.length === 0 && ( @@ -144,7 +146,7 @@ export function GroupChat(props: { No messages yet. Why not{isMember ? ` ` : ' join and '} <button className={'cursor-pointer font-bold text-gray-700'} - onClick={() => inputRef?.focus()} + onClick={focusInput} > add one? </button> @@ -162,15 +164,13 @@ export function GroupChat(props: { </div> <div className={'flex-1'}> <CommentInputTextArea - commentText={messageText} - setComment={setMessageText} - isReply={false} + editor={editor} + upload={upload} user={user} - replyToUsername={replyToUsername} + replyToUser={replyToUser} submitComment={submitMessage} isSubmitting={isSubmitting} - enterToSubmitOnDesktop={true} - setRef={setInputRef} + submitOnEnter /> </div> </div> @@ -292,16 +292,18 @@ function GroupChatNotificationsIcon(props: { const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined - comment: Comment + comments: Comment[] group: Group onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void highlight?: boolean tips: CommentTips }) { - const { comment, onReplyClick, group, setRef, highlight, user, tips } = props - const { text, userUsername, userName, userAvatarUrl, createdTime } = comment - const isCreatorsComment = user && comment.userId === user.id + const { comments, onReplyClick, group, setRef, highlight, user, tips } = props + const first = comments[0] + const { id, userUsername, userName, userAvatarUrl, createdTime } = first + + const isCreatorsComment = user && first.userId === user.id return ( <Col ref={setRef} @@ -331,23 +333,21 @@ const GroupMessage = memo(function GroupMessage_(props: { prefix={'group'} slug={group.slug} createdTime={createdTime} - elementId={comment.id} - /> - </Row> - <Row className={'text-black'}> - <TruncatedComment - comment={text} - moreHref={groupPath(group.slug)} - shouldTruncate={false} + elementId={id} /> </Row> + <div className="mt-2 text-black"> + {comments.map((comment) => ( + <Content content={comment.content || comment.text} /> + ))} + </div> <Row> {!isCreatorsComment && onReplyClick && ( <button className={ 'self-start py-1 text-xs font-bold text-gray-500 hover:underline' } - onClick={() => onReplyClick(comment)} + onClick={() => onReplyClick(first)} > Reply </button> @@ -357,7 +357,7 @@ const GroupMessage = memo(function GroupMessage_(props: { {formatMoney(sum(Object.values(tips)))} </span> )} - {!isCreatorsComment && <Tipper comment={comment} tips={tips} />} + {!isCreatorsComment && <Tipper comment={first} tips={tips} />} </Row> </Col> ) diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index 5775a2bb..e82c6d45 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -14,6 +14,7 @@ import { User } from 'common/user' import { Comment } from 'common/comment' import { removeUndefinedProps } from 'common/util/object' import { track } from '@amplitude/analytics-browser' +import { JSONContent } from '@tiptap/react' export type { Comment } @@ -21,7 +22,7 @@ export const MAX_COMMENT_LENGTH = 10000 export async function createCommentOnContract( contractId: string, - text: string, + content: JSONContent, commenter: User, betId?: string, answerOutcome?: string, @@ -34,7 +35,7 @@ export async function createCommentOnContract( id: ref.id, contractId, userId: commenter.id, - text: text.slice(0, MAX_COMMENT_LENGTH), + content: content, createdTime: Date.now(), userName: commenter.name, userUsername: commenter.username, @@ -53,7 +54,7 @@ export async function createCommentOnContract( } export async function createCommentOnGroup( groupId: string, - text: string, + content: JSONContent, user: User, replyToCommentId?: string ) { @@ -62,7 +63,7 @@ export async function createCommentOnGroup( id: ref.id, groupId, userId: user.id, - text: text.slice(0, MAX_COMMENT_LENGTH), + content: content, createdTime: Date.now(), userName: user.name, userUsername: user.username, diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 0da6c994..5866f899 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -354,7 +354,6 @@ function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} - truncate={false} smallAvatar={false} /> </div> From da977f62a9401b28e56284ec5765322f6239003e Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Sat, 6 Aug 2022 15:43:41 -0700 Subject: [PATCH 443/519] Make added text go after img instead of replacing (#725) --- web/components/contract/contract-description.tsx | 10 +++------- web/components/contract/contract-details.tsx | 11 +++++------ web/components/editor/utils.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 web/components/editor/utils.ts diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index f9db0cd9..4c9b77a2 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -13,6 +13,7 @@ import { TextEditor, useTextEditor } from 'web/components/editor' import { Button } from '../button' import { Spacer } from '../layout/spacer' import { Editor, Content as ContentType } from '@tiptap/react' +import { appendToEditor } from '../editor/utils' export function ContractDescription(props: { contract: Contract @@ -94,12 +95,7 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) { size="xs" onClick={() => { setEditing(true) - editor - ?.chain() - .setContent(contract.description) - .focus('end') - .insertContent(`<p>${editTimestamp()}</p>`) - .run() + appendToEditor(editor, `<p>${editTimestamp()}</p>`) }} > Edit description @@ -131,7 +127,7 @@ function EditQuestion(props: { function joinContent(oldContent: ContentType, newContent: string) { const editor = new Editor({ content: oldContent, extensions: exhibitExts }) - editor.chain().focus('end').insertContent(newContent).run() + appendToEditor(editor, newContent) return editor.getJSON() } diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 936f5e24..90b5f3d1 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -33,6 +33,7 @@ import { Col } from 'web/components/layout/col' import { ContractGroupsList } from 'web/components/groups/contract-groups-list' import { SiteLink } from 'web/components/site-link' import { groupPath } from 'web/lib/firebase/groups' +import { appendToEditor } from '../editor/utils' export type ShowTime = 'resolve-date' | 'close-date' @@ -282,12 +283,10 @@ function EditableCloseDate(props: { const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') const editor = new Editor({ content, extensions: exhibitExts }) - editor - .chain() - .focus('end') - .insertContent('<br /><br />') - .insertContent(`Close date updated to ${formattedCloseDate}`) - .run() + appendToEditor( + editor, + `<br><p>Close date updated to ${formattedCloseDate}</p>` + ) updateContract(contract.id, { closeTime: newCloseTime, diff --git a/web/components/editor/utils.ts b/web/components/editor/utils.ts new file mode 100644 index 00000000..74af38c5 --- /dev/null +++ b/web/components/editor/utils.ts @@ -0,0 +1,10 @@ +import { Editor, Content } from '@tiptap/react' + +export function appendToEditor(editor: Editor | null, content: Content) { + editor + ?.chain() + .focus('end') + .createParagraphNear() + .insertContent(content) + .run() +} From 1f8aef2891e3e900a4c995358ee950bc1d97257c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 6 Aug 2022 17:45:19 -0700 Subject: [PATCH 444/519] Disable challenges for private instances --- common/challenge.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/challenge.ts b/common/challenge.ts index 1a227f94..9bac8c08 100644 --- a/common/challenge.ts +++ b/common/challenge.ts @@ -1,3 +1,5 @@ +import { IS_PRIVATE_MANIFOLD } from './envs/constants' + export type Challenge = { // The link to send: https://manifold.markets/challenges/username/market-slug/{slug} // Also functions as the unique id for the link. @@ -60,4 +62,4 @@ export type Acceptance = { createdTime: number } -export const CHALLENGES_ENABLED = true +export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD From abd344b951ebafc22d701d15f0b6b210ee93dd2c Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 6 Aug 2022 19:24:50 -0700 Subject: [PATCH 445/519] Fix a bug with expiration of refresh and custom tokens --- web/lib/firebase/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index b363189c..5363aa08 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -7,8 +7,8 @@ const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10 const TOKEN_KINDS = ['refresh', 'id', 'custom'] as const const TOKEN_AGES = { id: ONE_HOUR_SECS, - refresh: ONE_HOUR_SECS, - custom: TEN_YEARS_SECS, + refresh: TEN_YEARS_SECS, + custom: ONE_HOUR_SECS, } as const export type TokenKind = typeof TOKEN_KINDS[number] From 012b67e3c549de14a7658c525ef4b91e6c5277fc Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 7 Aug 2022 09:56:42 -0700 Subject: [PATCH 446/519] Revert "Fix a bug with expiration of refresh and custom tokens" This reverts commit abd344b951ebafc22d701d15f0b6b210ee93dd2c. --- web/lib/firebase/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index 5363aa08..b363189c 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -7,8 +7,8 @@ const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10 const TOKEN_KINDS = ['refresh', 'id', 'custom'] as const const TOKEN_AGES = { id: ONE_HOUR_SECS, - refresh: TEN_YEARS_SECS, - custom: ONE_HOUR_SECS, + refresh: ONE_HOUR_SECS, + custom: TEN_YEARS_SECS, } as const export type TokenKind = typeof TOKEN_KINDS[number] From a910e5dc174e431e6722ee783a366217599dd417 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 7 Aug 2022 09:57:18 -0700 Subject: [PATCH 447/519] Revert "Revert "Fix a bug with expiration of refresh and custom tokens"" This reverts commit 012b67e3c549de14a7658c525ef4b91e6c5277fc. --- web/lib/firebase/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index b363189c..5363aa08 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -7,8 +7,8 @@ const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10 const TOKEN_KINDS = ['refresh', 'id', 'custom'] as const const TOKEN_AGES = { id: ONE_HOUR_SECS, - refresh: ONE_HOUR_SECS, - custom: TEN_YEARS_SECS, + refresh: TEN_YEARS_SECS, + custom: ONE_HOUR_SECS, } as const export type TokenKind = typeof TOKEN_KINDS[number] From 8fb3b42ea1c86c4ee6dcd59ac64df61b1e31e1b9 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 7 Aug 2022 16:43:53 -0700 Subject: [PATCH 448/519] Default to trending. Fix close date being opposite --- web/pages/contract-search-firestore.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 9a09b101..eb609d26 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -27,7 +27,7 @@ export default function ContractSearchFirestore(props: { const { querySortOptions, additionalFilter } = props const { initialSort, initialQuery } = useInitialQueryAndSort(querySortOptions) - const [sort, setSort] = useState(initialSort || 'newest') + const [sort, setSort] = useState(initialSort ?? 'score') const [query, setQuery] = useState(initialQuery) let matches = (contracts ?? []).filter((c) => @@ -48,11 +48,7 @@ export default function ContractSearchFirestore(props: { matches.sort((a, b) => a.createdTime - b.createdTime) } else if (sort === 'close-date') { matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours) - matches = sortBy( - matches, - (contract) => - (sort === 'close-date' ? -1 : 1) * (contract.closeTime ?? Infinity) - ) + matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity) } else if (sort === 'most-traded') { matches.sort((a, b) => b.volume - a.volume) } else if (sort === 'score') { @@ -109,9 +105,8 @@ export default function ContractSearchFirestore(props: { value={sort} onChange={(e) => setSort(e.target.value as Sort)} > - <option value="newest">Newest</option> - <option value="oldest">Oldest</option> <option value="score">Trending</option> + <option value="newest">Newest</option> <option value="most-traded">Most traded</option> <option value="24-hour-vol">24h volume</option> <option value="close-date">Closing soon</option> From 98806a806f7a0657b5d72502d921db1f82037910 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 7 Aug 2022 16:50:12 -0700 Subject: [PATCH 449/519] Fix query params on emulator/private instance --- web/hooks/use-sort-and-query-params.tsx | 49 +------------------------ web/pages/contract-search-firestore.tsx | 8 ++-- 2 files changed, 4 insertions(+), 53 deletions(-) diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index ad009443..c4bce0c0 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -1,4 +1,4 @@ -import { defaults, debounce } from 'lodash' +import { debounce } from 'lodash' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import { DEFAULT_SORT } from 'web/components/contract-search' @@ -25,53 +25,6 @@ export function getSavedSort() { } } -export function useInitialQueryAndSort(options?: { - defaultSort: Sort - shouldLoadFromStorage?: boolean -}) { - const { defaultSort, shouldLoadFromStorage } = defaults(options, { - defaultSort: DEFAULT_SORT, - shouldLoadFromStorage: true, - }) - const router = useRouter() - - const [initialSort, setInitialSort] = useState<Sort | undefined>(undefined) - const [initialQuery, setInitialQuery] = useState('') - - useEffect(() => { - // If there's no sort option, then set the one from localstorage - if (router.isReady) { - const { s: sort, q: query } = router.query as { - q?: string - s?: Sort - } - - setInitialQuery(query ?? '') - - if (!sort && shouldLoadFromStorage) { - console.log('ready loading from storage ', sort ?? defaultSort) - const localSort = getSavedSort() - if (localSort) { - // Use replace to not break navigating back. - router.replace( - { query: { ...router.query, s: localSort } }, - undefined, - { shallow: true } - ) - } - setInitialSort(localSort ?? defaultSort) - } else { - setInitialSort(sort ?? defaultSort) - } - } - }, [defaultSort, router.isReady, shouldLoadFromStorage]) - - return { - initialSort, - initialQuery, - } -} - export function useQueryAndSortParams(options?: { defaultSort?: Sort shouldLoadFromStorage?: boolean diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index eb609d26..e2ca308c 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -1,12 +1,11 @@ import { Answer } from 'common/answer' import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' -import { useState } from 'react' import { ContractsGrid } from 'web/components/contract/contracts-grid' import { useContracts } from 'web/hooks/use-contracts' import { Sort, - useInitialQueryAndSort, + useQueryAndSortParams, } from 'web/hooks/use-sort-and-query-params' const MAX_CONTRACTS_RENDERED = 100 @@ -26,9 +25,8 @@ export default function ContractSearchFirestore(props: { const contracts = useContracts() const { querySortOptions, additionalFilter } = props - const { initialSort, initialQuery } = useInitialQueryAndSort(querySortOptions) - const [sort, setSort] = useState(initialSort ?? 'score') - const [query, setQuery] = useState(initialQuery) + const { query, setQuery, sort, setSort } = + useQueryAndSortParams(querySortOptions) let matches = (contracts ?? []).filter((c) => searchInAny( From 85e55312ca43a353990a1aab9edffdba0180103a Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 8 Aug 2022 15:05:25 -0700 Subject: [PATCH 450/519] What will be removed, is removed (#721) --- web/pages/trades.tsx | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 web/pages/trades.tsx diff --git a/web/pages/trades.tsx b/web/pages/trades.tsx deleted file mode 100644 index a29fb7f0..00000000 --- a/web/pages/trades.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Router from 'next/router' -import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' - -export const getServerSideProps = redirectIfLoggedOut('/') - -// Deprecated: redirects to /portfolio. -// Eventually, this will be removed. -export default function TradesPage() { - Router.replace('/portfolio') -} From fd308151b355e26ee15eec9b280216252232c757 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 8 Aug 2022 15:24:28 -0700 Subject: [PATCH 451/519] Disable bouncing Challenge --- web/components/contract/share-row.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx index 9c8c1573..9011ff1b 100644 --- a/web/components/contract/share-row.tsx +++ b/web/components/contract/share-row.tsx @@ -52,7 +52,6 @@ export function ShareRow(props: { () => setIsOpen(true), 'click challenge button' )} - className="animate-bounce" > ⚔️ Challenge <CreateChallengeModal From 564916134834fab8a9779a58090bda427bdc7a98 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 8 Aug 2022 22:42:52 -0700 Subject: [PATCH 452/519] Pass page props user to auth provider if present (#724) * Pass page props user to auth provider if present * Rename `user` -> `serverUser` * Don't load from local storage if server told us a user --- web/components/auth-context.tsx | 21 +++++++++++++-------- web/pages/_app.tsx | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 24adde25..332c96be 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect } from 'react' +import { ReactNode, createContext, useEffect } from 'react' import { User } from 'common/user' import { onIdTokenChanged } from 'firebase/auth' import { @@ -28,15 +28,20 @@ const ensureDeviceToken = () => { return deviceToken } -export const AuthContext = createContext<AuthUser>(null) - -export function AuthProvider({ children }: any) { - const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(undefined) +export const AuthContext = createContext<AuthUser>(undefined) +export function AuthProvider(props: { + children: ReactNode + serverUser?: AuthUser +}) { + const { children, serverUser } = props + const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(serverUser) useEffect(() => { - const cachedUser = localStorage.getItem(CACHED_USER_KEY) - setAuthUser(cachedUser && JSON.parse(cachedUser)) - }, [setAuthUser]) + if (serverUser === undefined) { + const cachedUser = localStorage.getItem(CACHED_USER_KEY) + setAuthUser(cachedUser && JSON.parse(cachedUser)) + } + }, [setAuthUser, serverUser]) useEffect(() => { return onIdTokenChanged(auth, async (fbUser) => { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 14dd6cf0..42b5e922 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -79,7 +79,7 @@ function MyApp({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1, maximum-scale=1" /> </Head> - <AuthProvider> + <AuthProvider serverUser={pageProps.user}> <QueryClientProvider client={queryClient}> <Welcome {...pageProps} /> <Component {...pageProps} /> From e7f1d3924bce982482ede0ee628641ed123085e4 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Mon, 8 Aug 2022 22:43:04 -0700 Subject: [PATCH 453/519] Fix up several pages to load user data on the server (#722) * Fix up several pages to load user data on the server * Add key prop to `EditUserField` --- web/lib/firebase/users.ts | 5 ++ web/pages/create.tsx | 27 +++---- web/pages/links.tsx | 19 ++--- web/pages/notifications.tsx | 23 ++---- web/pages/profile.tsx | 146 ++++++++++++++++-------------------- 5 files changed, 101 insertions(+), 119 deletions(-) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 5e00affe..3096f00f 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -52,6 +52,11 @@ export async function getUser(userId: string) { return (await getDoc(doc(users, userId))).data()! } +export async function getPrivateUser(userId: string) { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return (await getDoc(doc(users, userId))).data()! +} + export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. const q = query(users, where('username', '==', username), limit(1)) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 642cbaec..19ab2fe0 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx' import dayjs from 'dayjs' import Textarea from 'react-expanding-textarea' import { Spacer } from 'web/components/layout/spacer' -import { useUser } from 'web/hooks/use-user' +import { getUser } from 'web/lib/firebase/users' import { Contract, contractPath } from 'web/lib/firebase/contracts' import { createMarket } from 'web/lib/firebase/api' import { FIXED_ANTE } from 'common/antes' @@ -33,7 +33,10 @@ import { Title } from 'web/components/title' import { SEO } from 'web/components/SEO' import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers' -export const getServerSideProps = redirectIfLoggedOut('/') +export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { + const user = await getUser(creds.user.uid) + return { props: { user } } +}) type NewQuestionParams = { groupId?: string @@ -49,8 +52,9 @@ type NewQuestionParams = { initValue?: string } -export default function Create() { +export default function Create(props: { user: User }) { useTracking('view create page') + const { user } = props const router = useRouter() const params = router.query as NewQuestionParams // TODO: Not sure why Question is pulled out as its own component; @@ -60,8 +64,7 @@ export default function Create() { setQuestion(params.q ?? '') }, [params.q]) - const creator = useUser() - if (!router.isReady || !creator) return <div /> + if (!router.isReady) return <div /> return ( <Page> @@ -93,7 +96,7 @@ export default function Create() { </div> </form> <Spacer h={6} /> - <NewContract question={question} params={params} creator={creator} /> + <NewContract question={question} params={params} creator={user} /> </div> </div> </Page> @@ -102,7 +105,7 @@ export default function Create() { // Allow user to create a new contract export function NewContract(props: { - creator?: User | null + creator: User question: string params?: NewQuestionParams }) { @@ -120,14 +123,14 @@ export function NewContract(props: { const [answers, setAnswers] = useState<string[]>([]) // for multiple choice useEffect(() => { - if (groupId && creator) + if (groupId) getGroup(groupId).then((group) => { if (group && canModifyGroupContracts(group, creator.id)) { setSelectedGroup(group) setShowGroupSelector(false) } }) - }, [creator, groupId]) + }, [creator.id, groupId]) const [ante, _setAnte] = useState(FIXED_ANTE) // If params.closeTime is set, extract out the specified date and time @@ -152,7 +155,7 @@ export function NewContract(props: { ? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf() : undefined - const balance = creator?.balance || 0 + const balance = creator.balance || 0 const min = minString ? parseFloat(minString) : undefined const max = maxString ? parseFloat(maxString) : undefined @@ -214,7 +217,7 @@ export function NewContract(props: { async function submit() { // TODO: Tell users why their contract is invalid - if (!creator || !isValid) return + if (!isValid) return setIsSubmitting(true) try { const result = await createMarket( @@ -249,8 +252,6 @@ export function NewContract(props: { } } - if (!creator) return <></> - return ( <div> <label className="label"> diff --git a/web/pages/links.tsx b/web/pages/links.tsx index be3015ee..55939b19 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -11,10 +11,11 @@ import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' import { Subtitle } from 'web/components/subtitle' -import { useUser } from 'web/hooks/use-user' +import { getUser } from 'web/lib/firebase/users' import { useUserManalinks } from 'web/lib/firebase/manalinks' import { useUserById } from 'web/hooks/use-user' import { ManalinkTxn } from 'common/txn' +import { User } from 'common/user' import { Avatar } from 'web/components/avatar' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { UserLink } from 'web/components/user-page' @@ -27,15 +28,19 @@ import { Manalink } from 'common/manalink' import { REFERRAL_AMOUNT } from 'common/user' const LINKS_PER_PAGE = 24 -export const getServerSideProps = redirectIfLoggedOut('/') + +export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { + const user = await getUser(creds.user.uid) + return { props: { user } } +}) export function getManalinkUrl(slug: string) { return `${location.protocol}//${location.host}/link/${slug}` } -export default function LinkPage() { - const user = useUser() - const links = useUserManalinks(user?.id ?? '') +export default function LinkPage(props: { user: User }) { + const { user } = props + const links = useUserManalinks(user.id ?? '') // const manalinkTxns = useManalinkTxns(user?.id ?? '') const [highlightedSlug, setHighlightedSlug] = useState('') const unclaimedLinks = links.filter( @@ -44,10 +49,6 @@ export default function LinkPage() { (l.expiresTime == null || l.expiresTime > Date.now()) ) - if (user == null) { - return null - } - return ( <Page> <SEO diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 89ffb5d9..69139f9c 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,5 +1,4 @@ import { Tabs } from 'web/components/layout/tabs' -import { usePrivateUser } from 'web/hooks/use-user' import React, { useEffect, useMemo, useState } from 'react' import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' @@ -13,9 +12,8 @@ import { MANIFOLD_AVATAR_URL, MANIFOLD_USERNAME, PrivateUser, - User, } from 'common/user' -import { getUser } from 'web/lib/firebase/users' +import { getPrivateUser } from 'web/lib/firebase/users' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { Linkify } from 'web/components/linkify' @@ -35,7 +33,6 @@ import { formatMoney } from 'common/util/format' import { groupPath } from 'web/lib/firebase/groups' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { groupBy, sum, uniq } from 'lodash' -import Custom404 from 'web/pages/404' import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' @@ -49,13 +46,12 @@ const MULTIPLE_USERS_KEY = 'multipleUsers' const HIGHLIGHT_CLASS = 'bg-indigo-50' export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const user = await getUser(creds.user.uid) - return { props: { user } } + const privateUser = await getPrivateUser(creds.user.uid) + return { props: { privateUser } } }) -export default function Notifications(props: { user: User }) { - const { user } = props - const privateUser = usePrivateUser(user?.id) +export default function Notifications(props: { privateUser: PrivateUser }) { + const { privateUser } = props const local = safeLocalStorage() let localNotifications = [] as Notification[] const localSavedNotificationGroups = local?.getItem('notification-groups') @@ -67,7 +63,6 @@ export default function Notifications(props: { user: User }) { .flat() } - if (!user) return <Custom404 /> return ( <Page> <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> @@ -81,17 +76,11 @@ export default function Notifications(props: { user: User }) { tabs={[ { title: 'Notifications', - content: privateUser ? ( + content: ( <NotificationsList privateUser={privateUser} cachedNotifications={localNotifications} /> - ) : ( - <div className={'min-h-[100vh]'}> - <RenderNotificationGroups - notificationGroups={localNotificationGroups} - /> - </div> ), }, { diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 541f5de9..42bcb5c3 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -1,25 +1,35 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { RefreshIcon } from '@heroicons/react/outline' import { AddFundsButton } from 'web/components/add-funds-button' import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' -import { usePrivateUser, useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { changeUserInfo } from 'web/lib/firebase/api' import { uploadImage } from 'web/lib/firebase/storage' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' -import { User } from 'common/user' -import { updateUser, updatePrivateUser } from 'web/lib/firebase/users' +import { User, PrivateUser } from 'common/user' +import { + getUser, + getPrivateUser, + updateUser, + updatePrivateUser, +} from 'web/lib/firebase/users' import { defaultBannerUrl } from 'web/components/user-page' import { SiteLink } from 'web/components/site-link' import Textarea from 'react-expanding-textarea' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' -export const getServerSideProps = redirectIfLoggedOut('/') +export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { + const [user, privateUser] = await Promise.all([ + getUser(creds.user.uid), + getPrivateUser(creds.user.uid), + ]) + return { props: { user, privateUser } } +}) function EditUserField(props: { user: User @@ -58,64 +68,45 @@ function EditUserField(props: { ) } -export default function ProfilePage() { - const user = useUser() - const privateUser = usePrivateUser(user?.id) - - const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '') +export default function ProfilePage(props: { + user: User + privateUser: PrivateUser +}) { + const { user, privateUser } = props + const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '') const [avatarLoading, setAvatarLoading] = useState(false) - const [name, setName] = useState(user?.name || '') - const [username, setUsername] = useState(user?.username || '') - const [apiKey, setApiKey] = useState(privateUser?.apiKey || '') - - useEffect(() => { - if (user) { - setAvatarUrl(user.avatarUrl || '') - setName(user.name || '') - setUsername(user.username || '') - } - }, [user]) - - useEffect(() => { - if (privateUser) { - setApiKey(privateUser.apiKey || '') - } - }, [privateUser]) + const [name, setName] = useState(user.name) + const [username, setUsername] = useState(user.username) + const [apiKey, setApiKey] = useState(privateUser.apiKey || '') const updateDisplayName = async () => { const newName = cleanDisplayName(name) - if (newName) { setName(newName) - await changeUserInfo({ name: newName }).catch((_) => - setName(user?.name || '') - ) + await changeUserInfo({ name: newName }).catch((_) => setName(user.name)) } else { - setName(user?.name || '') + setName(user.name) } } const updateUsername = async () => { const newUsername = cleanUsername(username) - if (newUsername) { setUsername(newUsername) await changeUserInfo({ username: newUsername }).catch((_) => - setUsername(user?.username || '') + setUsername(user.username) ) } else { - setUsername(user?.username || '') + setUsername(user.username) } } const updateApiKey = async (e: React.MouseEvent) => { const newApiKey = crypto.randomUUID() - if (user?.id != null) { - setApiKey(newApiKey) - await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => { - setApiKey(privateUser?.apiKey || '') - }) - } + setApiKey(newApiKey) + await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => { + setApiKey(privateUser.apiKey || '') + }) e.preventDefault() } @@ -124,7 +115,7 @@ export default function ProfilePage() { setAvatarLoading(true) - await uploadImage(user?.username || 'default', file) + await uploadImage(user.username, file) .then(async (url) => { await changeUserInfo({ avatarUrl: url }) setAvatarUrl(url) @@ -132,14 +123,10 @@ export default function ProfilePage() { }) .catch(() => { setAvatarLoading(false) - setAvatarUrl(user?.avatarUrl || '') + setAvatarUrl(user.avatarUrl || '') }) } - if (user == null) { - return <></> - } - return ( <Page> <SEO title="Profile" description="User profile settings" url="/profile" /> @@ -147,7 +134,7 @@ export default function ProfilePage() { <Col className="max-w-lg rounded bg-white p-6 shadow-md sm:mx-auto"> <Row className="justify-between"> <Title className="!mt-0" text="Edit Profile" /> - <SiteLink className="btn btn-primary" href={`/${user?.username}`}> + <SiteLink className="btn btn-primary" href={`/${user.username}`}> Done </SiteLink> </Row> @@ -192,54 +179,53 @@ export default function ProfilePage() { /> </div> - {user && ( - <> - {/* TODO: Allow users with M$ 2000 of assets to set custom banners */} - {/* <EditUserField + {/* TODO: Allow users with M$ 2000 of assets to set custom banners */} + {/* <EditUserField user={user} field="bannerUrl" label="Banner Url" isEditing={isEditing} /> */} - <label className="label"> - Banner image{' '} - <span className="text-sm text-gray-400"> - Not editable for now - </span> - </label> - <div - className="h-32 w-full bg-cover bg-center sm:h-40" - style={{ - backgroundImage: `url(${ - user.bannerUrl || defaultBannerUrl(user.id) - })`, - }} - /> + <label className="label"> + Banner image{' '} + <span className="text-sm text-gray-400">Not editable for now</span> + </label> + <div + className="h-32 w-full bg-cover bg-center sm:h-40" + style={{ + backgroundImage: `url(${ + user.bannerUrl || defaultBannerUrl(user.id) + })`, + }} + /> - {( - [ - ['bio', 'Bio'], - ['website', 'Website URL'], - ['twitterHandle', 'Twitter'], - ['discordHandle', 'Discord'], - ] as const - ).map(([field, label]) => ( - <EditUserField user={user} field={field} label={label} /> - ))} - </> - )} + {( + [ + ['bio', 'Bio'], + ['website', 'Website URL'], + ['twitterHandle', 'Twitter'], + ['discordHandle', 'Discord'], + ] as const + ).map(([field, label]) => ( + <EditUserField + key={field} + user={user} + field={field} + label={label} + /> + ))} <div> <label className="label">Email</label> <div className="ml-1 text-gray-500"> - {privateUser?.email ?? '\u00a0'} + {privateUser.email ?? '\u00a0'} </div> </div> <div> <label className="label">Balance</label> <Row className="ml-1 items-start gap-4 text-gray-500"> - {formatMoney(user?.balance || 0)} + {formatMoney(user.balance)} <AddFundsButton /> </Row> </div> From 592125b5e7eec06af499ef0380c663866a27fec0 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 08:50:11 -0700 Subject: [PATCH 454/519] Fix broken `useBets` filters (#731) --- web/hooks/use-bets.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/hooks/use-bets.ts b/web/hooks/use-bets.ts index 38b73dd1..9155d25e 100644 --- a/web/hooks/use-bets.ts +++ b/web/hooks/use-bets.ts @@ -14,21 +14,22 @@ export const useBets = ( options?: { filterChallenges: boolean; filterRedemptions: boolean } ) => { const [bets, setBets] = useState<Bet[] | undefined>() - + const filterChallenges = !!options?.filterChallenges + const filterRedemptions = !!options?.filterRedemptions useEffect(() => { if (contractId) return listenForBets(contractId, (bets) => { - if (options) + if (filterChallenges || filterRedemptions) setBets( bets.filter( (bet) => - (options.filterChallenges ? !bet.challengeSlug : true) && - (options.filterRedemptions ? !bet.isRedemption : true) + (filterChallenges ? !bet.challengeSlug : true) && + (filterRedemptions ? !bet.isRedemption : true) ) ) else setBets(bets) }) - }, [contractId, options]) + }, [contractId, filterChallenges, filterRedemptions]) return bets } From 49541d3eec310a168d894e4fd26736b47f30f26c Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 9 Aug 2022 10:08:14 -0700 Subject: [PATCH 455/519] Stop interpolating on portfolio value graph --- web/components/portfolio/portfolio-value-graph.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index 50a6b59a..d1dae0bd 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -61,7 +61,8 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { min: Math.min(...points.map((p) => p.y)), }} gridYValues={numYTickValues} - curve="monotoneX" + curve="stepAfter" + enablePoints={false} colors={{ datum: 'color' }} axisBottom={{ tickValues: numXTickValues, From 914fc476ce3e00a06af3d387dc18e7e8a704c3ad Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Tue, 9 Aug 2022 10:17:44 -0700 Subject: [PATCH 456/519] Remove top/bottom margin from indented list items (#733) --- web/components/editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 2371bbf8..3bee8298 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -32,7 +32,7 @@ import { Row } from './layout/row' import { Spacer } from './layout/spacer' const proseClass = clsx( - 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', + 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'font-light prose-a:font-light prose-blockquote:font-light' ) From 1e3c5cb9369efa2163857c7a657d1896146f7c29 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 9 Aug 2022 12:27:52 -0500 Subject: [PATCH 457/519] add qr code to referrals --- web/pages/referrals.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx index b2666309..c30418cf 100644 --- a/web/pages/referrals.tsx +++ b/web/pages/referrals.tsx @@ -9,6 +9,7 @@ import { REFERRAL_AMOUNT } from 'common/user' import { CopyLinkButton } from 'web/components/copy-link-button' import { ENV_CONFIG } from 'common/envs/constants' import { InfoBox } from 'web/components/info-box' +import { QRCode } from 'web/components/qr-code' export const getServerSideProps = redirectIfLoggedOut('/') @@ -50,6 +51,8 @@ export default function ReferralsPage() { toastClassName={'-left-28 mt-1'} /> + <QRCode url={url} className="mt-4 self-center" /> + <InfoBox title="FYI" className="mt-4 max-w-md" From 5715b0e44afc70bcd62664468d6b0517ea0eb4bf Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 13:25:42 -0700 Subject: [PATCH 458/519] Random contract page fixups (#712) * Remove some divs and so on * Correctly align bet avatars and text in feed * Extract sidebar component on contract page --- web/components/avatar.tsx | 13 +++-- web/components/feed/feed-bets.tsx | 64 +++++++++++----------- web/pages/[username]/[contractSlug].tsx | 71 ++++++++++++++----------- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 0436d61c..19b6066e 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -47,14 +47,21 @@ export function Avatar(props: { ) } -export function EmptyAvatar(props: { size?: number; multi?: boolean }) { - const { size = 8, multi } = props +export function EmptyAvatar(props: { + className?: string + size?: number + multi?: boolean +}) { + const { className, size = 8, multi } = props const insize = size - 3 const Icon = multi ? UsersIcon : UserIcon return ( <div - className={`flex flex-shrink-0 h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`} + className={clsx( + `flex flex-shrink-0 h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`, + className + )} > <Icon className={`h-${insize} w-${insize} text-gray-500`} aria-hidden /> </div> diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 29645136..ffa53de3 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -36,38 +36,33 @@ export function FeedBet(props: { const isSelf = user?.id === userId return ( - <> - <Row className={'flex w-full gap-2 pt-3'}> - {isSelf ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={user.avatarUrl} - username={user.username} - /> - ) : bettor ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={bettor.avatarUrl} - username={bettor.username} - /> - ) : ( - <div className="relative px-1"> - <EmptyAvatar /> - </div> - )} - <div className={'min-w-0 flex-1 py-1.5'}> - <BetStatusText - bet={bet} - contract={contract} - isSelf={isSelf} - bettor={bettor} - hideOutcome={hideOutcome} - /> - </div> - </Row> - </> + <Row className={'flex w-full items-center gap-2 pt-3'}> + {isSelf ? ( + <Avatar + className={clsx(smallAvatar && 'ml-1')} + size={smallAvatar ? 'sm' : undefined} + avatarUrl={user.avatarUrl} + username={user.username} + /> + ) : bettor ? ( + <Avatar + className={clsx(smallAvatar && 'ml-1')} + size={smallAvatar ? 'sm' : undefined} + avatarUrl={bettor.avatarUrl} + username={bettor.username} + /> + ) : ( + <EmptyAvatar className="mx-1" /> + )} + <BetStatusText + bet={bet} + contract={contract} + isSelf={isSelf} + bettor={bettor} + hideOutcome={hideOutcome} + className="flex-1" + /> + </Row> ) } @@ -77,8 +72,9 @@ export function BetStatusText(props: { isSelf: boolean bettor?: User hideOutcome?: boolean + className?: string }) { - const { bet, contract, bettor, isSelf, hideOutcome } = props + const { bet, contract, bettor, isSelf, hideOutcome, className } = props const { outcomeType } = contract const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isFreeResponse = outcomeType === 'FREE_RESPONSE' @@ -123,7 +119,7 @@ export function BetStatusText(props: { : formatPercent(bet.limitProb ?? bet.probAfter) return ( - <div className="text-sm text-gray-500"> + <div className={clsx('text-sm text-gray-500', className)}> {bettor ? ( <UserLink name={bettor.name} username={bettor.username} /> ) : ( diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 5866f899..8d12e9c0 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -106,6 +106,43 @@ export default function ContractPage(props: { return <ContractPageContent {...{ ...props, contract }} /> } +export function ContractPageSidebar(props: { + user: User | null | undefined + contract: Contract +}) { + const { contract, user } = props + const { creatorId, isResolved, outcomeType } = contract + + const isCreator = user?.id === creatorId + const isBinary = outcomeType === 'BINARY' + const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const isNumeric = outcomeType === 'NUMERIC' + const allowTrade = tradingAllowed(contract) + const allowResolve = !isResolved && isCreator && !!user + const hasSidePanel = + (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) + + return hasSidePanel ? ( + <Col className="gap-4"> + {allowTrade && + (isNumeric ? ( + <NumericBetPanel className="hidden xl:flex" contract={contract} /> + ) : ( + <BetPanel + className="hidden xl:flex" + contract={contract as CPMMBinaryContract} + /> + ))} + {allowResolve && + (isNumeric || isPseudoNumeric ? ( + <NumericResolutionPanel creator={user} contract={contract} /> + ) : ( + <ResolutionPanel creator={user} contract={contract} /> + ))} + </Col> + ) : null +} + export function ContractPageContent( props: Parameters<typeof ContractPage>[0] & { contract: Contract } ) { @@ -142,16 +179,9 @@ export function ContractPageContent( setShowConfetti(shouldSeeConfetti) }, [contract, user]) - const { creatorId, isResolved, question, outcomeType } = contract + const { isResolved, question, outcomeType } = contract - const isCreator = user?.id === creatorId - const isBinary = outcomeType === 'BINARY' - const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const isNumeric = outcomeType === 'NUMERIC' const allowTrade = tradingAllowed(contract) - const allowResolve = !isResolved && isCreator && !!user - const hasSidePanel = - (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) const ogCardProps = getOpenGraphProps(contract) @@ -160,26 +190,7 @@ export function ContractPageContent( contractId: contract.id, }) - const rightSidebar = hasSidePanel ? ( - <Col className="gap-4"> - {allowTrade && - (isNumeric ? ( - <NumericBetPanel className="hidden xl:flex" contract={contract} /> - ) : ( - <BetPanel - className="hidden xl:flex" - contract={contract as CPMMBinaryContract} - /> - ))} - {allowResolve && - (isNumeric || isPseudoNumeric ? ( - <NumericResolutionPanel creator={user} contract={contract} /> - ) : ( - <ResolutionPanel creator={user} contract={contract} /> - ))} - </Col> - ) : null - + const rightSidebar = <ContractPageSidebar user={user} contract={contract} /> return ( <Page rightSidebar={rightSidebar}> {showConfetti && ( @@ -216,7 +227,7 @@ export function ContractPageContent( bets={bets.filter((b) => !b.challengeSlug)} /> - {isNumeric && ( + {outcomeType === 'NUMERIC' && ( <AlertBox title="Warning" text="Distributional numeric markets were introduced as an experimental feature and are now deprecated." @@ -232,7 +243,7 @@ export function ContractPageContent( </> )} - {isNumeric && allowTrade && ( + {outcomeType === 'NUMERIC' && allowTrade && ( <NumericBetPanel className="xl:hidden" contract={contract} /> )} From 847d3d0f2770e0da440e23848d5a4c9c3d6e921a Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 15:28:27 -0700 Subject: [PATCH 459/519] Fix efficiency problems with visibility checking code (#730) * Fix problems with visibility checking code * Tear out old contract tracking stuff per James * Use `useEvent` in VisibilityObserver per James suggestion --- web/components/contract-search.tsx | 1 - web/components/contract/contracts-grid.tsx | 30 +++++++------- web/components/feed/feed-items.tsx | 8 +--- web/components/landing-page-panel.tsx | 6 +-- web/components/visibility-observer.tsx | 24 +++++++++++ web/hooks/use-is-visible.ts | 28 ------------- web/hooks/use-seen-contracts.ts | 47 ---------------------- web/pages/contract-search-firestore.tsx | 7 +--- 8 files changed, 43 insertions(+), 108 deletions(-) create mode 100644 web/components/visibility-observer.tsx delete mode 100644 web/hooks/use-is-visible.ts delete mode 100644 web/hooks/use-seen-contracts.ts diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 265b25c6..8bc1341f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -345,7 +345,6 @@ export function ContractSearch(props: { <ContractsGrid contracts={hitsByPage[0] === undefined ? undefined : contracts} loadMore={loadMore} - hasMore={true} showTime={showTime} onContractClick={onContractClick} overrideGridClassName={overrideGridClassName} diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 31a564d3..77269ea3 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -5,10 +5,10 @@ import { SiteLink } from '../site-link' import { ContractCard } from './contract-card' import { ShowTime } from './contract-details' import { ContractSearch } from '../contract-search' -import { useIsVisible } from 'web/hooks/use-is-visible' -import { useEffect, useState } from 'react' +import { useCallback } from 'react' import clsx from 'clsx' import { LoadingIndicator } from '../loading-indicator' +import { VisibilityObserver } from '../visibility-observer' export type ContractHighlightOptions = { contractIds?: string[] @@ -17,8 +17,7 @@ export type ContractHighlightOptions = { export function ContractsGrid(props: { contracts: Contract[] | undefined - loadMore: () => void - hasMore: boolean + loadMore?: () => void showTime?: ShowTime onContractClick?: (contract: Contract) => void overrideGridClassName?: string @@ -31,7 +30,6 @@ export function ContractsGrid(props: { const { contracts, showTime, - hasMore, loadMore, onContractClick, overrideGridClassName, @@ -39,16 +37,15 @@ export function ContractsGrid(props: { highlightOptions, } = props const { hideQuickBet, hideGroupLink } = cardHideOptions || {} - const { contractIds, highlightClassName } = highlightOptions || {} - const [elem, setElem] = useState<HTMLElement | null>(null) - const isBottomVisible = useIsVisible(elem) - - useEffect(() => { - if (isBottomVisible && hasMore) { - loadMore() - } - }, [isBottomVisible, hasMore, loadMore]) + const onVisibilityUpdated = useCallback( + (visible) => { + if (visible && loadMore) { + loadMore() + } + }, + [loadMore] + ) if (contracts === undefined) { return <LoadingIndicator /> @@ -92,7 +89,10 @@ export function ContractsGrid(props: { /> ))} </ul> - <div ref={setElem} className="relative -top-96 h-1" /> + <VisibilityObserver + onVisibilityUpdated={onVisibilityUpdated} + className="relative -top-96 h-1" + /> </Col> ) } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index b1cd765c..d60fb8da 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -1,5 +1,5 @@ // From https://tailwindui.com/components/application-ui/lists/feeds -import React, { useState } from 'react' +import React from 'react' import { BanIcon, CheckIcon, @@ -22,7 +22,6 @@ import { UserLink } from '../user-page' import BetRow from '../bet-row' import { Avatar } from '../avatar' import { ActivityItem } from './activity-items' -import { useSaveSeenContract } from 'web/hooks/use-seen-contracts' import { useUser } from 'web/hooks/use-user' import { trackClick } from 'web/lib/firebase/tracking' import { DAY_MS } from 'common/util/time' @@ -50,11 +49,8 @@ export function FeedItems(props: { const { contract, items, className, betRowClassName, user } = props const { outcomeType } = contract - const [elem, setElem] = useState<HTMLElement | null>(null) - useSaveSeenContract(elem, contract) - return ( - <div className={clsx('flow-root', className)} ref={setElem}> + <div className={clsx('flow-root', className)}> <div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}> {items.map((item, activityItemIdx) => ( <div key={item.id} className={'relative pb-4'}> diff --git a/web/components/landing-page-panel.tsx b/web/components/landing-page-panel.tsx index 4b436442..2e3d85e2 100644 --- a/web/components/landing-page-panel.tsx +++ b/web/components/landing-page-panel.tsx @@ -59,11 +59,7 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) { <SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> Trending markets </Row> - <ContractsGrid - contracts={hotContracts?.slice(0, 10) || []} - loadMore={() => {}} - hasMore={false} - /> + <ContractsGrid contracts={hotContracts?.slice(0, 10) || []} /> </> ) } diff --git a/web/components/visibility-observer.tsx b/web/components/visibility-observer.tsx new file mode 100644 index 00000000..9af410c7 --- /dev/null +++ b/web/components/visibility-observer.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' +import { useEvent } from '../hooks/use-event' + +export function VisibilityObserver(props: { + className?: string + onVisibilityUpdated: (visible: boolean) => void +}) { + const { className } = props + const [elem, setElem] = useState<HTMLElement | null>(null) + const onVisibilityUpdated = useEvent(props.onVisibilityUpdated) + + useEffect(() => { + const hasIOSupport = !!window.IntersectionObserver + if (!hasIOSupport || !elem) return + + const observer = new IntersectionObserver(([entry]) => { + onVisibilityUpdated(entry.isIntersecting) + }, {}) + observer.observe(elem) + return () => observer.disconnect() + }, [elem, onVisibilityUpdated]) + + return <div ref={setElem} className={className}></div> +} diff --git a/web/hooks/use-is-visible.ts b/web/hooks/use-is-visible.ts deleted file mode 100644 index a3e2c1fa..00000000 --- a/web/hooks/use-is-visible.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from 'react' - -export function useIsVisible(element: HTMLElement | null) { - return !!useIntersectionObserver(element)?.isIntersecting -} - -function useIntersectionObserver( - elem: HTMLElement | null -): IntersectionObserverEntry | undefined { - const [entry, setEntry] = useState<IntersectionObserverEntry>() - - const updateEntry = ([entry]: IntersectionObserverEntry[]): void => { - setEntry(entry) - } - - useEffect(() => { - const hasIOSupport = !!window.IntersectionObserver - - if (!hasIOSupport || !elem) return - - const observer = new IntersectionObserver(updateEntry, {}) - observer.observe(elem) - - return () => observer.disconnect() - }, [elem]) - - return entry -} diff --git a/web/hooks/use-seen-contracts.ts b/web/hooks/use-seen-contracts.ts deleted file mode 100644 index d21ca84c..00000000 --- a/web/hooks/use-seen-contracts.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { mapValues } from 'lodash' -import { useEffect, useState } from 'react' -import { Contract } from 'common/contract' -import { trackView } from 'web/lib/firebase/tracking' -import { useIsVisible } from './use-is-visible' -import { useUser } from './use-user' - -export const useSeenContracts = () => { - const [seenContracts, setSeenContracts] = useState<{ - [contractId: string]: number - }>({}) - - useEffect(() => { - setSeenContracts(getSeenContracts()) - }, []) - - return seenContracts -} - -export const useSaveSeenContract = ( - elem: HTMLElement | null, - contract: Contract -) => { - const isVisible = useIsVisible(elem) - const user = useUser() - - useEffect(() => { - if (isVisible && user) { - const newSeenContracts = { - ...getSeenContracts(), - [contract.id]: Date.now(), - } - localStorage.setItem(key, JSON.stringify(newSeenContracts)) - - trackView(user.id, contract.id) - } - }, [isVisible, user, contract]) -} - -const key = 'feed-seen-contracts' - -const getSeenContracts = () => { - return mapValues( - JSON.parse(localStorage.getItem(key) ?? '{}'), - (time) => +time - ) -} diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index e2ca308c..fb05cf3a 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -110,12 +110,7 @@ export default function ContractSearchFirestore(props: { <option value="close-date">Closing soon</option> </select> </div> - <ContractsGrid - contracts={matches} - loadMore={() => {}} - hasMore={false} - showTime={showTime} - /> + <ContractsGrid contracts={matches} showTime={showTime} /> </div> ) } From c07daafb8dc0b36484c0cfc815d517d757845415 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 15:28:52 -0700 Subject: [PATCH 460/519] Make homepage load user via SSR, pass it to contract stuff (#729) --- web/components/contract-search.tsx | 5 +++-- web/components/contract/contracts-grid.tsx | 8 ++++++-- web/components/user-page.tsx | 4 +++- web/pages/[username]/[contractSlug].tsx | 12 +++++++----- web/pages/group/[...slugs]/index.tsx | 2 ++ web/pages/home.tsx | 12 ++++++++++-- web/pages/markets.tsx | 4 +++- web/pages/tag/[tag].tsx | 3 +++ 8 files changed, 37 insertions(+), 13 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 8bc1341f..f3bdbd6a 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -2,6 +2,7 @@ import algoliasearch from 'algoliasearch/lite' import { Contract } from 'common/contract' +import { User } from 'common/user' import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, @@ -11,7 +12,6 @@ import { Row } from './layout/row' import { useEffect, useMemo, useState } from 'react' import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' -import { useUser } from 'web/hooks/use-user' import { useFollows } from 'web/hooks/use-follows' import { track, trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' @@ -45,6 +45,7 @@ export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' export function ContractSearch(props: { + user: User | null | undefined querySortOptions?: { defaultSort: Sort defaultFilter?: filter @@ -67,6 +68,7 @@ export function ContractSearch(props: { } }) { const { + user, querySortOptions, additionalFilter, onContractClick, @@ -77,7 +79,6 @@ export function ContractSearch(props: { highlightOptions, } = props - const user = useUser() const memberGroups = (useMemberGroups(user?.id) ?? []).filter( (group) => !NEW_USER_GROUP_SLUGS.includes(group.slug) ) diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 77269ea3..f62c3c85 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -97,11 +97,15 @@ export function ContractsGrid(props: { ) } -export function CreatorContractsList(props: { creator: User }) { - const { creator } = props +export function CreatorContractsList(props: { + user: User | null | undefined + creator: User +}) { + const { user, creator } = props return ( <ContractSearch + user={user} querySortOptions={{ defaultSort: 'newest', defaultFilter: 'all', diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index fb349149..cd220cab 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -300,7 +300,9 @@ export function UserPage(props: { user: User; currentUser?: User }) { tabs={[ { title: 'Markets', - content: <CreatorContractsList creator={user} />, + content: ( + <CreatorContractsList user={currentUser} creator={user} /> + ), tabIcon: ( <span className="px-0.5 font-bold"> {usersContracts.length} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 8d12e9c0..c35f5d98 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -92,6 +92,7 @@ export default function ContractPage(props: { slug: '', } + const user = useUser() const inIframe = useIsIframe() if (inIframe) { return <ContractEmbedPage {...props} /> @@ -103,7 +104,7 @@ export default function ContractPage(props: { return <Custom404 /> } - return <ContractPageContent {...{ ...props, contract }} /> + return <ContractPageContent {...{ ...props, contract, user }} /> } export function ContractPageSidebar(props: { @@ -144,9 +145,12 @@ export function ContractPageSidebar(props: { } export function ContractPageContent( - props: Parameters<typeof ContractPage>[0] & { contract: Contract } + props: Parameters<typeof ContractPage>[0] & { + contract: Contract + user?: User | null + } ) { - const { backToHome, comments } = props + const { backToHome, comments, user } = props const contract = useContractWithPreload(props.contract) ?? props.contract @@ -164,8 +168,6 @@ export function ContractPageContent( const tips = useTipTxns({ contractId: contract.id }) - const user = useUser() - const { width, height } = useWindowSize() const [showConfetti, setShowConfetti] = useState(false) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index b96d6436..8e7ec19d 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -201,6 +201,7 @@ export default function GroupPage(props: { const questionsTab = ( <ContractSearch + user={user} querySortOptions={{ shouldLoadFromStorage: true, defaultSort: getSavedSort() ?? 'newest', @@ -614,6 +615,7 @@ function AddContractButton(props: { group: Group; user: User }) { <div className={'overflow-y-scroll sm:px-8'}> <ContractSearch + user={user} hideOrderSelector={true} onContractClick={addContractToCurrentGroup} overrideGridClassName={ diff --git a/web/pages/home.tsx b/web/pages/home.tsx index ab915ae3..10671c15 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -7,16 +7,22 @@ import { Col } from 'web/components/layout/col' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search' import { Contract } from 'common/contract' +import { User } from 'common/user' import { ContractPageContent } from './[username]/[contractSlug]' import { getContractFromSlug } from 'web/lib/firebase/contracts' +import { getUser } from 'web/lib/firebase/users' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { useSaveReferral } from 'web/hooks/use-save-referral' -export const getServerSideProps = redirectIfLoggedOut('/') +export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { + const user = await getUser(creds.user.uid) + return { props: { user } } +}) -const Home = () => { +const Home = (props: { user: User }) => { + const { user } = props const [contract, setContract] = useContractPage() const router = useRouter() @@ -29,6 +35,7 @@ const Home = () => { <Page suspend={!!contract}> <Col className="mx-auto w-full p-2"> <ContractSearch + user={user} querySortOptions={{ shouldLoadFromStorage: true, defaultSort: getSavedSort() ?? DEFAULT_SORT, @@ -56,6 +63,7 @@ const Home = () => { {contract && ( <ContractPageContent contract={contract} + user={user} username={contract.creatorUsername} slug={contract.slug} bets={[]} diff --git a/web/pages/markets.tsx b/web/pages/markets.tsx index 2d3346c1..c42364d5 100644 --- a/web/pages/markets.tsx +++ b/web/pages/markets.tsx @@ -1,9 +1,11 @@ +import { useUser } from 'web/hooks/use-user' import { ContractSearch } from '../components/contract-search' import { Page } from '../components/page' import { SEO } from '../components/SEO' // TODO: Rename endpoint to "Explore" export default function Markets() { + const user = useUser() return ( <Page> <SEO @@ -11,7 +13,7 @@ export default function Markets() { description="Discover what's new, trending, or soon-to-close. Or search thousands of prediction markets." url="/markets" /> - <ContractSearch /> + <ContractSearch user={user} /> </Page> ) } diff --git a/web/pages/tag/[tag].tsx b/web/pages/tag/[tag].tsx index 476afecf..c1dce29e 100644 --- a/web/pages/tag/[tag].tsx +++ b/web/pages/tag/[tag].tsx @@ -1,10 +1,12 @@ import { useRouter } from 'next/router' +import { useUser } from 'web/hooks/use-user' import { ContractSearch } from '../../components/contract-search' import { Page } from '../../components/page' import { Title } from '../../components/title' export default function TagPage() { const router = useRouter() + const user = useUser() const { tag } = router.query as { tag: string } if (!router.isReady) return <div /> @@ -12,6 +14,7 @@ export default function TagPage() { <Page> <Title text={`#${tag}`} /> <ContractSearch + user={user} querySortOptions={{ defaultSort: 'newest', defaultFilter: 'all', From 0b9ca6b7ee15a58adab8a1cac7967f2ee05d6eda Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Tue, 9 Aug 2022 19:04:55 -0700 Subject: [PATCH 461/519] Editor improvements (#735) * Allow focus on all parts of editor * Fix background and text colors * Restrict height of image in comment * Remove "Type *markdown*" placeholder it's a little misleading (can't do markdown links) and messes with focus to be replaced with a highlight menu in the future --- web/components/comments-list.tsx | 2 +- web/components/editor.tsx | 64 ++++++++++++++++----------- web/components/feed/feed-comments.tsx | 6 +-- web/components/groups/group-chat.tsx | 2 +- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index 2a467f6d..de4ea67f 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -65,7 +65,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) { />{' '} <RelativeTimestamp time={createdTime} /> </p> - <Content content={content || text} /> + <Content content={content || text} smallImage /> </div> </Row> ) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 3bee8298..cef1aa36 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -3,7 +3,6 @@ import Placeholder from '@tiptap/extension-placeholder' import { useEditor, EditorContent, - FloatingMenu, JSONContent, Content, Editor, @@ -11,13 +10,11 @@ import { import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' -import { Mention } from '@tiptap/extension-mention' import clsx from 'clsx' import { useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' -import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import { useUsers } from 'web/hooks/use-users' @@ -31,6 +28,18 @@ import { Button } from './button' import { Row } from './layout/row' import { Spacer } from './layout/spacer' +const DisplayImage = Image.configure({ + HTMLAttributes: { + class: 'max-h-60', + }, +}) + +const DisplayLink = Link.configure({ + HTMLAttributes: { + class: clsx('no-underline !text-indigo-700', linkClass), + }, +}) + const proseClass = clsx( 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'font-light prose-a:font-light prose-blockquote:font-light' @@ -64,15 +73,11 @@ export function useTextEditor(props: { Placeholder.configure({ placeholder, emptyEditorClass: - 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', + 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text', }), CharacterCount.configure({ limit: max }), - Image, - Link.configure({ - HTMLAttributes: { - class: clsx('no-underline !text-indigo-700', linkClass), - }, - }), + simple ? DisplayImage : Image, + DisplayLink, DisplayMention.configure({ suggestion: mentionSuggestion(users), }), @@ -132,15 +137,7 @@ export function TextEditor(props: { <> {/* hide placeholder when focused */} <div className="relative w-full [&:focus-within_p.is-empty]:before:content-none"> - {editor && ( - <FloatingMenu - editor={editor} - className={clsx(proseClass, '-ml-2 mr-2 w-full text-slate-300 ')} - > - Type <em>*markdown*</em> - </FloatingMenu> - )} - <div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> + <div className="rounded-lg border border-gray-300 bg-white shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> <EditorContent editor={editor} /> {/* Toolbar, with buttons for images and embeds */} <div className="flex h-9 items-center gap-5 pl-4 pr-1"> @@ -168,7 +165,14 @@ export function TextEditor(props: { <span className="sr-only">Embed an iframe</span> </button> </div> - <div className="ml-auto" /> + {/* Spacer that also focuses editor on click */} + <div + className="grow cursor-text self-stretch" + onMouseDown={() => + editor?.chain().focus('end').createParagraphNear().run() + } + aria-hidden + /> {children} </div> </div> @@ -258,14 +262,19 @@ const useUploadMutation = (editor: Editor | null) => } ) -function RichContent(props: { content: JSONContent | string }) { - const { content } = props +function RichContent(props: { + content: JSONContent | string + smallImage?: boolean +}) { + const { content, smallImage } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, extensions: [ - // replace tiptap's Mention with ours, to add style and link - ...exhibitExts.filter((ex) => ex.name !== Mention.name), + StarterKit, + smallImage ? DisplayImage : Image, + DisplayLink, DisplayMention, + Iframe, ], content, editable: false, @@ -276,13 +285,16 @@ function RichContent(props: { content: JSONContent | string }) { } // backwards compatibility: we used to store content as strings -export function Content(props: { content: JSONContent | string }) { +export function Content(props: { + content: JSONContent | string + smallImage?: boolean +}) { const { content } = props return typeof content === 'string' ? ( <div className="whitespace-pre-line font-light leading-relaxed"> <Linkify text={content} /> </div> ) : ( - <RichContent content={content} /> + <RichContent {...props} /> ) } diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 8c84039e..d4ba98b6 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -254,7 +254,7 @@ export function FeedComment(props: { /> </div> <div className="mt-2 text-[15px] text-gray-700"> - <Content content={content || text} /> + <Content content={content || text} smallImage /> </div> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> @@ -394,8 +394,8 @@ export function CommentInput(props: { /> </div> <div className={'min-w-0 flex-1'}> - <div className="pl-0.5 text-sm text-gray-500"> - <div className={'mb-1'}> + <div className="pl-0.5 text-sm"> + <div className="mb-1 text-gray-500"> {mostRecentCommentableBet && ( <BetStatusText contract={contract} diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 2d25351a..d872c980 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -338,7 +338,7 @@ const GroupMessage = memo(function GroupMessage_(props: { </Row> <div className="mt-2 text-black"> {comments.map((comment) => ( - <Content content={comment.content || comment.text} /> + <Content content={comment.content || comment.text} smallImage /> ))} </div> <Row> From 63538ae925cd46057df5b0392cf4841442813396 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 9 Aug 2022 21:51:01 -0500 Subject: [PATCH 462/519] referral link on user page, manalinks, market share dialog; native sharer on mobile --- web/components/contract/share-modal.tsx | 44 ++++++++++++++++--------- web/components/share-icon-button.tsx | 4 +-- web/components/user-page.tsx | 6 ++-- web/pages/links.tsx | 9 +++-- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 017d3174..6651db13 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -14,7 +14,9 @@ import { Button } from '../button' import { copyToClipboard } from 'web/lib/util/copy' import { track } from 'web/lib/service/analytics' import { ENV_CONFIG } from 'common/envs/constants' -import { User } from 'common/user' +import { REFERRAL_AMOUNT, User } from 'common/user' +import { SiteLink } from '../site-link' +import { formatMoney } from 'common/util/format' export function ShareModal(props: { contract: Contract @@ -26,36 +28,50 @@ export function ShareModal(props: { const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" /> - const copyPayload = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ + const shareUrl = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ user?.username && contract.creatorUsername !== user?.username ? '?referrer=' + user?.username : '' }` return ( - <Modal open={isOpen} setOpen={setOpen}> + <Modal open={isOpen} setOpen={setOpen} size="md"> <Col className="gap-4 rounded bg-white p-4"> - <Title className="!mt-0 mb-2" text="Share this market" /> + <Title className="!mt-0 !mb-2" text="Share this market" /> + <p> + Earn{' '} + <SiteLink href="/referrals"> + {formatMoney(REFERRAL_AMOUNT)} referral bonus + </SiteLink>{' '} + if a new user signs up using the link! + </p> <Button size="2xl" color="gradient" className={'mb-2 flex max-w-xs self-center'} onClick={() => { - copyToClipboard(copyPayload) + if (window.navigator.share) { + window.navigator.share({ + url: shareUrl, + title: contract.question, + }) + } else { + copyToClipboard(shareUrl) + toast.success('Link copied!', { + icon: linkIcon, + }) + } track('copy share link') - toast.success('Link copied!', { - icon: linkIcon, - }) }} > - {linkIcon} Copy link + {!!window.navigator.share ? 'Share' : <>{linkIcon} Copy link</>} </Button> - <Row className="justify-start gap-4 self-center"> + <Row className="z-0 justify-start gap-4 self-center"> <TweetButton className="self-start" - tweetText={getTweetText(contract)} + tweetText={getTweetText(contract, shareUrl)} /> <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> <DuplicateContractButton contract={contract} /> @@ -65,13 +81,9 @@ export function ShareModal(props: { ) } -const getTweetText = (contract: Contract) => { +const getTweetText = (contract: Contract, url: string) => { const { question, resolution } = contract - const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' - const timeParam = `${Date.now()}`.substring(7) - const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` - return `${question}\n\n${url}${tweetDescription}` } diff --git a/web/components/share-icon-button.tsx b/web/components/share-icon-button.tsx index 4db192a9..da1fc570 100644 --- a/web/components/share-icon-button.tsx +++ b/web/components/share-icon-button.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { ShareIcon } from '@heroicons/react/outline' +import { LinkIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { copyToClipboard } from 'web/lib/util/copy' @@ -40,7 +40,7 @@ export function ShareIconButton(props: { setTimeout(() => setShowToast(false), 2000) }} > - <ShareIcon + <LinkIcon className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} aria-hidden="true" /> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index cd220cab..5415ed39 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -280,8 +280,10 @@ export function UserPage(props: { user: User; currentUser?: User }) { } > <span> - Refer a friend and earn {formatMoney(500)} when they sign up! You - have <ReferralsButton user={user} currentUser={currentUser} /> + <SiteLink href="/referrals"> + Refer a friend and earn {formatMoney(500)} when they sign up! + </SiteLink>{' '} + You have <ReferralsButton user={user} currentUser={currentUser} /> </span> <ShareIconButton copyPayload={`https://${ENV_CONFIG.domain}?referrer=${currentUser.username}`} diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 55939b19..258c782a 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -26,6 +26,7 @@ import { ManalinkCardFromView } from 'web/components/manalink-card' import { Pagination } from 'web/components/pagination' import { Manalink } from 'common/manalink' import { REFERRAL_AMOUNT } from 'common/user' +import { SiteLink } from 'web/components/site-link' const LINKS_PER_PAGE = 24 @@ -69,9 +70,11 @@ export default function LinkPage(props: { user: User }) { </Row> <p> You can use manalinks to send mana (M$) to other people, even if they - don't yet have a Manifold account. Manalinks are also eligible - for the referral bonus. Invite a new user to Manifold and get M$ - {REFERRAL_AMOUNT} if they sign up! + don't yet have a Manifold account.{' '} + <SiteLink href="/referrals"> + Eligible for {formatMoney(REFERRAL_AMOUNT)} referral bonus if a new + user signs up! + </SiteLink> </p> <Subtitle text="Your Manalinks" /> <ManalinksDisplay From 5f77a026aa1d4e30d6e5c8b2c49b8bdb0a8d7006 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Tue, 9 Aug 2022 21:59:40 -0500 Subject: [PATCH 463/519] fix modal --- web/components/contract/share-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 6651db13..c462e78b 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -65,7 +65,7 @@ export function ShareModal(props: { track('copy share link') }} > - {!!window.navigator.share ? 'Share' : <>{linkIcon} Copy link</>} + {linkIcon} Copy link </Button> <Row className="z-0 justify-start gap-4 self-center"> From 818c90a95ece1b40e7316603322ca11ef118450f Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 23:05:56 -0700 Subject: [PATCH 464/519] Refactor tipper (#734) * Clean up tipping components * Pass comment into tip callback --- web/components/tipper.tsx | 70 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 68ca5308..1c76c3e7 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -37,7 +37,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { // declare debounced function only on first render const [saveTip] = useState(() => - debounce(async (user: User, change: number) => { + debounce(async (user: User, comment: Comment, change: number) => { if (change === 0) { return } @@ -71,30 +71,24 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { // instant save on unrender useEffect(() => () => void saveTip.flush(), [saveTip]) - const changeTip = (tip: number) => { - setLocalTip(tip) - me && saveTip(me, tip - savedTip) + const addTip = (delta: number) => { + setLocalTip(localTip + delta) + me && saveTip(me, comment, localTip - savedTip + delta) } + const canDown = me && localTip > savedTip + const canUp = me && me.id !== comment.userId && me.balance >= localTip + 5 return ( <Row className="items-center gap-0.5"> - <DownTip - value={localTip} - onChange={changeTip} - disabled={!me || localTip <= savedTip} - /> + <DownTip onClick={canDown ? () => addTip(-5) : undefined} /> <span className="font-bold">{Math.floor(total)}</span> - <UpTip - value={localTip} - onChange={changeTip} - disabled={!me || me.id === comment.userId || me.balance < localTip + 5} - /> + <UpTip onClick={canUp ? () => addTip(+5) : undefined} value={localTip} /> {localTip === 0 ? ( '' ) : ( <span className={clsx( - 'font-semibold', + 'ml-1 font-semibold', localTip > 0 ? 'text-primary' : 'text-red-400' )} > @@ -105,21 +99,17 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { ) } -function DownTip(prop: { - value: number - onChange: (tip: number) => void - disabled?: boolean -}) { - const { onChange, value, disabled } = prop +function DownTip(props: { onClick?: () => void }) { + const { onClick } = props return ( <Tooltip - className="tooltip-bottom" - text={!disabled && `-${formatMoney(5)}`} + className="tooltip-bottom h-6 w-6" + text={onClick && `-${formatMoney(5)}`} > <button - className="flex h-max items-center hover:text-red-600 disabled:text-gray-300" - disabled={disabled} - onClick={() => onChange(value - 5)} + className="hover:text-red-600 disabled:text-gray-300" + disabled={!onClick} + onClick={onClick} > <ChevronLeftIcon className="h-6 w-6" /> </button> @@ -127,30 +117,20 @@ function DownTip(prop: { ) } -function UpTip(prop: { - value: number - onChange: (tip: number) => void - disabled?: boolean -}) { - const { onChange, value, disabled } = prop - +function UpTip(props: { onClick?: () => void; value: number }) { + const { onClick, value } = props + const IconKind = value >= 10 ? ChevronDoubleRightIcon : ChevronRightIcon return ( <Tooltip - className="tooltip-bottom" - text={!disabled && `Tip ${formatMoney(5)}`} + className="tooltip-bottom h-6 w-6" + text={onClick && `Tip ${formatMoney(5)}`} > <button - className="hover:text-primary flex h-max items-center disabled:text-gray-300" - disabled={disabled} - onClick={() => onChange(value + 5)} + className="hover:text-primary disabled:text-gray-300" + disabled={!onClick} + onClick={onClick} > - {value >= 10 ? ( - <ChevronDoubleRightIcon className="text-primary mx-1 h-6 w-6" /> - ) : value > 0 ? ( - <ChevronRightIcon className="text-primary h-6 w-6" /> - ) : ( - <ChevronRightIcon className="h-6 w-6" /> - )} + <IconKind className={clsx('h-6 w-6', value ? 'text-primary' : '')} /> </button> </Tooltip> ) From 521c479abfdd5c22a7b762bf7f6e852341668c56 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Tue, 9 Aug 2022 23:55:41 -0700 Subject: [PATCH 465/519] Fix an embarrassing bug in `getPrivateUser` --- web/lib/firebase/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 3096f00f..cf2415ab 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -54,7 +54,7 @@ export async function getUser(userId: string) { export async function getPrivateUser(userId: string) { /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - return (await getDoc(doc(users, userId))).data()! + return (await getDoc(doc(privateUsers, userId))).data()! } export async function getUserByUsername(username: string) { From 52a2a3d8421d9ac5128a54a5881b19bdd795be3d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 11:50:21 -0500 Subject: [PATCH 466/519] track search --- web/components/contract-search.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index f3bdbd6a..30be1f6e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -236,7 +236,7 @@ export function ContractSearch(props: { if (newFilter === filter) return setFilter(newFilter) setPage(0) - trackCallback('select search filter', { filter: newFilter }) + track('select search filter', { filter: newFilter }) } const selectSort = (newSort: Sort) => { @@ -244,7 +244,7 @@ export function ContractSearch(props: { setPage(0) setSort(newSort) - track('select sort', { sort: newSort }) + track('select search sort', { sort: newSort }) } if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { @@ -263,6 +263,7 @@ export function ContractSearch(props: { type="text" value={query} onChange={(e) => updateQuery(e.target.value)} + onBlur={trackCallback('search', { query })} placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} className="input input-bordered w-full" /> From 05c9d3513a1759f31f8a6df5645a41aa8381705c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:05:52 -0500 Subject: [PATCH 467/519] Don't reference window outside useEffect or click event. --- web/components/nav/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 713bc575..8879c052 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -329,7 +329,7 @@ function GroupsList(props: { const { height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) const remainingHeight = - (height ?? window.innerHeight) - (containerRef?.offsetTop ?? 0) + (height ?? 0) - (containerRef?.offsetTop ?? 0) const notifIsForThisItem = useMemo( () => (itemHref: string) => From 3d30a1adbc8287b1578a64fbeff2143fc6b25ccf Mon Sep 17 00:00:00 2001 From: jahooma <jahooma@users.noreply.github.com> Date: Wed, 10 Aug 2022 17:06:34 +0000 Subject: [PATCH 468/519] Auto-prettification --- web/components/nav/sidebar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 8879c052..d45fdf19 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -328,8 +328,7 @@ function GroupsList(props: { const { height } = useWindowSize() const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) - const remainingHeight = - (height ?? 0) - (containerRef?.offsetTop ?? 0) + const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) const notifIsForThisItem = useMemo( () => (itemHref: string) => From dc26db28642fb04d436148f62ba580c8f2807df6 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 12:17:33 -0500 Subject: [PATCH 469/519] add salem to sidebar; clean up code --- common/util/array.ts | 16 +++++++ web/components/nav/sidebar.tsx | 87 +++++++++++++++------------------- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/common/util/array.ts b/common/util/array.ts index d81edba1..2ad86843 100644 --- a/common/util/array.ts +++ b/common/util/array.ts @@ -1,3 +1,19 @@ export function filterDefined<T>(array: (T | null | undefined)[]) { return array.filter((item) => item !== null && item !== undefined) as T[] } + +export function buildArray<T>( + ...params: (T | T[] | false | undefined | null)[] +) { + const array: T[] = [] + + for (const el of params) { + if (Array.isArray(el)) { + array.push(...el) + } else if (el) { + array.push(el) + } + } + + return array +} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index d45fdf19..3a3c932f 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -30,6 +30,7 @@ import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { PrivateUser } from 'common/user' import { useWindowSize } from 'web/hooks/use-window-size' import { CHALLENGES_ENABLED } from 'common/challenge' +import { buildArray } from 'common/util/array' const logout = async () => { // log out, and then reload the page, in case SSR wants to boot them out @@ -61,42 +62,31 @@ function getMoreNavigation(user?: User | null) { } if (!user) { - if (CHALLENGES_ENABLED) - return [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Charity', href: '/charity' }, - { name: 'Blog', href: 'https://news.manifold.markets' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, - ] - else - return [ + return buildArray( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ { name: 'Charity', href: '/charity' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, { name: 'Blog', href: 'https://news.manifold.markets' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, { name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' }, ] + ) } - if (CHALLENGES_ENABLED) - return [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, - { - name: 'Sign out', - href: '#', - onClick: logout, - }, - ] - else - return [ + return buildArray( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ { name: 'Referrals', href: '/referrals' }, { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, { name: 'About', href: 'https://docs.manifold.markets/$how-to' }, { @@ -105,6 +95,7 @@ function getMoreNavigation(user?: User | null) { onClick: logout, }, ] + ) } const signedOutNavigation = [ @@ -141,29 +132,27 @@ const signedInMobileNavigation = [ ] function getMoreMobileNav() { - return [ - ...(IS_PRIVATE_MANIFOLD - ? [] - : CHALLENGES_ENABLED - ? [ - { name: 'Challenges', href: '/challenges' }, - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - ] - : [ - { name: 'Referrals', href: '/referrals' }, - { name: 'Charity', href: '/charity' }, - { name: 'Send M$', href: '/links' }, - { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, - ]), - { - name: 'Sign out', - href: '#', - onClick: logout, - }, - ] + const signOut = { + name: 'Sign out', + href: '#', + onClick: logout, + } + if (IS_PRIVATE_MANIFOLD) return [signOut] + + return buildArray<Item>( + CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, + [ + { name: 'Referrals', href: '/referrals' }, + { + name: 'Salem tournament', + href: 'https://salemcenter.manifold.markets/', + }, + { name: 'Charity', href: '/charity' }, + { name: 'Send M$', href: '/links' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ], + signOut + ) } export type Item = { From 4d953d58a1c755e44539a8fa0af325461b5da68f Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:27:59 -0500 Subject: [PATCH 470/519] Move group chat back into a tab --- web/pages/group/[...slugs]/index.tsx | 30 +++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 8e7ec19d..b9c8c277 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -16,7 +16,7 @@ import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' -import { usePrivateUser, useUser } from 'web/hooks/use-user' +import { useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' @@ -30,7 +30,6 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { CreateQuestionButton } from 'web/components/create-question-button' import React, { useState } from 'react' -import { GroupChatInBubble } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' import { getSavedSort } from 'web/hooks/use-sort-and-query-params' @@ -51,6 +50,7 @@ import { useSaveReferral } from 'web/hooks/use-save-referral' import { Button } from 'web/components/button' import { listAllCommentsOnGroup } from 'web/lib/firebase/comments' import { Comment } from 'common/comment' +import { GroupChat } from 'web/components/groups/group-chat' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -157,16 +157,12 @@ export default function GroupPage(props: { const messages = useCommentsOnGroup(group?.id) ?? props.messages const user = useUser() - const privateUser = usePrivateUser(user?.id) useSaveReferral(user, { defaultReferrerUsername: creator.username, groupId: group?.id, }) - const chatDisabled = !group || group.chatDisabled - const showChatBubble = !chatDisabled - if (group === null || !groupSubpages.includes(page) || slugs[2]) { return <Custom404 /> } @@ -211,12 +207,21 @@ export default function GroupPage(props: { /> ) + const chatTab = ( + <GroupChat messages={messages} group={group} user={user} tips={tips} /> + ) + const tabs = [ { title: 'Markets', content: questionsTab, href: groupPath(group.slug, 'markets'), }, + { + title: 'Chat', + content: chatTab, + href: groupPath(group.slug, 'chat'), + }, { title: 'Leaderboards', content: leaderboard, @@ -229,7 +234,9 @@ export default function GroupPage(props: { }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) + const tabIndex = tabs + .map((t) => t.title.toLowerCase()) + .indexOf(page ?? 'markets') return ( <Page> @@ -265,15 +272,6 @@ export default function GroupPage(props: { defaultIndex={tabIndex > 0 ? tabIndex : 0} tabs={tabs} /> - {showChatBubble && ( - <GroupChatInBubble - group={group} - user={user} - privateUser={privateUser} - tips={tips} - messages={messages} - /> - )} </Page> ) } From e591de8b29530396bd2a62b3873f7a3aa2dc8b15 Mon Sep 17 00:00:00 2001 From: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Date: Thu, 11 Aug 2022 02:31:28 +0900 Subject: [PATCH 471/519] Increase description max length (#739) --- common/contract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/contract.ts b/common/contract.ts index 8bdab6fe..c414a332 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -139,7 +139,7 @@ export const OUTCOME_TYPES = [ ] as const export const MAX_QUESTION_LENGTH = 480 -export const MAX_DESCRIPTION_LENGTH = 10000 +export const MAX_DESCRIPTION_LENGTH = 16000 export const MAX_TAG_LENGTH = 60 export const CPMM_MIN_POOL_QTY = 0.01 From 35df201e2e7548b2c57280651c111fb91f0543e1 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 10 Aug 2022 12:32:22 -0500 Subject: [PATCH 472/519] prob bar for multiple choice --- web/components/contract/quick-bet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 09a5d4bc..482aea47 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -319,7 +319,7 @@ function getProb(contract: Contract) { ? getBinaryProb(contract) : outcomeType === 'PSEUDO_NUMERIC' ? getProbability(contract) - : outcomeType === 'FREE_RESPONSE' + : outcomeType === 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE' ? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '') : outcomeType === 'NUMERIC' ? getNumericScale(contract) From 654790315c82f1f1c82bd6262199310dc14f291a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:33:21 -0500 Subject: [PATCH 473/519] Fix missing key console error --- web/components/groups/group-chat.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index d872c980..62d327f2 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -338,7 +338,11 @@ const GroupMessage = memo(function GroupMessage_(props: { </Row> <div className="mt-2 text-black"> {comments.map((comment) => ( - <Content content={comment.content || comment.text} smallImage /> + <Content + key={comment.id} + content={comment.content || comment.text} + smallImage + /> ))} </div> <Row> From d7b021b79fb911ccdc0cb5cf824924633f2ec3f5 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 12:37:51 -0500 Subject: [PATCH 474/519] Clear entered limit probs on submit limit order --- web/components/bet-panel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index c0f7ff94..9c572e0c 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -484,6 +484,8 @@ function LimitOrderPanel(props: { setIsSubmitting(false) setWasSubmitted(true) setBetAmount(undefined) + setLowLimitProb(undefined) + setHighLimitProb(undefined) if (onBuySuccess) onBuySuccess() }) From b5b77be188bdbfa8ba3ef524b38ec9efc985353d Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 10 Aug 2022 11:03:39 -0700 Subject: [PATCH 475/519] Accept URLs in the iframe editor TODO: Update placeholder text to mention this --- web/components/editor.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index cef1aa36..d52913b3 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -125,6 +125,13 @@ function isValidIframe(text: string) { return /^<iframe.*<\/iframe>$/.test(text) } +function isValidUrl(text: string) { + // Conjured by Codex, not sure if it's actually good + return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test( + text + ) +} + export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> @@ -191,8 +198,9 @@ function IframeModal(props: { setOpen: (open: boolean) => void }) { const { editor, open, setOpen } = props - const [embedCode, setEmbedCode] = useState('') - const valid = isValidIframe(embedCode) + const [input, setInput] = useState('') + const valid = isValidIframe(input) || isValidUrl(input) + const embedCode = isValidIframe(input) ? input : `<iframe src="${input}" />` return ( <Modal open={open} setOpen={setOpen}> @@ -209,8 +217,8 @@ function IframeModal(props: { id="embed" className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder='e.g. <iframe src="..."></iframe>' - value={embedCode} - onChange={(e) => setEmbedCode(e.target.value)} + value={input} + onChange={(e) => setInput(e.target.value)} /> {/* Preview the embed if it's valid */} @@ -222,7 +230,7 @@ function IframeModal(props: { onClick={() => { if (editor && valid) { editor.chain().insertContent(embedCode).run() - setEmbedCode('') + setInput('') setOpen(false) } }} @@ -232,7 +240,7 @@ function IframeModal(props: { <Button color="gray" onClick={() => { - setEmbedCode('') + setInput('') setOpen(false) }} > From 8c537537a12c1300976b37a3afb3db1a27f527af Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Wed, 10 Aug 2022 11:03:55 -0700 Subject: [PATCH 476/519] Add cache headers to avatars (#737) * Set cache headers on newly uploaded avatars * Go fix up all the old avatars to have cache headers --- .../src/scripts/set-avatar-cache-headers.ts | 27 +++++++++++++++++++ web/lib/firebase/storage.ts | 6 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 functions/src/scripts/set-avatar-cache-headers.ts diff --git a/functions/src/scripts/set-avatar-cache-headers.ts b/functions/src/scripts/set-avatar-cache-headers.ts new file mode 100644 index 00000000..676ec62d --- /dev/null +++ b/functions/src/scripts/set-avatar-cache-headers.ts @@ -0,0 +1,27 @@ +import { initAdmin } from './script-init' +import { log } from '../utils' + +const app = initAdmin() +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 +const AVATAR_EXTENSION_RE = /\.(gif|tiff|jpe?g|png|webp)$/i + +const processAvatars = async () => { + const storage = app.storage() + const bucket = storage.bucket(`${app.options.projectId}.appspot.com`) + const [files] = await bucket.getFiles({ prefix: 'user-images' }) + log(`${files.length} avatar images to process.`) + for (const file of files) { + if (AVATAR_EXTENSION_RE.test(file.name)) { + log(`Updating metadata for ${file.name}.`) + await file.setMetadata({ + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + }) + } else { + log(`Skipping ${file.name} because it probably isn't an avatar.`) + } + } +} + +if (require.main === module) { + processAvatars().catch((e) => console.error(e)) +} diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index fcf4422d..3acfbae9 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -3,6 +3,8 @@ import imageCompression from 'browser-image-compression' import { nanoid } from 'nanoid' import { storage } from './init' +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 + export const uploadImage = async ( username: string, file: File, @@ -24,7 +26,9 @@ export const uploadImage = async ( }) } - const uploadTask = uploadBytesResumable(storageRef, file) + const uploadTask = uploadBytesResumable(storageRef, file, { + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + }) let resolvePromise: (url: string) => void let rejectPromise: (reason?: any) => void From 61ae481a035612ba1699f3e7d70c0069b1823ccc Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 10 Aug 2022 18:42:44 -0500 Subject: [PATCH 477/519] Document cancel bet --- docs/docs/api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/api.md b/docs/docs/api.md index 48564cb3..e4936418 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -528,6 +528,10 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application "contractId":"{...}"}' ``` +### `POST /v0/bet/cancel/[id]` + +Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable. + ### `POST /v0/market` Creates a new market on behalf of the authorized user. From 6e93f11a5949147876a13ff6f95dd7471f5ef9bc Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 11 Aug 2022 00:00:40 -0500 Subject: [PATCH 478/519] Fix bolded group chat not getting unbolded --- web/components/groups/group-chat.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 62d327f2..0f9e8955 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -21,6 +21,7 @@ import { Content, useTextEditor } from 'web/components/editor' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' +import { usePrivateUser } from 'web/hooks/use-user' export function GroupChat(props: { messages: Comment[] @@ -29,6 +30,9 @@ export function GroupChat(props: { tips: CommentTipMap }) { const { messages, user, group, tips } = props + + const privateUser = usePrivateUser(user?.id) + const { editor, upload } = useTextEditor({ simple: true, placeholder: 'Send a message', @@ -175,6 +179,15 @@ export function GroupChat(props: { </div> </div> )} + + {privateUser && ( + <GroupChatNotificationsIcon + group={group} + privateUser={privateUser} + shouldSetAsSeen={true} + hidden={true} + /> + )} </Col> ) } @@ -248,6 +261,7 @@ export function GroupChatInBubble(props: { group={group} privateUser={privateUser} shouldSetAsSeen={shouldShowChat} + hidden={false} /> )} </button> @@ -259,8 +273,9 @@ function GroupChatNotificationsIcon(props: { group: Group privateUser: PrivateUser shouldSetAsSeen: boolean + hidden: boolean }) { - const { privateUser, group, shouldSetAsSeen } = props + const { privateUser, group, shouldSetAsSeen, hidden } = props const preferredNotificationsForThisGroup = useUnseenPreferredNotifications( privateUser, { @@ -282,7 +297,9 @@ function GroupChatNotificationsIcon(props: { return ( <div className={ - preferredNotificationsForThisGroup.length > 0 && !shouldSetAsSeen + !hidden && + preferredNotificationsForThisGroup.length > 0 && + !shouldSetAsSeen ? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500' : 'hidden' } From 3f6ca6c8ed8fb649b55975c647b5b2d724f3650a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 11 Aug 2022 00:38:15 -0500 Subject: [PATCH 479/519] Make Manifold account able to resolve markets --- common/envs/constants.ts | 4 ++++ functions/src/resolve-market.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/envs/constants.ts b/common/envs/constants.ts index 7092d711..48f9bf63 100644 --- a/common/envs/constants.ts +++ b/common/envs/constants.ts @@ -25,6 +25,10 @@ export function isAdmin(email: string) { return ENV_CONFIG.adminEmails.includes(email) } +export function isManifoldId(userId: string) { + return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' +} + export const DOMAIN = ENV_CONFIG.domain export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index cc07d4be..7277f40b 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -18,7 +18,7 @@ import { groupPayoutsByUser, Payout, } from '../../common/payouts' -import { isAdmin } from '../../common/envs/constants' +import { isManifoldId } from '../../common/envs/constants' import { removeUndefinedProps } from '../../common/util/object' import { LiquidityProvision } from '../../common/liquidity-provision' import { APIError, newEndpoint, validate } from './api' @@ -82,7 +82,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => { req.body ) - if (creatorId !== auth.uid && !isAdmin(auth.uid)) + if (creatorId !== auth.uid && !isManifoldId(auth.uid)) throw new APIError(403, 'User is not creator of contract') if (contract.resolution) throw new APIError(400, 'Contract already resolved') From 99326eb65a1ef74288f1160857c1d4ef2294864d Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 11 Aug 2022 12:30:34 -0700 Subject: [PATCH 480/519] fix spacing of long group names on markets --- web/components/contract/contract-details.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 90b5f3d1..6fdd82bd 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -149,7 +149,7 @@ export function ContractDetails(props: { const groupInfo = ( <Row> <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> - <span className={'line-clamp-1'}> + <span className="truncate"> {groupToDisplay ? groupToDisplay.name : 'No group'} </span> </Row> From daa86fa330ccbf88fe3d664ff6ed00f41d80379b Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 12:53:42 -0700 Subject: [PATCH 481/519] Change tabs to keep all individual tab components in the DOM (#743) --- web/components/layout/tabs.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index a87c6607..3d72b13c 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -28,7 +28,6 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) { className, currentPageForAnalytics, } = props - const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case return ( <> <nav @@ -64,7 +63,11 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) { </a> ))} </nav> - {activeTab?.content} + {tabs.map((tab, i) => ( + <div key={i} className={i === activeIndex ? 'block' : 'hidden'}> + {tab.content} + </div> + ))} </> ) } From ad75ecdc8780c580681f10f992e1fe5d75aacc45 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 12:53:54 -0700 Subject: [PATCH 482/519] Move liquidity provision fetch down into ContractTabs (#741) --- web/components/contract/contract-tabs.tsx | 8 +++++--- web/pages/[username]/[contractSlug].tsx | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 5aee7899..eb455df0 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -8,18 +8,17 @@ import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' import { Col } from '../layout/col' import { CommentTipMap } from 'web/hooks/use-tip-txns' -import { LiquidityProvision } from 'common/liquidity-provision' import { useComments } from 'web/hooks/use-comments' +import { useLiquidity } from 'web/hooks/use-liquidity' export function ContractTabs(props: { contract: Contract user: User | null | undefined bets: Bet[] - liquidityProvisions: LiquidityProvision[] comments: Comment[] tips: CommentTipMap }) { - const { contract, user, bets, tips, liquidityProvisions } = props + const { contract, user, bets, tips } = props const { outcomeType } = contract const userBets = user && bets.filter((bet) => bet.userId === user.id) @@ -27,6 +26,9 @@ export function ContractTabs(props: { (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 ) + const liquidityProvisions = + useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? [] + // Load comments here, so the badge count will be correct const updatedComments = useComments(contract.id) const comments = updatedComments ?? props.comments diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index c35f5d98..e122db04 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -36,7 +36,6 @@ import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' -import { useLiquidity } from 'web/hooks/use-liquidity' import { useSaveReferral } from 'web/hooks/use-save-referral' import { getOpenGraphProps } from 'web/components/contract/contract-card-preview' import { User } from 'common/user' @@ -161,8 +160,7 @@ export function ContractPageContent( }) const bets = useBets(contract.id) ?? props.bets - const liquidityProvisions = - useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? [] + // Sort for now to see if bug is fixed. comments.sort((c1, c2) => c1.createdTime - c2.createdTime) @@ -267,7 +265,6 @@ export function ContractPageContent( <ContractTabs contract={contract} user={user} - liquidityProvisions={liquidityProvisions} bets={bets} tips={tips} comments={comments} From b9f347b7f43a4f7bfc8975c1eb2a1d99fac53afb Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 12:54:09 -0700 Subject: [PATCH 483/519] Use `UserFollowButton` instead of `FollowButton` in `UserPage` (#742) --- web/components/user-page.tsx | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 5415ed39..1adf9f2d 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -6,12 +6,7 @@ import { LinkIcon } from '@heroicons/react/solid' import { PencilIcon } from '@heroicons/react/outline' import Confetti from 'react-confetti' -import { - follow, - getPortfolioHistory, - unfollow, - User, -} from 'web/lib/firebase/users' +import { getPortfolioHistory, User } from 'web/lib/firebase/users' import { CreatorContractsList } from './contract/contracts-grid' import { SEO } from './SEO' import { Page } from './page' @@ -31,8 +26,7 @@ import { getContractFromId, listContracts } from 'web/lib/firebase/contracts' import { LoadingIndicator } from './loading-indicator' import { BetsList } from './bets-list' import { FollowersButton, FollowingButton } from './following-button' -import { useFollows } from 'web/hooks/use-follows' -import { FollowButton } from './follow-button' +import { UserFollowButton } from './follow-button' import { PortfolioMetrics } from 'common/user' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' @@ -120,19 +114,8 @@ export function UserPage(props: { user: User; currentUser?: User }) { } }, [userBets, usersComments]) - const yourFollows = useFollows(currentUser?.id) - const isFollowing = yourFollows?.includes(user.id) const profit = user.profitCached.allTime - const onFollow = () => { - if (!currentUser) return - follow(currentUser.id, user.id) - } - const onUnfollow = () => { - if (!currentUser) return - unfollow(currentUser.id, user.id) - } - return ( <Page key={user.id}> <SEO @@ -167,13 +150,7 @@ export function UserPage(props: { user: User; currentUser?: User }) { {/* Top right buttons (e.g. edit, follow) */} <div className="absolute right-0 top-0 mt-4 mr-4"> - {!isCurrentUser && ( - <FollowButton - isFollowing={isFollowing} - onFollow={onFollow} - onUnfollow={onUnfollow} - /> - )} + {!isCurrentUser && <UserFollowButton userId={user.id} />} {isCurrentUser && ( <SiteLink className="btn" href="/profile"> <PencilIcon className="h-5 w-5" />{' '} From 4e8b94a28c2d54157da26ffe658ba003395cfa2c Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 12:55:25 -0700 Subject: [PATCH 484/519] Componentize confetti to isolate re-renders due to window size (#740) * Componentize confetti to isolate re-renders due to window size * Clean up debug logging --- web/components/fullscreen-confetti.tsx | 7 +++++++ web/components/user-page.tsx | 11 ++--------- web/pages/[username]/[contractSlug].tsx | 12 ++---------- 3 files changed, 11 insertions(+), 19 deletions(-) create mode 100644 web/components/fullscreen-confetti.tsx diff --git a/web/components/fullscreen-confetti.tsx b/web/components/fullscreen-confetti.tsx new file mode 100644 index 00000000..390b3a9b --- /dev/null +++ b/web/components/fullscreen-confetti.tsx @@ -0,0 +1,7 @@ +import Confetti, { Props as ConfettiProps } from 'react-confetti' +import { useWindowSize } from 'web/hooks/use-window-size' + +export function FullscreenConfetti(props: ConfettiProps) { + const { width, height } = useWindowSize() + return <Confetti {...props} width={width} height={height} /> +} diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 1adf9f2d..322ddb96 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' import { PencilIcon } from '@heroicons/react/outline' -import Confetti from 'react-confetti' import { getPortfolioHistory, User } from 'web/lib/firebase/users' import { CreatorContractsList } from './contract/contracts-grid' @@ -19,11 +18,11 @@ import { Row } from './layout/row' import { genHash } from 'common/util/random' import { QueryUncontrolledTabs } from './layout/tabs' import { UserCommentsList } from './comments-list' -import { useWindowSize } from 'web/hooks/use-window-size' import { Comment, getUsersComments } from 'web/lib/firebase/comments' import { Contract } from 'common/contract' import { getContractFromId, listContracts } from 'web/lib/firebase/contracts' import { LoadingIndicator } from './loading-indicator' +import { FullscreenConfetti } from 'web/components/fullscreen-confetti' import { BetsList } from './bets-list' import { FollowersButton, FollowingButton } from './following-button' import { UserFollowButton } from './follow-button' @@ -82,7 +81,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { Dictionary<Contract> | undefined >() const [showConfetti, setShowConfetti] = useState(false) - const { width, height } = useWindowSize() useEffect(() => { const claimedMana = router.query['claimed-mana'] === 'yes' @@ -124,12 +122,7 @@ export function UserPage(props: { user: User; currentUser?: User }) { url={`/${user.username}`} /> {showConfetti && ( - <Confetti - width={width ? width : 500} - height={height ? height : 500} - recycle={false} - numberOfPieces={300} - /> + <FullscreenConfetti recycle={false} numberOfPieces={300} /> )} {/* Banner image up top, with an circle avatar overlaid */} <div diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index e122db04..94773b6d 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -25,8 +25,7 @@ import { Leaderboard } from 'web/components/leaderboard' import { resolvedPayout } from 'common/calculate' import { formatMoney } from 'common/util/format' import { ContractTabs } from 'web/components/contract/contract-tabs' -import { useWindowSize } from 'web/hooks/use-window-size' -import Confetti from 'react-confetti' +import { FullscreenConfetti } from 'web/components/fullscreen-confetti' import { NumericBetPanel } from 'web/components/numeric-bet-panel' import { NumericResolutionPanel } from 'web/components/numeric-resolution-panel' import { useIsIframe } from 'web/hooks/use-is-iframe' @@ -166,8 +165,6 @@ export function ContractPageContent( const tips = useTipTxns({ contractId: contract.id }) - const { width, height } = useWindowSize() - const [showConfetti, setShowConfetti] = useState(false) useEffect(() => { @@ -194,12 +191,7 @@ export function ContractPageContent( return ( <Page rightSidebar={rightSidebar}> {showConfetti && ( - <Confetti - width={width ? width : 500} - height={height ? height : 500} - recycle={false} - numberOfPieces={300} - /> + <FullscreenConfetti recycle={false} numberOfPieces={300} /> )} {ogCardProps && ( From dc95587cca13ae53013d58ed4fd0f7ff88d3f71f Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 11 Aug 2022 14:32:02 -0700 Subject: [PATCH 485/519] Add editor toolbar to choose and embed markets (#702) * Embed markets using the "add markets" template * Override dev domain * Improve market modal style - contract searchbar now sticky - entire card clickable to select (if quickbet is hidden) - adjust selected card styles * remove extra export * Hide pills * Fix browser redirect warning * Insert all markets instead of just one * fix type error * fixup "Insert all markets instead of just one" Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com> --- common/envs/dev.ts | 1 + web/components/contract-search.tsx | 180 +++++++++--------- web/components/contract/contract-card.tsx | 3 +- .../contract/contract-description.tsx | 7 +- web/components/contract/contract-details.tsx | 5 +- web/components/editor.tsx | 36 +++- web/components/editor/market-modal.tsx | 86 +++++++++ web/components/editor/utils.ts | 17 +- web/components/share-embed-button.tsx | 8 +- web/hooks/use-sort-and-query-params.tsx | 25 ++- web/pages/contract-search-firestore.tsx | 6 +- 11 files changed, 249 insertions(+), 125 deletions(-) create mode 100644 web/components/editor/market-modal.tsx diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 3c062472..719de36e 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -2,6 +2,7 @@ import { EnvConfig, PROD_CONFIG } from './prod' export const DEV_CONFIG: EnvConfig = { ...PROD_CONFIG, + domain: 'dev.manifold.markets', firebaseConfig: { apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', authDomain: 'dev-mantic-markets.firebaseapp.com', diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 30be1f6e..40a62923 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -3,14 +3,17 @@ import algoliasearch from 'algoliasearch/lite' import { Contract } from 'common/contract' import { User } from 'common/user' -import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' +import { + QuerySortOptions, + Sort, + useQueryAndSortParams, +} from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, ContractsGrid, } from './contract/contracts-grid' import { Row } from './layout/row' import { useEffect, useMemo, useState } from 'react' -import { Spacer } from './layout/spacer' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useFollows } from 'web/hooks/use-follows' import { track, trackCallback } from 'web/lib/service/analytics' @@ -21,6 +24,7 @@ import { PillButton } from './buttons/pill-button' import { range, sortBy } from 'lodash' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { Col } from './layout/col' +import clsx from 'clsx' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -45,12 +49,8 @@ export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' export function ContractSearch(props: { - user: User | null | undefined - querySortOptions?: { - defaultSort: Sort - defaultFilter?: filter - shouldLoadFromStorage?: boolean - } + user?: User | null + querySortOptions?: { defaultFilter?: filter } & QuerySortOptions additionalFilter?: { creatorId?: string tag?: string @@ -66,6 +66,7 @@ export function ContractSearch(props: { hideGroupLink?: boolean hideQuickBet?: boolean } + headerClassName?: string }) { const { user, @@ -77,6 +78,7 @@ export function ContractSearch(props: { showPlaceHolder, cardHideOptions, highlightOptions, + headerClassName, } = props const memberGroups = (useMemberGroups(user?.id) ?? []).filter( @@ -99,11 +101,8 @@ export function ContractSearch(props: { const follows = useFollows(user?.id) - const { shouldLoadFromStorage, defaultSort } = querySortOptions ?? {} - const { query, setQuery, sort, setSort } = useQueryAndSortParams({ - defaultSort, - shouldLoadFromStorage, - }) + const { query, setQuery, sort, setSort } = + useQueryAndSortParams(querySortOptions) const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' @@ -257,87 +256,90 @@ export function ContractSearch(props: { } return ( - <Col> - <Row className="gap-1 sm:gap-2"> - <input - type="text" - value={query} - onChange={(e) => updateQuery(e.target.value)} - onBlur={trackCallback('search', { query })} - placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} - className="input input-bordered w-full" - /> - {!query && ( - <select - className="select select-bordered" - value={filter} - onChange={(e) => selectFilter(e.target.value as filter)} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> + <Col className="h-full"> + <Col + className={clsx( + 'bg-base-200 sticky top-0 z-20 gap-3 pb-3', + headerClassName )} - {!hideOrderSelector && !query && ( - <select - className="select select-bordered" - value={sort} - onChange={(e) => selectSort(e.target.value as Sort)} - > - {sortOptions.map((option) => ( - <option key={option.value} value={option.value}> - {option.label} - </option> - ))} - </select> - )} - </Row> - - <Spacer h={3} /> - - {pillsEnabled && ( - <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> - <PillButton - key={'all'} - selected={pillFilter === undefined} - onSelect={selectPill(undefined)} - > - All - </PillButton> - <PillButton - key={'personal'} - selected={pillFilter === 'personal'} - onSelect={selectPill('personal')} - > - {user ? 'For you' : 'Featured'} - </PillButton> - - {user && ( - <PillButton - key={'your-bets'} - selected={pillFilter === 'your-bets'} - onSelect={selectPill('your-bets')} + > + <Row className="gap-1 sm:gap-2"> + <input + type="text" + value={query} + onChange={(e) => updateQuery(e.target.value)} + onBlur={trackCallback('search', { query })} + placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} + className="input input-bordered w-full" + /> + {!query && ( + <select + className="select select-bordered" + value={filter} + onChange={(e) => selectFilter(e.target.value as filter)} > - Your bets - </PillButton> + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + )} + {!hideOrderSelector && !query && ( + <select + className="select select-bordered" + value={sort} + onChange={(e) => selectSort(e.target.value as Sort)} + > + {sortOptions.map((option) => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </select> )} - - {pillGroups.map(({ name, slug }) => { - return ( - <PillButton - key={slug} - selected={pillFilter === slug} - onSelect={selectPill(slug)} - > - {name} - </PillButton> - ) - })} </Row> - )} - <Spacer h={3} /> + {pillsEnabled && ( + <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> + <PillButton + key={'all'} + selected={pillFilter === undefined} + onSelect={selectPill(undefined)} + > + All + </PillButton> + <PillButton + key={'personal'} + selected={pillFilter === 'personal'} + onSelect={selectPill('personal')} + > + {user ? 'For you' : 'Featured'} + </PillButton> + + {user && ( + <PillButton + key={'your-bets'} + selected={pillFilter === 'your-bets'} + onSelect={selectPill('your-bets')} + > + Your bets + </PillButton> + )} + + {pillGroups.map(({ name, slug }) => { + return ( + <PillButton + key={slug} + selected={pillFilter === slug} + onSelect={selectPill(slug)} + > + {name} + </PillButton> + ) + })} + </Row> + )} + </Col> {filter === 'personal' && (follows ?? []).length === 0 && diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 4ef90884..248c3863 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -76,7 +76,8 @@ export function ContractCard(props: { <Col className="relative flex-1 gap-3 pr-1"> <div className={clsx( - 'peer absolute -left-6 -top-4 -bottom-4 right-0 z-10' + 'peer absolute -left-6 -top-4 -bottom-4 z-10', + hideQuickBet ? '-right-20' : 'right-0' )} > {onClick ? ( diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index 4c9b77a2..9bf2114b 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -13,7 +13,7 @@ import { TextEditor, useTextEditor } from 'web/components/editor' import { Button } from '../button' import { Spacer } from '../layout/spacer' import { Editor, Content as ContentType } from '@tiptap/react' -import { appendToEditor } from '../editor/utils' +import { insertContent } from '../editor/utils' export function ContractDescription(props: { contract: Contract @@ -95,7 +95,8 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) { size="xs" onClick={() => { setEditing(true) - appendToEditor(editor, `<p>${editTimestamp()}</p>`) + editor?.commands.focus('end') + insertContent(editor, `<p>${editTimestamp()}</p>`) }} > Edit description @@ -127,7 +128,7 @@ function EditQuestion(props: { function joinContent(oldContent: ContentType, newContent: string) { const editor = new Editor({ content: oldContent, extensions: exhibitExts }) - appendToEditor(editor, newContent) + insertContent(editor, newContent) return editor.getJSON() } diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 6fdd82bd..081b035d 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -33,7 +33,7 @@ import { Col } from 'web/components/layout/col' import { ContractGroupsList } from 'web/components/groups/contract-groups-list' import { SiteLink } from 'web/components/site-link' import { groupPath } from 'web/lib/firebase/groups' -import { appendToEditor } from '../editor/utils' +import { insertContent } from '../editor/utils' export type ShowTime = 'resolve-date' | 'close-date' @@ -283,7 +283,8 @@ function EditableCloseDate(props: { const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') const editor = new Editor({ content, extensions: exhibitExts }) - appendToEditor( + editor.commands.focus('end') + insertContent( editor, `<br><p>Close date updated to ${formattedCloseDate}</p>` ) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index d52913b3..d8a8d37f 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,12 +21,18 @@ import { useUsers } from 'web/hooks/use-users' import { mentionSuggestion } from './editor/mention-suggestion' import { DisplayMention } from './editor/mention' import Iframe from 'common/util/tiptap-iframe' -import { CodeIcon, PhotographIcon } from '@heroicons/react/solid' +import { + CodeIcon, + PhotographIcon, + PresentationChartLineIcon, +} from '@heroicons/react/solid' import { Modal } from './layout/modal' import { Col } from './layout/col' import { Button } from './button' import { Row } from './layout/row' import { Spacer } from './layout/spacer' +import { MarketModal } from './editor/market-modal' +import { insertContent } from './editor/utils' const DisplayImage = Image.configure({ HTMLAttributes: { @@ -105,7 +111,7 @@ export function useTextEditor(props: { // If the pasted content is iframe code, directly inject it const text = event.clipboardData?.getData('text/plain').trim() ?? '' if (isValidIframe(text)) { - editor.chain().insertContent(text).run() + insertContent(editor, text) return true // Prevent the code from getting pasted as text } @@ -139,6 +145,7 @@ export function TextEditor(props: { }) { const { editor, upload, children } = props const [iframeOpen, setIframeOpen] = useState(false) + const [marketOpen, setMarketOpen] = useState(false) return ( <> @@ -148,16 +155,15 @@ export function TextEditor(props: { <EditorContent editor={editor} /> {/* Toolbar, with buttons for images and embeds */} <div className="flex h-9 items-center gap-5 pl-4 pr-1"> - <div className="flex items-center"> + <div className="tooltip flex items-center" data-tip="Add image"> <FileUploadButton onFiles={upload.mutate} className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" > <PhotographIcon className="h-5 w-5" aria-hidden="true" /> - <span className="sr-only">Upload an image</span> </FileUploadButton> </div> - <div className="flex items-center"> + <div className="tooltip flex items-center" data-tip="Add embed"> <button type="button" onClick={() => setIframeOpen(true)} @@ -169,7 +175,23 @@ export function TextEditor(props: { setOpen={setIframeOpen} /> <CodeIcon className="h-5 w-5" aria-hidden="true" /> - <span className="sr-only">Embed an iframe</span> + </button> + </div> + <div className="tooltip flex items-center" data-tip="Add market"> + <button + type="button" + onClick={() => setMarketOpen(true)} + className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" + > + <MarketModal + editor={editor} + open={marketOpen} + setOpen={setMarketOpen} + /> + <PresentationChartLineIcon + className="h-5 w-5" + aria-hidden="true" + /> </button> </div> {/* Spacer that also focuses editor on click */} @@ -229,7 +251,7 @@ function IframeModal(props: { disabled={!valid} onClick={() => { if (editor && valid) { - editor.chain().insertContent(embedCode).run() + insertContent(editor, embedCode) setInput('') setOpen(false) } diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx new file mode 100644 index 00000000..1c88afbc --- /dev/null +++ b/web/components/editor/market-modal.tsx @@ -0,0 +1,86 @@ +import { Editor } from '@tiptap/react' +import { Contract } from 'common/contract' +import { useState } from 'react' +import { Button } from '../button' +import { ContractSearch } from '../contract-search' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { LoadingIndicator } from '../loading-indicator' +import { embedCode } from '../share-embed-button' +import { insertContent } from './utils' + +export function MarketModal(props: { + editor: Editor | null + open: boolean + setOpen: (open: boolean) => void +}) { + const { editor, open, setOpen } = props + + const [contracts, setContracts] = useState<Contract[]>([]) + const [loading, setLoading] = useState(false) + + async function addContract(contract: Contract) { + if (contracts.map((c) => c.id).includes(contract.id)) { + setContracts(contracts.filter((c) => c.id !== contract.id)) + } else setContracts([...contracts, contract]) + } + + async function doneAddingContracts() { + setLoading(true) + insertContent(editor, ...contracts.map(embedCode)) + setLoading(false) + setOpen(false) + setContracts([]) + } + + return ( + <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> + <Col className="h-[85vh] w-full gap-4 rounded-md bg-white"> + <Row className="p-8 pb-0"> + <div className={'text-xl text-indigo-700'}>Embed a market</div> + + {!loading && ( + <Row className="grow justify-end gap-4"> + {contracts.length > 0 && ( + <Button onClick={doneAddingContracts} color={'indigo'}> + Embed {contracts.length} question + {contracts.length > 1 && 's'} + </Button> + )} + <Button onClick={() => setContracts([])} color="gray"> + Cancel + </Button> + </Row> + )} + </Row> + + {loading && ( + <div className="w-full justify-center"> + <LoadingIndicator /> + </div> + )} + + <div className="overflow-y-scroll sm:px-8"> + <ContractSearch + hideOrderSelector + onContractClick={addContract} + overrideGridClassName={ + 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' + } + showPlaceHolder + cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} + querySortOptions={{ disableQueryString: true }} + highlightOptions={{ + contractIds: contracts.map((c) => c.id), + highlightClassName: + '!bg-indigo-100 outline outline-2 outline-indigo-300', + }} + additionalFilter={{}} /* hide pills */ + headerClassName="bg-white" + /> + </div> + </Col> + </Modal> + ) +} diff --git a/web/components/editor/utils.ts b/web/components/editor/utils.ts index 74af38c5..50b94ce2 100644 --- a/web/components/editor/utils.ts +++ b/web/components/editor/utils.ts @@ -1,10 +1,13 @@ import { Editor, Content } from '@tiptap/react' -export function appendToEditor(editor: Editor | null, content: Content) { - editor - ?.chain() - .focus('end') - .createParagraphNear() - .insertContent(content) - .run() +export function insertContent(editor: Editor | null, ...contents: Content[]) { + if (!editor) { + return + } + + let e = editor.chain() + for (const content of contents) { + e = e.createParagraphNear().insertContent(content) + } + e.run() } diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 1b24c689..8678299b 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -9,13 +9,11 @@ import { copyToClipboard } from 'web/lib/util/copy' import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' -function copyEmbedCode(contract: Contract) { +export function embedCode(contract: Contract) { const title = contract.question const src = `https://${DOMAIN}/embed${contractPath(contract)}` - const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` - - copyToClipboard(embedCode) + return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` } export function ShareEmbedButton(props: { @@ -29,7 +27,7 @@ export function ShareEmbedButton(props: { as="div" className="relative z-10 flex-shrink-0" onMouseUp={() => { - copyEmbedCode(contract) + copyToClipboard(embedCode(contract)) track('copy embed code') }} > diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index c4bce0c0..e917e4af 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -25,12 +25,18 @@ export function getSavedSort() { } } -export function useQueryAndSortParams(options?: { +export interface QuerySortOptions { defaultSort?: Sort shouldLoadFromStorage?: boolean -}) { - const { defaultSort = DEFAULT_SORT, shouldLoadFromStorage = true } = - options ?? {} + /** Use normal react state instead of url query string */ + disableQueryString?: boolean +} + +export function useQueryAndSortParams({ + defaultSort = DEFAULT_SORT, + shouldLoadFromStorage = true, + disableQueryString, +}: QuerySortOptions = {}) { const router = useRouter() const { s: sort, q: query } = router.query as { @@ -68,7 +74,9 @@ export function useQueryAndSortParams(options?: { const setQuery = (query: string | undefined) => { setQueryState(query) - pushQuery(query) + if (!disableQueryString) { + pushQuery(query) + } } useEffect(() => { @@ -86,10 +94,13 @@ export function useQueryAndSortParams(options?: { } }) + // use normal state if querydisableQueryString + const [sortState, setSortState] = useState(defaultSort) + return { - sort: sort ?? defaultSort, + sort: disableQueryString ? sortState : sort ?? defaultSort, query: queryState ?? '', - setSort, + setSort: disableQueryString ? setSortState : setSort, setQuery, } } diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index fb05cf3a..f56c82d1 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -4,6 +4,7 @@ import { sortBy } from 'lodash' import { ContractsGrid } from 'web/components/contract/contracts-grid' import { useContracts } from 'web/hooks/use-contracts' import { + QuerySortOptions, Sort, useQueryAndSortParams, } from 'web/hooks/use-sort-and-query-params' @@ -11,10 +12,7 @@ import { const MAX_CONTRACTS_RENDERED = 100 export default function ContractSearchFirestore(props: { - querySortOptions?: { - defaultSort: Sort - shouldLoadFromStorage?: boolean - } + querySortOptions?: QuerySortOptions additionalFilter?: { creatorId?: string tag?: string From daba28423ab0220116bd83b5a988c452f5a5b055 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 11 Aug 2022 14:41:21 -0700 Subject: [PATCH 486/519] Improve create page UI (#746) * Adjust create page styles * Keep answers when switching market type --- .../answers/multiple-choice-answers.tsx | 15 ++++++--------- web/components/choices-toggle-group.tsx | 5 ++++- web/pages/create.tsx | 9 +++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/web/components/answers/multiple-choice-answers.tsx b/web/components/answers/multiple-choice-answers.tsx index 450c221a..69f54648 100644 --- a/web/components/answers/multiple-choice-answers.tsx +++ b/web/components/answers/multiple-choice-answers.tsx @@ -1,26 +1,23 @@ import { MAX_ANSWER_LENGTH } from 'common/answer' -import { useState } from 'react' import Textarea from 'react-expanding-textarea' import { XIcon } from '@heroicons/react/solid' - import { Col } from '../layout/col' import { Row } from '../layout/row' export function MultipleChoiceAnswers(props: { + answers: string[] setAnswers: (answers: string[]) => void }) { - const [answers, setInternalAnswers] = useState(['', '', '']) + const { answers, setAnswers } = props const setAnswer = (i: number, answer: string) => { const newAnswers = setElement(answers, i, answer) - setInternalAnswers(newAnswers) - props.setAnswers(newAnswers) + setAnswers(newAnswers) } const removeAnswer = (i: number) => { const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1)) - setInternalAnswers(newAnswers) - props.setAnswers(newAnswers) + setAnswers(newAnswers) } const addAnswer = () => setAnswer(answers.length, '') @@ -40,10 +37,10 @@ export function MultipleChoiceAnswers(props: { /> {answers.length > 2 && ( <button - className="btn btn-xs btn-outline ml-2" + className="-mr-2 rounded p-2" onClick={() => removeAnswer(i)} > - <XIcon className="h-4 w-4 flex-shrink-0" /> + <XIcon className="h-5 w-5 flex-shrink-0" /> </button> )} </Row> diff --git a/web/components/choices-toggle-group.tsx b/web/components/choices-toggle-group.tsx index 61c4e4fd..1e918eda 100644 --- a/web/components/choices-toggle-group.tsx +++ b/web/components/choices-toggle-group.tsx @@ -22,7 +22,10 @@ export function ChoicesToggleGroup(props: { } = props return ( <RadioGroup - className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')} + className={clsx( + className, + 'flex flex-row flex-wrap items-center gap-2 sm:gap-3' + )} value={currentChoice.toString()} onChange={setChoice} > diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 19ab2fe0..3225fb4d 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -120,7 +120,8 @@ export function NewContract(props: { const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale) const [initialValueString, setInitialValueString] = useState(initValue) - const [answers, setAnswers] = useState<string[]>([]) // for multiple choice + // for multiple choice, init to 3 empty answers + const [answers, setAnswers] = useState(['', '', '']) useEffect(() => { if (groupId) @@ -285,7 +286,7 @@ export function NewContract(props: { <Spacer h={6} /> {outcomeType === 'MULTIPLE_CHOICE' && ( - <MultipleChoiceAnswers setAnswers={setAnswers} /> + <MultipleChoiceAnswers answers={answers} setAnswers={setAnswers} /> )} {outcomeType === 'PSEUDO_NUMERIC' && ( @@ -299,7 +300,7 @@ export function NewContract(props: { <Row className="gap-2"> <input type="number" - className="input input-bordered" + className="input input-bordered w-32" placeholder="MIN" onClick={(e) => e.stopPropagation()} onChange={(e) => setMinString(e.target.value)} @@ -310,7 +311,7 @@ export function NewContract(props: { /> <input type="number" - className="input input-bordered" + className="input input-bordered w-32" placeholder="MAX" onClick={(e) => e.stopPropagation()} onChange={(e) => setMaxString(e.target.value)} From 9311652bed12b818e01c29d61e6bcae1b6dbf9b8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 11 Aug 2022 20:18:01 -0700 Subject: [PATCH 487/519] Support Youtube, Tweet, and Metaculus embeds in editor (#744) * Embed a tweet by URL * Clean up imports * Prevent tweetId from getting interpreted as a number * Use a single place to embed iframe, Youtube, and Tweets * Support Manifold and Metaculus embeds * Remove unused import * Simplify Manifold paste logic * Clean up embed rewrite code * Add back comment * Rejigger config so tsx is only in web/ * Clean up comment * Revert unnecessary tsconfig change * Fix placeholder * Lighten placeholder --- common/util/parse.ts | 2 + common/util/tiptap-tweet-type.ts | 37 ++++++++ web/components/editor.tsx | 80 ++--------------- web/components/editor/embed-modal.tsx | 116 +++++++++++++++++++++++++ web/components/editor/tiptap-tweet.tsx | 13 +++ web/components/editor/tweet-embed.tsx | 19 ++++ web/package.json | 1 + yarn.lock | 12 +++ 8 files changed, 206 insertions(+), 74 deletions(-) create mode 100644 common/util/tiptap-tweet-type.ts create mode 100644 web/components/editor/embed-modal.tsx create mode 100644 web/components/editor/tiptap-tweet.tsx create mode 100644 web/components/editor/tweet-embed.tsx diff --git a/common/util/parse.ts b/common/util/parse.ts index f07e4097..4fac3225 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -22,6 +22,7 @@ import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import { Mention } from '@tiptap/extension-mention' import Iframe from './tiptap-iframe' +import TiptapTweet from './tiptap-tweet-type' import { uniq } from 'lodash' export function parseTags(text: string) { @@ -94,6 +95,7 @@ export const exhibitExts = [ Link, Mention, Iframe, + TiptapTweet, ] export function richTextToString(text?: JSONContent) { diff --git a/common/util/tiptap-tweet-type.ts b/common/util/tiptap-tweet-type.ts new file mode 100644 index 00000000..0b9acffc --- /dev/null +++ b/common/util/tiptap-tweet-type.ts @@ -0,0 +1,37 @@ +import { Node, mergeAttributes } from '@tiptap/core' + +export interface TweetOptions { + tweetId: string +} + +// This is a version of the Tiptap Node config without addNodeView, +// since that would require bundling in tsx +export const TiptapTweetNode = { + name: 'tiptapTweet', + group: 'block', + atom: true, + + addAttributes() { + return { + tweetId: { + default: null, + }, + } + }, + + parseHTML() { + return [ + { + tag: 'tiptap-tweet', + }, + ] + }, + + renderHTML(props: { HTMLAttributes: Record<string, any> }) { + return ['tiptap-tweet', mergeAttributes(props.HTMLAttributes)] + }, +} + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export default Node.create<TweetOptions>(TiptapTweetNode) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index d8a8d37f..74f608aa 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -21,16 +21,13 @@ import { useUsers } from 'web/hooks/use-users' import { mentionSuggestion } from './editor/mention-suggestion' import { DisplayMention } from './editor/mention' import Iframe from 'common/util/tiptap-iframe' +import TiptapTweet from './editor/tiptap-tweet' +import { EmbedModal } from './editor/embed-modal' import { CodeIcon, PhotographIcon, PresentationChartLineIcon, } from '@heroicons/react/solid' -import { Modal } from './layout/modal' -import { Col } from './layout/col' -import { Button } from './button' -import { Row } from './layout/row' -import { Spacer } from './layout/spacer' import { MarketModal } from './editor/market-modal' import { insertContent } from './editor/utils' @@ -88,6 +85,7 @@ export function useTextEditor(props: { suggestion: mentionSuggestion(users), }), Iframe, + TiptapTweet, ], content: defaultValue, }, @@ -131,13 +129,6 @@ function isValidIframe(text: string) { return /^<iframe.*<\/iframe>$/.test(text) } -function isValidUrl(text: string) { - // Conjured by Codex, not sure if it's actually good - return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test( - text - ) -} - export function TextEditor(props: { editor: Editor | null upload: ReturnType<typeof useUploadMutation> @@ -169,7 +160,7 @@ export function TextEditor(props: { onClick={() => setIframeOpen(true)} className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" > - <IframeModal + <EmbedModal editor={editor} open={iframeOpen} setOpen={setIframeOpen} @@ -214,66 +205,6 @@ export function TextEditor(props: { ) } -function IframeModal(props: { - editor: Editor | null - open: boolean - setOpen: (open: boolean) => void -}) { - const { editor, open, setOpen } = props - const [input, setInput] = useState('') - const valid = isValidIframe(input) || isValidUrl(input) - const embedCode = isValidIframe(input) ? input : `<iframe src="${input}" />` - - return ( - <Modal open={open} setOpen={setOpen}> - <Col className="gap-2 rounded bg-white p-6"> - <label - htmlFor="embed" - className="block text-sm font-medium text-gray-700" - > - Embed a market, Youtube video, etc. - </label> - <input - type="text" - name="embed" - id="embed" - className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - placeholder='e.g. <iframe src="..."></iframe>' - value={input} - onChange={(e) => setInput(e.target.value)} - /> - - {/* Preview the embed if it's valid */} - {valid ? <RichContent content={embedCode} /> : <Spacer h={2} />} - - <Row className="gap-2"> - <Button - disabled={!valid} - onClick={() => { - if (editor && valid) { - insertContent(editor, embedCode) - setInput('') - setOpen(false) - } - }} - > - Embed - </Button> - <Button - color="gray" - onClick={() => { - setInput('') - setOpen(false) - }} - > - Cancel - </Button> - </Row> - </Col> - </Modal> - ) -} - const useUploadMutation = (editor: Editor | null) => useMutation( (files: File[]) => @@ -292,7 +223,7 @@ const useUploadMutation = (editor: Editor | null) => } ) -function RichContent(props: { +export function RichContent(props: { content: JSONContent | string smallImage?: boolean }) { @@ -305,6 +236,7 @@ function RichContent(props: { DisplayLink, DisplayMention, Iframe, + TiptapTweet, ], content, editable: false, diff --git a/web/components/editor/embed-modal.tsx b/web/components/editor/embed-modal.tsx new file mode 100644 index 00000000..55b72d19 --- /dev/null +++ b/web/components/editor/embed-modal.tsx @@ -0,0 +1,116 @@ +import { Editor } from '@tiptap/react' +import { useState } from 'react' +import { Button } from '../button' +import { RichContent } from '../editor' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { Spacer } from '../layout/spacer' + +type EmbedPattern = { + // Regex should have a single capture group. + regex: RegExp + rewrite: (text: string) => string +} + +const embedPatterns: EmbedPattern[] = [ + { + regex: /^(<iframe.*<\/iframe>)$/, + rewrite: (text: string) => text, + }, + { + regex: /^https?:\/\/manifold\.markets\/([^\/]+\/[^\/]+)/, + rewrite: (slug) => + `<iframe src="https://manifold.markets/embed/${slug}"></iframe>`, + }, + { + regex: /^https?:\/\/twitter\.com\/.*\/status\/(\d+)/, + // Hack: append a leading 't', to prevent tweetId from being interpreted as a number. + // If it's a number, there may be numeric precision issues. + rewrite: (id) => `<tiptap-tweet tweetid="t${id}"></tiptap-tweet>`, + }, + { + regex: /^https?:\/\/www\.youtube\.com\/watch\?v=([^&]+)/, + rewrite: (id) => + `<iframe src="https://www.youtube.com/embed/${id}"></iframe>`, + }, + { + regex: /^https?:\/\/www\.metaculus\.com\/questions\/(\d+)/, + rewrite: (id) => + `<iframe src="https://www.metaculus.com/questions/embed/${id}"></iframe>`, + }, + { + regex: /^(https?:\/\/.*)/, + rewrite: (url) => `<iframe src="${url}"></iframe>`, + }, +] + +function embedCode(text: string) { + for (const pattern of embedPatterns) { + const match = text.match(pattern.regex) + if (match) { + return pattern.rewrite(match[1]) + } + } + return null +} + +export function EmbedModal(props: { + editor: Editor | null + open: boolean + setOpen: (open: boolean) => void +}) { + const { editor, open, setOpen } = props + const [input, setInput] = useState('') + const embed = embedCode(input) + + return ( + <Modal open={open} setOpen={setOpen}> + <Col className="gap-2 rounded bg-white p-6"> + <label + htmlFor="embed" + className="block text-sm font-medium text-gray-700" + > + Embed a Youtube video, Tweet, or other link + </label> + <input + type="text" + name="embed" + id="embed" + className="block w-full rounded-md border-gray-300 shadow-sm placeholder:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + placeholder="e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ" + value={input} + onChange={(e) => setInput(e.target.value)} + /> + + {/* Preview the embed if it's valid */} + {embed ? <RichContent content={embed} /> : <Spacer h={2} />} + + <Row className="gap-2"> + <Button + disabled={!embed} + onClick={() => { + if (editor && embed) { + editor.chain().insertContent(embed).run() + console.log('editorjson', editor.getJSON()) + setInput('') + setOpen(false) + } + }} + > + Embed + </Button> + <Button + color="gray" + onClick={() => { + setInput('') + setOpen(false) + }} + > + Cancel + </Button> + </Row> + </Col> + </Modal> + ) +} diff --git a/web/components/editor/tiptap-tweet.tsx b/web/components/editor/tiptap-tweet.tsx new file mode 100644 index 00000000..99106c43 --- /dev/null +++ b/web/components/editor/tiptap-tweet.tsx @@ -0,0 +1,13 @@ +import { Node } from '@tiptap/core' +import { ReactNodeViewRenderer } from '@tiptap/react' +import { TiptapTweetNode } from 'common/util/tiptap-tweet-type' +import WrappedTwitterTweetEmbed from './tweet-embed' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export default Node.create<TweetOptions>({ + ...TiptapTweetNode, + addNodeView() { + return ReactNodeViewRenderer(WrappedTwitterTweetEmbed) + }, +}) diff --git a/web/components/editor/tweet-embed.tsx b/web/components/editor/tweet-embed.tsx new file mode 100644 index 00000000..91b2fa65 --- /dev/null +++ b/web/components/editor/tweet-embed.tsx @@ -0,0 +1,19 @@ +import { NodeViewWrapper } from '@tiptap/react' +import { TwitterTweetEmbed } from 'react-twitter-embed' + +export default function WrappedTwitterTweetEmbed(props: { + node: { + attrs: { + tweetId: string + } + } +}): JSX.Element { + // Remove the leading 't' from the tweet id + const tweetId = props.node.attrs.tweetId.slice(1) + + return ( + <NodeViewWrapper className="tiptap-tweet"> + <TwitterTweetEmbed tweetId={tweetId} /> + </NodeViewWrapper> + ) +} diff --git a/web/package.json b/web/package.json index 4fba3359..a008026b 100644 --- a/web/package.json +++ b/web/package.json @@ -53,6 +53,7 @@ "react-hot-toast": "2.2.0", "react-instantsearch-hooks-web": "6.24.1", "react-query": "3.39.0", + "react-twitter-embed": "4.0.4", "string-similarity": "^4.0.4", "tippy.js": "6.3.7" }, diff --git a/yarn.lock b/yarn.lock index bbf8d3ee..e83ffc0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9964,6 +9964,13 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" +react-twitter-embed@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-twitter-embed/-/react-twitter-embed-4.0.4.tgz#4a6b8354acc266876ff1110b9f648518ea20db6d" + integrity sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA== + dependencies: + scriptjs "^2.5.9" + react-with-forwarded-ref@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5" @@ -10464,6 +10471,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +scriptjs@^2.5.9: + version "2.5.9" + resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.9.tgz#343915cd2ec2ed9bfdde2b9875cd28f59394b35f" + integrity sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg== + search-insights@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.2.1.tgz#9c93344fbae5fbf2f88c1a81b46b4b5d888c11f7" From af4c442105b7221536e292071e7bebd306ddb4ff Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 11 Aug 2022 20:23:33 -0700 Subject: [PATCH 488/519] Support Twitch video and channel embeds --- web/components/editor/embed-modal.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/web/components/editor/embed-modal.tsx b/web/components/editor/embed-modal.tsx index 55b72d19..6acfd8f0 100644 --- a/web/components/editor/embed-modal.tsx +++ b/web/components/editor/embed-modal.tsx @@ -1,4 +1,5 @@ import { Editor } from '@tiptap/react' +import { DOMAIN } from 'common/envs/constants' import { useState } from 'react' import { Button } from '../button' import { RichContent } from '../editor' @@ -39,6 +40,19 @@ const embedPatterns: EmbedPattern[] = [ rewrite: (id) => `<iframe src="https://www.metaculus.com/questions/embed/${id}"></iframe>`, }, + // Twitch is a bit annoying, since it requires the `&parent=DOMAIN` to match + { + // Twitch: https://www.twitch.tv/videos/1445087149 + regex: /^https?:\/\/www\.twitch\.tv\/videos\/(\d+)/, + rewrite: (id) => + `<iframe src="https://player.twitch.tv/?video=${id}&parent=${DOMAIN}"></iframe>`, + }, + { + // Twitch: https://www.twitch.tv/sirsalty + regex: /^https?:\/\/www\.twitch\.tv\/([^\/]+)/, + rewrite: (channel) => + `<iframe src="https://player.twitch.tv/?channel=${channel}&parent=${DOMAIN}"></iframe>`, + }, { regex: /^(https?:\/\/.*)/, rewrite: (url) => `<iframe src="${url}"></iframe>`, From 38d9e8190c88f69506f76b555c92db99a93ba384 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 20:44:51 -0700 Subject: [PATCH 489/519] Only load portfolio history inside user page bets tab (#747) --- .../portfolio/portfolio-value-section.tsx | 17 ++++++++++++----- web/components/user-page.tsx | 11 ++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 611a19d1..13880bd4 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -1,20 +1,27 @@ import { PortfolioMetrics } from 'common/user' import { formatMoney } from 'common/util/format' import { last } from 'lodash' -import { memo, useState } from 'react' -import { Period } from 'web/lib/firebase/users' +import { memo, useEffect, useState } from 'react' +import { Period, getPortfolioHistory } from 'web/lib/firebase/users' import { Col } from '../layout/col' import { Row } from '../layout/row' import { PortfolioValueGraph } from './portfolio-value-graph' export const PortfolioValueSection = memo( function PortfolioValueSection(props: { - portfolioHistory: PortfolioMetrics[] + userId: string disableSelector?: boolean }) { - const { portfolioHistory, disableSelector } = props - const lastPortfolioMetrics = last(portfolioHistory) + const { disableSelector, userId } = props + const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime') + const [portfolioHistory, setUsersPortfolioHistory] = useState< + PortfolioMetrics[] + >([]) + useEffect(() => { + getPortfolioHistory(userId).then(setUsersPortfolioHistory) + }, [userId]) + const lastPortfolioMetrics = last(portfolioHistory) if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { return <></> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 322ddb96..6a901b13 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' import { PencilIcon } from '@heroicons/react/outline' -import { getPortfolioHistory, User } from 'web/lib/firebase/users' +import { User } from 'web/lib/firebase/users' import { CreatorContractsList } from './contract/contracts-grid' import { SEO } from './SEO' import { Page } from './page' @@ -26,7 +26,6 @@ import { FullscreenConfetti } from 'web/components/fullscreen-confetti' import { BetsList } from './bets-list' import { FollowersButton, FollowingButton } from './following-button' import { UserFollowButton } from './follow-button' -import { PortfolioMetrics } from 'common/user' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' @@ -74,9 +73,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { ? 0 : userBets.filter((bet) => !bet.isRedemption && bet.amount !== 0).length - const [portfolioHistory, setUsersPortfolioHistory] = useState< - PortfolioMetrics[] - >([]) const [contractsById, setContractsById] = useState< Dictionary<Contract> | undefined >() @@ -91,7 +87,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { if (!user) return getUsersComments(user.id).then(setUsersComments) listContracts(user.id).then(setUsersContracts) - getPortfolioHistory(user.id).then(setUsersPortfolioHistory) }, [user]) // TODO: display comments on groups @@ -300,9 +295,7 @@ export function UserPage(props: { user: User; currentUser?: User }) { title: 'Bets', content: ( <div> - <PortfolioValueSection - portfolioHistory={portfolioHistory} - /> + <PortfolioValueSection userId={user.id} /> <BetsList user={user} bets={userBets} From e2eae01ad8370b0442b3e0aedb4c3850d4addc64 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 20:46:18 -0700 Subject: [PATCH 490/519] Add a shitload of logging to the server auth code (#749) --- web/lib/firebase/server-auth.ts | 50 ++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts index b0d225f1..8e5336c8 100644 --- a/web/lib/firebase/server-auth.ts +++ b/web/lib/firebase/server-auth.ts @@ -74,26 +74,34 @@ type RequestContext = { const authAndRefreshTokens = async (ctx: RequestContext) => { const adminAuth = (await ensureApp()).auth() const clientAuth = getAuth(clientApp) + console.debug('Initialized Firebase auth libraries.') + let { id, refresh, custom } = getTokensFromCookies(ctx.req) // step 0: if you have no refresh token you are logged out if (refresh == null) { + console.debug('User is unauthenticated.') return undefined } + console.debug('User may be authenticated; checking cookies.') + // step 1: given a valid refresh token, ensure a valid ID token if (id != null) { // if they have an ID token, throw it out if it's invalid/expired try { await adminAuth.verifyIdToken(id) + console.debug('Verified ID token.') } catch { id = undefined + console.debug('Invalid existing ID token.') } } if (id == null) { // ask for a new one from google using the refresh token try { const resp = await requestFirebaseIdToken(refresh) + console.debug('Obtained fresh ID token from Firebase.') id = resp.id_token refresh = resp.refresh_token } catch (e) { @@ -108,27 +116,23 @@ const authAndRefreshTokens = async (ctx: RequestContext) => { if (custom != null) { // sign in with this token, or throw it out if it's invalid/expired try { - return { - creds: await signInWithCustomToken(clientAuth, custom), - id, - refresh, - custom, - } + const creds = await signInWithCustomToken(clientAuth, custom) + console.debug('Signed in with custom token.') + return { creds, id, refresh, custom } } catch { custom = undefined + console.debug('Invalid existing custom token.') } } if (custom == null) { // ask for a new one from our cloud functions using the ID token, then sign in try { const resp = await requestManifoldCustomToken(id) + console.debug('Obtained fresh custom token from backend.') custom = resp.token - return { - creds: await signInWithCustomToken(clientAuth, custom), - id, - refresh, - custom, - } + const creds = await signInWithCustomToken(clientAuth, custom) + console.debug('Signed in with custom token.') + return { creds, id, refresh, custom } } catch (e) { // big unexpected problem -- functionally, they are not logged in console.error(e) @@ -138,13 +142,17 @@ const authAndRefreshTokens = async (ctx: RequestContext) => { } export const authenticateOnServer = async (ctx: RequestContext) => { + console.debug('Server authentication sequence starting.') const tokens = await authAndRefreshTokens(ctx) + console.debug('Finished checking and refreshing tokens.') const creds = tokens?.creds try { if (tokens == null) { deleteTokenCookies(ctx.res) + console.debug('Not logged in; cleared token cookies.') } else { setTokenCookies(tokens, ctx.res) + console.debug('Logged in; set current token cookies.') } } catch (e) { // definitely not supposed to happen, but let's be maximally robust @@ -168,8 +176,15 @@ export const redirectIfLoggedIn = <P>( return async (ctx: GetServerSidePropsContext) => { const creds = await authenticateOnServer(ctx) if (creds == null) { - return fn != null ? await fn(ctx) : { props: {} } + if (fn == null) { + return { props: {} } + } else { + const props = fn(ctx) + console.debug('Finished getting initial props for rendering.') + return props + } } else { + console.debug(`Redirecting to ${dest}.`) return { redirect: { destination: dest, permanent: false } } } } @@ -182,9 +197,16 @@ export const redirectIfLoggedOut = <P>( return async (ctx: GetServerSidePropsContext) => { const creds = await authenticateOnServer(ctx) if (creds == null) { + console.debug(`Redirecting to ${dest}.`) return { redirect: { destination: dest, permanent: false } } } else { - return fn != null ? await fn(ctx, creds) : { props: {} } + if (fn == null) { + return { props: {} } + } else { + const props = fn(ctx, creds) + console.debug('Finished getting initial props for rendering.') + return props + } } } } From 7ad8af848afcdb0ed57aa76cc15ce89838ea7b7e Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 11 Aug 2022 20:54:03 -0700 Subject: [PATCH 491/519] Replace DaisyUI buttons with TailwindUI buttons Maybe this should use the button component...? But that's styled differently, the rest of /create uses standard TailwindUI --- web/components/answers/multiple-choice-answers.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web/components/answers/multiple-choice-answers.tsx b/web/components/answers/multiple-choice-answers.tsx index 69f54648..fb0e8d03 100644 --- a/web/components/answers/multiple-choice-answers.tsx +++ b/web/components/answers/multiple-choice-answers.tsx @@ -3,6 +3,7 @@ import Textarea from 'react-expanding-textarea' import { XIcon } from '@heroicons/react/solid' import { Col } from '../layout/col' import { Row } from '../layout/row' +import { Button } from '../button' export function MultipleChoiceAnswers(props: { answers: string[] @@ -25,7 +26,7 @@ export function MultipleChoiceAnswers(props: { return ( <Col> {answers.map((answer, i) => ( - <Row className="mb-2 items-center align-middle"> + <Row className="mb-2 items-center gap-2 align-middle"> {i + 1}.{' '} <Textarea value={answer} @@ -37,17 +38,22 @@ export function MultipleChoiceAnswers(props: { /> {answers.length > 2 && ( <button - className="-mr-2 rounded p-2" onClick={() => removeAnswer(i)} + type="button" + className="inline-flex items-center rounded-full border border-gray-300 bg-white p-1 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" > - <XIcon className="h-5 w-5 flex-shrink-0" /> + <XIcon className="h-5 w-5" aria-hidden="true" /> </button> )} </Row> ))} <Row className="justify-end"> - <button className="btn btn-outline btn-xs" onClick={addAnswer}> + <button + type="button" + onClick={addAnswer} + className="inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + > Add answer </button> </Row> From 80fd38990fec494172a4006c8e4b66c5b62f40b8 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 11 Aug 2022 21:07:54 -0700 Subject: [PATCH 492/519] Experimentally do not optimizeCss --- web/next.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/next.config.js b/web/next.config.js index 37758952..7b56ecdf 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -7,7 +7,6 @@ module.exports = { optimizeFonts: false, experimental: { externalDir: true, - optimizeCss: true, modularizeImports: { '@heroicons/react/solid/?(((\\w*)?/?)*)': { transform: '@heroicons/react/solid/{{ matches.[1] }}/{{member}}', From 8ebccd05ec59055a9a1a7b151df3a217d6ac121c Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 12 Aug 2022 11:24:08 -0500 Subject: [PATCH 493/519] market movement warning; add bankroll warning to FR markets --- web/components/answers/answer-bet-panel.tsx | 19 +++++++++++++++++++ web/components/bet-panel.tsx | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 6dcba79b..238c7783 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -26,6 +26,7 @@ import { Bet } from 'common/bet' import { track } from 'web/lib/service/analytics' import { SignUpPrompt } from '../sign-up-prompt' import { isIOS } from 'web/lib/util/device' +import { AlertBox } from '../alert-box' export function AnswerBetPanel(props: { answer: Answer @@ -113,6 +114,8 @@ export function AnswerBetPanel(props: { const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturnPercent = formatPercent(currentReturn) + const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) + return ( <Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}> <Row className="items-center justify-between self-stretch"> @@ -139,6 +142,22 @@ export function AnswerBetPanel(props: { disabled={isSubmitting} inputRef={inputRef} /> + + {(betAmount ?? 0) > 10 && + bankrollFraction >= 0.5 && + bankrollFraction <= 1 ? ( + <AlertBox + title="Whoa, there!" + text={`You might not want to spend ${formatPercent( + bankrollFraction + )} of your balance on a single bet. \n\nCurrent balance: ${formatMoney( + user?.balance ?? 0 + )}`} + /> + ) : ( + '' + )} + <Col className="mt-3 w-full gap-3"> <Row className="items-center justify-between text-sm"> <div className="text-gray-500">Probability</div> diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 9c572e0c..d5f88d43 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -254,6 +254,7 @@ function BuyPanel(props: { const resultProb = getCpmmProbability(newPool, newP) const probStayedSame = formatPercent(resultProb) === formatPercent(initialProb) + const probChange = Math.abs(resultProb - initialProb) const currentPayout = newBet.shares @@ -305,6 +306,19 @@ function BuyPanel(props: { '' )} + {(betAmount ?? 0) > 10 && probChange >= 0.3 ? ( + <AlertBox + title="Whoa, there!" + text={`Are you sure you want to move the market ${ + isPseudoNumeric && contract.isLogScale + ? 'this much' + : format(probChange) + }?`} + /> + ) : ( + '' + )} + <Col className="mt-3 w-full gap-3"> <Row className="items-center justify-between text-sm"> <div className="text-gray-500"> From d2b634c775f5f54ed94e758730d9c830e5592a2c Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Fri, 12 Aug 2022 11:33:02 -0500 Subject: [PATCH 494/519] template email tracking --- functions/src/send-email.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/src/send-email.ts b/functions/src/send-email.ts index d081997f..7ff4c047 100644 --- a/functions/src/send-email.ts +++ b/functions/src/send-email.ts @@ -35,6 +35,8 @@ export const sendTemplateEmail = ( subject, template: templateId, 'h:X-Mailgun-Variables': JSON.stringify(templateData), + 'o:tag': templateId, + 'o:tracking': true, } const mg = initMailgun() From df858f916b09fa17a7d06fdb3f594bb660b7ef51 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 12 Aug 2022 12:04:23 -0700 Subject: [PATCH 495/519] Migrate daisy tooltips to our own to fix cutoffs (#748) * Make all tooltips use our component * Stop mobile tooltip crop (daisy -> floating-ui) * Show tooltip on tap for touch devices Except tooltips on buttons * migrate another daisy tooltip to ours * Prevent hidden tooltip from covering click/hover --- functions/package.json | 3 +- web/components/contract/contract-card.tsx | 16 ++-- web/components/datetime-tooltip.tsx | 20 ++--- web/components/editor.tsx | 13 ++-- web/components/feed/copy-link-date-time.tsx | 2 +- web/components/info-tooltip.tsx | 5 +- web/components/outcome-label.tsx | 27 +------ web/components/tipper.tsx | 8 +- web/components/tooltip.tsx | 81 +++++++++++++++++++-- yarn.lock | 19 +++++ 10 files changed, 127 insertions(+), 67 deletions(-) diff --git a/functions/package.json b/functions/package.json index b0d8e458..b6c1bb5c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -25,14 +25,15 @@ "main": "functions/src/index.js", "dependencies": { "@amplitude/node": "1.10.0", + "@floating-ui/react-dom": "1.0.0", "@google-cloud/functions-framework": "3.1.2", "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", - "dayjs": "1.11.4", "cors": "2.8.5", + "dayjs": "1.11.4", "express": "4.18.1", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 248c3863..ac1a2fa2 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -31,6 +31,7 @@ import { useUser } from 'web/hooks/use-user' import { track } from '@amplitude/analytics-browser' import { trackCallback } from 'web/lib/service/analytics' import { getMappedValue } from 'common/pseudo-numeric' +import { Tooltip } from '../tooltip' export function ContractCard(props: { contract: Contract @@ -333,22 +334,19 @@ export function PseudoNumericResolutionOrExpectation(props: { {resolution === 'CANCEL' ? ( <CancelLabel /> ) : ( - <div - className={clsx('tooltip', textColor)} - data-tip={value.toFixed(2)} - > + <Tooltip className={textColor} text={value.toFixed(2)}> {formatLargeNumber(value)} - </div> + </Tooltip> )} </> ) : ( <> - <div - className={clsx('tooltip text-3xl', textColor)} - data-tip={value.toFixed(2)} + <Tooltip + className={clsx('text-3xl', textColor)} + text={value.toFixed(2)} > {formatLargeNumber(value)} - </div> + </Tooltip> <div className={clsx('text-base', textColor)}>expected</div> </> )} diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx index 7f7a9b45..7aaf61aa 100644 --- a/web/components/datetime-tooltip.tsx +++ b/web/components/datetime-tooltip.tsx @@ -1,9 +1,8 @@ -import React from 'react' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' import advanced from 'dayjs/plugin/advancedFormat' -import { ClientRender } from './client-render' +import { Tooltip } from './tooltip' dayjs.extend(utc) dayjs.extend(timezone) @@ -13,23 +12,16 @@ export function DateTimeTooltip(props: { time: number text?: string children?: React.ReactNode + noTap?: boolean }) { - const { time, text } = props + const { time, text, noTap } = props const formattedTime = dayjs(time).format('MMM DD, YYYY hh:mm a z') const toolTip = text ? `${text} ${formattedTime}` : formattedTime return ( - <> - <ClientRender> - <span - className="tooltip hidden cursor-default sm:inline-block" - data-tip={toolTip} - > - {props.children} - </span> - </ClientRender> - <span className="whitespace-nowrap sm:hidden">{props.children}</span> - </> + <Tooltip text={toolTip} noTap={noTap}> + {props.children} + </Tooltip> ) } diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 74f608aa..f4166f27 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -30,6 +30,7 @@ import { } from '@heroicons/react/solid' import { MarketModal } from './editor/market-modal' import { insertContent } from './editor/utils' +import { Tooltip } from './tooltip' const DisplayImage = Image.configure({ HTMLAttributes: { @@ -146,15 +147,15 @@ export function TextEditor(props: { <EditorContent editor={editor} /> {/* Toolbar, with buttons for images and embeds */} <div className="flex h-9 items-center gap-5 pl-4 pr-1"> - <div className="tooltip flex items-center" data-tip="Add image"> + <Tooltip className="flex items-center" text="Add image" noTap> <FileUploadButton onFiles={upload.mutate} className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" > <PhotographIcon className="h-5 w-5" aria-hidden="true" /> </FileUploadButton> - </div> - <div className="tooltip flex items-center" data-tip="Add embed"> + </Tooltip> + <Tooltip className="flex items-center" text="Add embed" noTap> <button type="button" onClick={() => setIframeOpen(true)} @@ -167,8 +168,8 @@ export function TextEditor(props: { /> <CodeIcon className="h-5 w-5" aria-hidden="true" /> </button> - </div> - <div className="tooltip flex items-center" data-tip="Add market"> + </Tooltip> + <Tooltip className="flex items-center" text="Add market" noTap> <button type="button" onClick={() => setMarketOpen(true)} @@ -184,7 +185,7 @@ export function TextEditor(props: { aria-hidden="true" /> </button> - </div> + </Tooltip> {/* Spacer that also focuses editor on click */} <div className="grow cursor-text self-stretch" diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index 89e94816..cea8300a 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -30,7 +30,7 @@ export function CopyLinkDateTimeComponent(props: { } return ( <div className={clsx('inline', className)}> - <DateTimeTooltip time={createdTime}> + <DateTimeTooltip time={createdTime} noTap> <Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}> <a onClick={(event) => copyLinkToComment(event)} diff --git a/web/components/info-tooltip.tsx b/web/components/info-tooltip.tsx index 0eabffed..1c12d8e2 100644 --- a/web/components/info-tooltip.tsx +++ b/web/components/info-tooltip.tsx @@ -1,10 +1,11 @@ import { InformationCircleIcon } from '@heroicons/react/outline' +import { Tooltip } from './tooltip' export function InfoTooltip(props: { text: string }) { const { text } = props return ( - <div className="tooltip" data-tip={text}> + <Tooltip text={text}> <InformationCircleIcon className="h-5 w-5 text-gray-500" /> - </div> + </Tooltip> ) } diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index a6c3a563..85e171d8 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -1,5 +1,4 @@ import clsx from 'clsx' -import { ReactNode } from 'react' import { Answer } from 'common/answer' import { getProbability } from 'common/calculate' import { getValueFromBucket } from 'common/calculate-dpm' @@ -11,7 +10,7 @@ import { resolution, } from 'common/contract' import { formatLargeNumber, formatPercent } from 'common/util/format' -import { ClientRender } from './client-render' +import { Tooltip } from './tooltip' export function OutcomeLabel(props: { contract: Contract @@ -91,13 +90,13 @@ export function FreeResponseOutcomeLabel(props: { const chosen = contract.answers?.find((answer) => answer.id === resolution) if (!chosen) return <AnswerNumberLabel number={resolution} /> return ( - <FreeResponseAnswerToolTip text={chosen.text}> + <Tooltip text={chosen.text}> <AnswerLabel answer={chosen} truncate={truncate} className={answerClassName} /> - </FreeResponseAnswerToolTip> + </Tooltip> ) } @@ -174,23 +173,3 @@ export function AnswerLabel(props: { </span> ) } - -function FreeResponseAnswerToolTip(props: { - text: string - children?: ReactNode -}) { - const { text } = props - return ( - <> - <ClientRender> - <span - className="tooltip hidden cursor-default sm:inline-block" - data-tip={text} - > - {props.children} - </span> - </ClientRender> - <span className="whitespace-nowrap sm:hidden">{props.children}</span> - </> - ) -} diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 1c76c3e7..dbacbee9 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -103,8 +103,10 @@ function DownTip(props: { onClick?: () => void }) { const { onClick } = props return ( <Tooltip - className="tooltip-bottom h-6 w-6" + className="h-6 w-6" + placement="bottom" text={onClick && `-${formatMoney(5)}`} + noTap > <button className="hover:text-red-600 disabled:text-gray-300" @@ -122,8 +124,10 @@ function UpTip(props: { onClick?: () => void; value: number }) { const IconKind = value >= 10 ? ChevronDoubleRightIcon : ChevronRightIcon return ( <Tooltip - className="tooltip-bottom h-6 w-6" + className="h-6 w-6" + placement="bottom" text={onClick && `Tip ${formatMoney(5)}`} + noTap > <button className="hover:text-primary disabled:text-gray-300" diff --git a/web/components/tooltip.tsx b/web/components/tooltip.tsx index 46d51762..ca77a29b 100644 --- a/web/components/tooltip.tsx +++ b/web/components/tooltip.tsx @@ -1,14 +1,79 @@ +import { + arrow, + autoUpdate, + flip, + offset, + Placement, + shift, + useFloating, +} from '@floating-ui/react-dom' import clsx from 'clsx' +import { ReactNode, useRef } from 'react' + +// See https://floating-ui.com/docs/react-dom + +export function Tooltip(props: { + text: string | false | undefined | null + children: ReactNode + className?: string + placement?: Placement + noTap?: boolean +}) { + const { text, children, className, placement = 'top', noTap } = props + + const arrowRef = useRef(null) + + const { x, y, refs, reference, floating, strategy, middlewareData } = + useFloating({ + whileElementsMounted: autoUpdate, + placement, + middleware: [ + offset(8), + flip(), + shift({ padding: 4 }), + arrow({ element: arrowRef }), + ], + }) + + const { x: arrowX, y: arrowY } = middlewareData.arrow ?? {} + + // which side of tooltip arrow is on. like: if tooltip is top-left, arrow is on bottom of tooltip + const arrowSide = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right ', + }[placement.split('-')[0]] as string -export function Tooltip( - props: { - text: string | false | undefined | null - } & JSX.IntrinsicElements['div'] -) { - const { text, children, className } = props return text ? ( - <div className={clsx(className, 'tooltip z-10')} data-tip={text}> - {children} + <div className="contents"> + <div + className={clsx('peer inline-block', className)} + ref={reference} + tabIndex={noTap ? undefined : 0} + onTouchStart={() => (refs.reference.current as HTMLElement).focus()} + > + {children} + </div> + <div + role="tooltip" + ref={floating} + style={{ position: strategy, top: y ?? 0, left: x ?? 0 }} + className="-z-10 max-w-xs rounded bg-slate-700 px-2 py-1 text-center text-sm text-white opacity-0 transition-opacity peer-hover:z-10 peer-hover:opacity-100 peer-focus:z-10 peer-focus:opacity-100" + > + {text} + <div + ref={arrowRef} + className="absolute h-2 w-2 rotate-45 bg-slate-700" + style={{ + top: arrowY != null ? arrowY : '', + left: arrowX != null ? arrowX : '', + right: '', + bottom: '', + [arrowSide]: '-4px', + }} + /> + </div> </div> ) : ( <>{children}</> diff --git a/yarn.lock b/yarn.lock index e83ffc0a..4966f7e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2178,6 +2178,25 @@ resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.1.tgz#0c74724ba6e9ea6ad25a391eab60a79eaba4c556" integrity sha512-9FqhNjKQWpQ3fGnSOCovHOm+yhhiorKEqYLAfd525jWavunDJcx8rOW6i6ozAh+FbwcYMkL7b+3j4UR/30MpoQ== +"@floating-ui/core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.1.tgz#00e64d74e911602c8533957af0cce5af6b2e93c8" + integrity sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA== + +"@floating-ui/dom@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.1.tgz#3321d4e799d6ac2503e729131d07ad0e714aabeb" + integrity sha512-wBDiLUKWU8QNPNOTAFHiIAkBv1KlHauG2AhqjSeh2H+wR8PX+AArXfz8NkRexH5PgMJMmSOS70YS89AbWYh5dA== + dependencies: + "@floating-ui/core" "^1.0.1" + +"@floating-ui/react-dom@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.0.0.tgz#e0975966694433f1f0abffeee5d8e6bb69b7d16e" + integrity sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg== + dependencies: + "@floating-ui/dom" "^1.0.0" + "@google-cloud/firestore@^4.5.0": version "4.15.1" resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-4.15.1.tgz#ed764fc76823ce120e68fe8c27ef1edd0650cd93" From 88535e551246e8c322e42a88a738c1941fceae33 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 12 Aug 2022 12:10:07 -0700 Subject: [PATCH 496/519] fix lint error --- web/components/answers/multiple-choice-answers.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/answers/multiple-choice-answers.tsx b/web/components/answers/multiple-choice-answers.tsx index fb0e8d03..c2857eb2 100644 --- a/web/components/answers/multiple-choice-answers.tsx +++ b/web/components/answers/multiple-choice-answers.tsx @@ -3,7 +3,6 @@ import Textarea from 'react-expanding-textarea' import { XIcon } from '@heroicons/react/solid' import { Col } from '../layout/col' import { Row } from '../layout/row' -import { Button } from '../button' export function MultipleChoiceAnswers(props: { answers: string[] From 20ab313c6c73e5b93d50b209702c096d84cd5e53 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 12 Aug 2022 12:10:45 -0700 Subject: [PATCH 497/519] Improve profile comments vis d --- web/components/comments-list.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index de4ea67f..304a213f 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -27,20 +27,22 @@ export function UserCommentsList(props: { {Object.entries(commentsByContract).map(([contractId, comments]) => { const contract = contractsById[contractId] return ( - <div key={contractId} className={'border-width-1 border-b p-5'}> + <div key={contractId} className="border-b p-5"> <SiteLink - className={'mb-2 block text-sm text-indigo-700'} + className="mb-2 block pb-2 font-medium text-indigo-700" href={contractPath(contract)} > {contract.question} </SiteLink> - {comments.map((comment) => ( - <ProfileComment - key={comment.id} - comment={comment} - className="relative flex items-start space-x-3 pb-6" - /> - ))} + <Col className="gap-6"> + {comments.map((comment) => ( + <ProfileComment + key={comment.id} + comment={comment} + className="relative flex items-start space-x-3" + /> + ))} + </Col> </div> ) })} From 3cbf5a6f7d0b9f21bb04a26ae175672decf5e95d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 12 Aug 2022 14:35:27 -0500 Subject: [PATCH 498/519] Always show search placeholder --- web/components/contract-search.tsx | 4 +--- web/components/editor/market-modal.tsx | 1 - web/pages/group/[...slugs]/index.tsx | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 40a62923..f9c48b78 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -59,7 +59,6 @@ export function ContractSearch(props: { } highlightOptions?: ContractHighlightOptions onContractClick?: (contract: Contract) => void - showPlaceHolder?: boolean hideOrderSelector?: boolean overrideGridClassName?: string cardHideOptions?: { @@ -75,7 +74,6 @@ export function ContractSearch(props: { onContractClick, overrideGridClassName, hideOrderSelector, - showPlaceHolder, cardHideOptions, highlightOptions, headerClassName, @@ -269,7 +267,7 @@ export function ContractSearch(props: { value={query} onChange={(e) => updateQuery(e.target.value)} onBlur={trackCallback('search', { query })} - placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} + placeholder={'Search'} className="input input-bordered w-full" /> {!query && ( diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx index 1c88afbc..0486b9e9 100644 --- a/web/components/editor/market-modal.tsx +++ b/web/components/editor/market-modal.tsx @@ -68,7 +68,6 @@ export function MarketModal(props: { overrideGridClassName={ 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' } - showPlaceHolder cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} querySortOptions={{ disableQueryString: true }} highlightOptions={{ diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index b9c8c277..0daf74c7 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -619,7 +619,6 @@ function AddContractButton(props: { group: Group; user: User }) { overrideGridClassName={ 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' } - showPlaceHolder={true} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} additionalFilter={{ excludeContractIds: group.contractIds }} highlightOptions={{ From 3cb28cdecb42009fef15f9dde768145c96e4e31c Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 13:41:00 -0700 Subject: [PATCH 499/519] Teach `AuthContext` to manage the private user doc (#738) * Return both user and privateUser from `createuser` * Make `useStateCheckEquality` more flexible * Make `AuthContext` track the private user doc * Change `usePrivateUser` hook to use the auth context data * Pass both user and private user through SSR to auth context * Fix bug in create user flow --- functions/src/create-user.ts | 2 +- web/components/auth-context.tsx | 53 ++++++++++++++++++--------- web/components/groups/group-chat.tsx | 2 +- web/components/nav/nav-bar.tsx | 2 +- web/components/nav/sidebar.tsx | 2 +- web/components/notifications-icon.tsx | 5 +-- web/hooks/use-admin.ts | 5 +-- web/hooks/use-state-check-equality.ts | 5 ++- web/hooks/use-user.ts | 28 ++++---------- web/lib/firebase/users.ts | 12 ++++++ web/pages/_app.tsx | 2 +- web/pages/create.tsx | 9 ++--- web/pages/home.tsx | 9 ++--- web/pages/links.tsx | 9 ++--- web/pages/notifications.tsx | 11 +++--- web/pages/profile.tsx | 14 ++----- 16 files changed, 89 insertions(+), 81 deletions(-) diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index c30e78c3..bd65b14a 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -98,7 +98,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => { await sendWelcomeEmail(user, privateUser) await track(auth.uid, 'create user', { username }, { ip: req.ip }) - return user + return { user, privateUser } }) const firestore = admin.firestore() diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 332c96be..f62c10a2 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -1,10 +1,11 @@ import { ReactNode, createContext, useEffect } from 'react' -import { User } from 'common/user' import { onIdTokenChanged } from 'firebase/auth' import { + UserAndPrivateUser, auth, listenForUser, - getUser, + listenForPrivateUser, + getUserAndPrivateUser, setCachedReferralInfoForUser, } from 'web/lib/firebase/users' import { deleteTokenCookies, setTokenCookies } from 'web/lib/firebase/auth' @@ -14,10 +15,10 @@ import { identifyUser, setUserProperty } from 'web/lib/service/analytics' import { useStateCheckEquality } from 'web/hooks/use-state-check-equality' // Either we haven't looked up the logged in user yet (undefined), or we know -// the user is not logged in (null), or we know the user is logged in (User). -type AuthUser = undefined | null | User +// the user is not logged in (null), or we know the user is logged in. +type AuthUser = undefined | null | UserAndPrivateUser -const CACHED_USER_KEY = 'CACHED_USER_KEY' +const CACHED_USER_KEY = 'CACHED_USER_KEY_V2' const ensureDeviceToken = () => { let deviceToken = localStorage.getItem('device-token') @@ -36,6 +37,7 @@ export function AuthProvider(props: { }) { const { children, serverUser } = props const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(serverUser) + useEffect(() => { if (serverUser === undefined) { const cachedUser = localStorage.getItem(CACHED_USER_KEY) @@ -50,16 +52,16 @@ export function AuthProvider(props: { id: await fbUser.getIdToken(), refresh: fbUser.refreshToken, }) - let user = await getUser(fbUser.uid) - if (!user) { + let current = await getUserAndPrivateUser(fbUser.uid) + if (!current.user || !current.privateUser) { const deviceToken = ensureDeviceToken() - user = (await createUser({ deviceToken })) as User + current = (await createUser({ deviceToken })) as UserAndPrivateUser } - setAuthUser(user) + setAuthUser(current) // Persist to local storage, to reduce login blink next time. // Note: Cap on localStorage size is ~5mb - localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user)) - setCachedReferralInfoForUser(user) + localStorage.setItem(CACHED_USER_KEY, JSON.stringify(current)) + setCachedReferralInfoForUser(current.user) } else { // User logged out; reset to null deleteTokenCookies() @@ -69,15 +71,30 @@ export function AuthProvider(props: { }) }, [setAuthUser]) - const authUserId = authUser?.id - const authUsername = authUser?.username + const uid = authUser?.user.id + const username = authUser?.user.username useEffect(() => { - if (authUserId && authUsername) { - identifyUser(authUserId) - setUserProperty('username', authUsername) - return listenForUser(authUserId, setAuthUser) + if (uid && username) { + identifyUser(uid) + setUserProperty('username', username) + const userListener = listenForUser(uid, (user) => + setAuthUser((authUser) => { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return { ...authUser!, user: user! } + }) + ) + const privateUserListener = listenForPrivateUser(uid, (privateUser) => { + setAuthUser((authUser) => { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return { ...authUser!, privateUser: privateUser! } + }) + }) + return () => { + userListener() + privateUserListener() + } } - }, [authUserId, authUsername, setAuthUser]) + }, [uid, username, setAuthUser]) return ( <AuthContext.Provider value={authUser}>{children}</AuthContext.Provider> diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 0f9e8955..4fd8c2fe 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -31,7 +31,7 @@ export function GroupChat(props: { }) { const { messages, user, group, tips } = props - const privateUser = usePrivateUser(user?.id) + const privateUser = usePrivateUser() const { editor, upload } = useTextEditor({ simple: true, diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 971aa89a..a935173a 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -44,7 +44,7 @@ export function BottomNavBar() { const currentPage = router.pathname const user = useUser() - const privateUser = usePrivateUser(user?.id) + const privateUser = usePrivateUser() const isIframe = useIsIframe() if (isIframe) { diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 3a3c932f..dfb7805e 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -221,7 +221,7 @@ export default function Sidebar(props: { className?: string }) { const currentPage = router.pathname const user = useUser() - const privateUser = usePrivateUser(user?.id) + const privateUser = usePrivateUser() // usePing(user?.id) const navigationOptions = !user ? signedOutNavigation : getNavigation() diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx index 0dfc5054..dbdad6a9 100644 --- a/web/components/notifications-icon.tsx +++ b/web/components/notifications-icon.tsx @@ -2,15 +2,14 @@ import { BellIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { Row } from 'web/components/layout/row' import { useEffect, useState } from 'react' -import { usePrivateUser, useUser } from 'web/hooks/use-user' +import { usePrivateUser } from 'web/hooks/use-user' import { useRouter } from 'next/router' import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' import { PrivateUser } from 'common/user' export default function NotificationsIcon(props: { className?: string }) { - const user = useUser() - const privateUser = usePrivateUser(user?.id) + const privateUser = usePrivateUser() return ( <Row className={clsx('justify-center')}> diff --git a/web/hooks/use-admin.ts b/web/hooks/use-admin.ts index a80b3ac4..551c588b 100644 --- a/web/hooks/use-admin.ts +++ b/web/hooks/use-admin.ts @@ -1,8 +1,7 @@ import { isAdmin } from 'common/envs/constants' -import { usePrivateUser, useUser } from './use-user' +import { usePrivateUser } from './use-user' export const useAdmin = () => { - const user = useUser() - const privateUser = usePrivateUser(user?.id) + const privateUser = usePrivateUser() return isAdmin(privateUser?.email || '') } diff --git a/web/hooks/use-state-check-equality.ts b/web/hooks/use-state-check-equality.ts index d8dc88dd..e5d97f56 100644 --- a/web/hooks/use-state-check-equality.ts +++ b/web/hooks/use-state-check-equality.ts @@ -1,5 +1,5 @@ import { isEqual } from 'lodash' -import { useMemo, useRef, useState } from 'react' +import { SetStateAction, useMemo, useRef, useState } from 'react' export const useStateCheckEquality = <T>(initialState: T) => { const [state, setState] = useState(initialState) @@ -8,8 +8,9 @@ export const useStateCheckEquality = <T>(initialState: T) => { stateRef.current = state const checkSetState = useMemo( - () => (newState: T) => { + () => (next: SetStateAction<T>) => { const state = stateRef.current + const newState = next instanceof Function ? next(state) : next if (!isEqual(state, newState)) { setState(newState) } diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index d84c7d03..b0cb1bc3 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -1,31 +1,19 @@ -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { QueryClient } from 'react-query' -import { doc, DocumentData, where } from 'firebase/firestore' -import { PrivateUser } from 'common/user' -import { - getUser, - listenForPrivateUser, - User, - users, -} from 'web/lib/firebase/users' +import { doc, DocumentData } from 'firebase/firestore' +import { getUser, User, users } from 'web/lib/firebase/users' import { AuthContext } from 'web/components/auth-context' export const useUser = () => { - return useContext(AuthContext) + const authUser = useContext(AuthContext) + return authUser ? authUser.user : authUser } -export const usePrivateUser = (userId?: string) => { - const [privateUser, setPrivateUser] = useState< - PrivateUser | null | undefined - >(undefined) - - useEffect(() => { - if (userId) return listenForPrivateUser(userId, setPrivateUser) - }, [userId]) - - return privateUser +export const usePrivateUser = () => { + const authUser = useContext(AuthContext) + return authUser ? authUser.privateUser : authUser } export const useUserById = (userId = '_') => { diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index cf2415ab..ac0eb099 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -43,6 +43,8 @@ export const privateUsers = coll<PrivateUser>('private-users') export type { User } +export type UserAndPrivateUser = { user: User; privateUser: PrivateUser } + export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime' export const auth = getAuth(app) @@ -57,6 +59,16 @@ export async function getPrivateUser(userId: string) { return (await getDoc(doc(privateUsers, userId))).data()! } +export async function getUserAndPrivateUser(userId: string) { + const [user, privateUser] = ( + await Promise.all([ + getDoc(doc(users, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + getDoc(doc(privateUsers, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + ]) + ).map((d) => d.data()) as [User, PrivateUser] + return { user, privateUser } as UserAndPrivateUser +} + export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. const q = query(users, where('username', '==', username), limit(1)) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 42b5e922..36524dc5 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -79,7 +79,7 @@ function MyApp({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1, maximum-scale=1" /> </Head> - <AuthProvider serverUser={pageProps.user}> + <AuthProvider serverUser={pageProps.authUser}> <QueryClientProvider client={queryClient}> <Welcome {...pageProps} /> <Component {...pageProps} /> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 3225fb4d..ab566c9e 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx' import dayjs from 'dayjs' import Textarea from 'react-expanding-textarea' import { Spacer } from 'web/components/layout/spacer' -import { getUser } from 'web/lib/firebase/users' +import { getUserAndPrivateUser } from 'web/lib/firebase/users' import { Contract, contractPath } from 'web/lib/firebase/contracts' import { createMarket } from 'web/lib/firebase/api' import { FIXED_ANTE } from 'common/antes' @@ -34,8 +34,7 @@ import { SEO } from 'web/components/SEO' import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers' export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const user = await getUser(creds.user.uid) - return { props: { user } } + return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } } }) type NewQuestionParams = { @@ -52,9 +51,9 @@ type NewQuestionParams = { initValue?: string } -export default function Create(props: { user: User }) { +export default function Create(props: { auth: { user: User } }) { useTracking('view create page') - const { user } = props + const { user } = props.auth const router = useRouter() const params = router.query as NewQuestionParams // TODO: Not sure why Question is pulled out as its own component; diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 10671c15..839a08f3 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -10,19 +10,18 @@ import { Contract } from 'common/contract' import { User } from 'common/user' import { ContractPageContent } from './[username]/[contractSlug]' import { getContractFromSlug } from 'web/lib/firebase/contracts' -import { getUser } from 'web/lib/firebase/users' +import { getUserAndPrivateUser } from 'web/lib/firebase/users' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { useSaveReferral } from 'web/hooks/use-save-referral' export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const user = await getUser(creds.user.uid) - return { props: { user } } + return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } } }) -const Home = (props: { user: User }) => { - const { user } = props +const Home = (props: { auth: { user: User } }) => { + const { user } = props.auth const [contract, setContract] = useContractPage() const router = useRouter() diff --git a/web/pages/links.tsx b/web/pages/links.tsx index 258c782a..351abefb 100644 --- a/web/pages/links.tsx +++ b/web/pages/links.tsx @@ -11,7 +11,7 @@ import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' import { Subtitle } from 'web/components/subtitle' -import { getUser } from 'web/lib/firebase/users' +import { getUserAndPrivateUser } from 'web/lib/firebase/users' import { useUserManalinks } from 'web/lib/firebase/manalinks' import { useUserById } from 'web/hooks/use-user' import { ManalinkTxn } from 'common/txn' @@ -31,16 +31,15 @@ import { SiteLink } from 'web/components/site-link' const LINKS_PER_PAGE = 24 export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const user = await getUser(creds.user.uid) - return { props: { user } } + return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } } }) export function getManalinkUrl(slug: string) { return `${location.protocol}//${location.host}/link/${slug}` } -export default function LinkPage(props: { user: User }) { - const { user } = props +export default function LinkPage(props: { auth: { user: User } }) { + const { user } = props.auth const links = useUserManalinks(user.id ?? '') // const manalinkTxns = useManalinkTxns(user?.id ?? '') const [highlightedSlug, setHighlightedSlug] = useState('') diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 69139f9c..c875dbf2 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -13,7 +13,7 @@ import { MANIFOLD_USERNAME, PrivateUser, } from 'common/user' -import { getPrivateUser } from 'web/lib/firebase/users' +import { getUserAndPrivateUser } from 'web/lib/firebase/users' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' import { Linkify } from 'web/components/linkify' @@ -46,12 +46,13 @@ const MULTIPLE_USERS_KEY = 'multipleUsers' const HIGHLIGHT_CLASS = 'bg-indigo-50' export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const privateUser = await getPrivateUser(creds.user.uid) - return { props: { privateUser } } + return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } } }) -export default function Notifications(props: { privateUser: PrivateUser }) { - const { privateUser } = props +export default function Notifications(props: { + auth: { privateUser: PrivateUser } +}) { + const { privateUser } = props.auth const local = safeLocalStorage() let localNotifications = [] as Notification[] const localSavedNotificationGroups = local?.getItem('notification-groups') diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 42bcb5c3..ca1f3489 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -13,8 +13,7 @@ import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { User, PrivateUser } from 'common/user' import { - getUser, - getPrivateUser, + getUserAndPrivateUser, updateUser, updatePrivateUser, } from 'web/lib/firebase/users' @@ -24,11 +23,7 @@ import Textarea from 'react-expanding-textarea' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { - const [user, privateUser] = await Promise.all([ - getUser(creds.user.uid), - getPrivateUser(creds.user.uid), - ]) - return { props: { user, privateUser } } + return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } } }) function EditUserField(props: { @@ -69,10 +64,9 @@ function EditUserField(props: { } export default function ProfilePage(props: { - user: User - privateUser: PrivateUser + auth: { user: User; privateUser: PrivateUser } }) { - const { user, privateUser } = props + const { user, privateUser } = props.auth const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '') const [avatarLoading, setAvatarLoading] = useState(false) const [name, setName] = useState(user.name) From 79be0c555b071b1fa537d4f877b80a0a6dec59c3 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 13:45:38 -0700 Subject: [PATCH 500/519] Fix tiny bug in auth context code --- web/pages/_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 36524dc5..bb620950 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -79,7 +79,7 @@ function MyApp({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1, maximum-scale=1" /> </Head> - <AuthProvider serverUser={pageProps.authUser}> + <AuthProvider serverUser={pageProps.auth}> <QueryClientProvider client={queryClient}> <Welcome {...pageProps} /> <Component {...pageProps} /> From 96a378ec4b92c64a1f9a3336304ad3a0e38ef106 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 17:48:41 -0700 Subject: [PATCH 501/519] Make `RelativeTimestamp` a little more efficient (#754) * Don't do extra dayjs work in timestamp components * Remove extra wrapper from `RelativeTimestamp` --- web/components/contract/contract-details.tsx | 25 ++++++++++---------- web/components/datetime-tooltip.tsx | 11 +++++---- web/components/feed/copy-link-date-time.tsx | 4 +++- web/components/relative-timestamp.tsx | 12 ++++++---- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 081b035d..4a9d40af 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -83,12 +83,10 @@ export function MiscDetails(props: { {!hideGroupLink && groupLinks && groupLinks.length > 0 && ( <SiteLink href={groupPath(groupLinks[0].slug)} - className="text-sm text-gray-400" + className="line-clamp-1 text-sm text-gray-400" > - <Row className={'line-clamp-1 flex-wrap items-center '}> - <UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" /> - {groupLinks[0].name} - </Row> + <UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" /> + {groupLinks[0].name} </SiteLink> )} </Row> @@ -211,7 +209,7 @@ export function ContractDetails(props: { <> <DateTimeTooltip text="Market resolved:" - time={contract.resolutionTime} + time={dayjs(contract.resolutionTime)} > {resolvedDate} </DateTimeTooltip> @@ -267,13 +265,16 @@ function EditableCloseDate(props: { }) { const { closeTime, contract, isCreator } = props + const dayJsCloseTime = dayjs(closeTime) + const dayJsNow = dayjs() + const [isEditingCloseTime, setIsEditingCloseTime] = useState(false) const [closeDate, setCloseDate] = useState( - closeTime && dayjs(closeTime).format('YYYY-MM-DDTHH:mm') + closeTime && dayJsCloseTime.format('YYYY-MM-DDTHH:mm') ) - const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year') - const isSameDay = dayjs(closeTime).isSame(dayjs(), 'day') + const isSameYear = dayJsCloseTime.isSame(dayJsNow, 'year') + const isSameDay = dayJsCloseTime.isSame(dayJsNow, 'day') const onSave = () => { const newCloseTime = dayjs(closeDate).valueOf() @@ -314,11 +315,11 @@ function EditableCloseDate(props: { ) : ( <DateTimeTooltip text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'} - time={closeTime} + time={dayJsCloseTime} > {isSameYear - ? dayjs(closeTime).format('MMM D') - : dayjs(closeTime).format('MMM D, YYYY')} + ? dayJsCloseTime.format('MMM D') + : dayJsCloseTime.format('MMM D, YYYY')} {isSameDay && <> ({fromNow(closeTime)})</>} </DateTimeTooltip> )} diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx index 7aaf61aa..d820e728 100644 --- a/web/components/datetime-tooltip.tsx +++ b/web/components/datetime-tooltip.tsx @@ -1,4 +1,4 @@ -import dayjs from 'dayjs' +import dayjs, { Dayjs } from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' import advanced from 'dayjs/plugin/advancedFormat' @@ -9,18 +9,19 @@ dayjs.extend(timezone) dayjs.extend(advanced) export function DateTimeTooltip(props: { - time: number + time: Dayjs text?: string + className?: string children?: React.ReactNode noTap?: boolean }) { - const { time, text, noTap } = props + const { className, time, text, noTap } = props - const formattedTime = dayjs(time).format('MMM DD, YYYY hh:mm a z') + const formattedTime = time.format('MMM DD, YYYY hh:mm a z') const toolTip = text ? `${text} ${formattedTime}` : formattedTime return ( - <Tooltip text={toolTip} noTap={noTap}> + <Tooltip className={className} text={toolTip} noTap={noTap}> {props.children} </Tooltip> ) diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index cea8300a..8238d3e3 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -7,6 +7,7 @@ import { fromNow } from 'web/lib/util/time' import { ToastClipboard } from 'web/components/toast-clipboard' import { LinkIcon } from '@heroicons/react/outline' import clsx from 'clsx' +import dayjs from 'dayjs' export function CopyLinkDateTimeComponent(props: { prefix: string @@ -17,6 +18,7 @@ export function CopyLinkDateTimeComponent(props: { }) { const { prefix, slug, elementId, createdTime, className } = props const [showToast, setShowToast] = useState(false) + const time = dayjs(createdTime) function copyLinkToComment( event: React.MouseEvent<HTMLAnchorElement, MouseEvent> @@ -30,7 +32,7 @@ export function CopyLinkDateTimeComponent(props: { } return ( <div className={clsx('inline', className)}> - <DateTimeTooltip time={createdTime} noTap> + <DateTimeTooltip time={time} noTap> <Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}> <a onClick={(event) => copyLinkToComment(event)} diff --git a/web/components/relative-timestamp.tsx b/web/components/relative-timestamp.tsx index 160a665d..bd029cf6 100644 --- a/web/components/relative-timestamp.tsx +++ b/web/components/relative-timestamp.tsx @@ -1,14 +1,16 @@ import { DateTimeTooltip } from './datetime-tooltip' -import { fromNow } from 'web/lib/util/time' +import dayjs from 'dayjs' import React from 'react' export function RelativeTimestamp(props: { time: number }) { const { time } = props + const dayJsTime = dayjs(time) return ( - <DateTimeTooltip time={time}> - <span className="ml-1 whitespace-nowrap text-gray-400"> - {fromNow(time)} - </span> + <DateTimeTooltip + className="ml-1 whitespace-nowrap text-gray-400" + time={dayJsTime} + > + {dayJsTime.fromNow()} </DateTimeTooltip> ) } From facb19a347f002ebb13cbcc45d2054cbb4b55dac Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 12 Aug 2022 17:49:08 -0700 Subject: [PATCH 502/519] fix dependency peer-dep warnings, mostly (#752) --- common/package.json | 1 + docs/package.json | 3 ++- functions/package.json | 1 - package.json | 1 + web/package.json | 5 +++++ yarn.lock | 4 ++-- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/common/package.json b/common/package.json index c324379f..955e9662 100644 --- a/common/package.json +++ b/common/package.json @@ -8,6 +8,7 @@ }, "sideEffects": false, "dependencies": { + "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-mention": "2.0.0-beta.102", diff --git a/docs/package.json b/docs/package.json index 9e320306..38b69777 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,7 +30,8 @@ }, "devDependencies": { "@docusaurus/module-type-aliases": "2.0.0-beta.17", - "@tsconfig/docusaurus": "^1.0.4" + "@tsconfig/docusaurus": "^1.0.4", + "@types/react": "^17.0.2" }, "browserslist": { "production": [ diff --git a/functions/package.json b/functions/package.json index b6c1bb5c..5839b5eb 100644 --- a/functions/package.json +++ b/functions/package.json @@ -25,7 +25,6 @@ "main": "functions/src/index.js", "dependencies": { "@amplitude/node": "1.10.0", - "@floating-ui/react-dom": "1.0.0", "@google-cloud/functions-framework": "3.1.2", "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-image": "2.0.0-beta.30", diff --git a/package.json b/package.json index 77420607..05924ef0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "5.25.0", "@typescript-eslint/parser": "5.25.0", + "@types/node": "16.11.11", "concurrently": "6.5.1", "eslint": "8.15.0", "eslint-plugin-lodash": "^7.4.0", diff --git a/web/package.json b/web/package.json index a008026b..dc7d06f5 100644 --- a/web/package.json +++ b/web/package.json @@ -21,11 +21,14 @@ }, "dependencies": { "@amplitude/analytics-browser": "0.4.1", + "@floating-ui/react-dom": "1.0.0", "@headlessui/react": "1.6.1", "@heroicons/react": "1.0.5", "@nivo/core": "0.74.0", "@nivo/line": "0.74.0", + "@nivo/tooltip": "0.74.0", "@react-query-firebase/firestore": "0.4.2", + "@tiptap/core": "2.0.0-beta.181", "@tiptap/extension-character-count": "2.0.0-beta.31", "@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-link": "2.0.0-beta.43", @@ -68,10 +71,12 @@ "autoprefixer": "10.2.6", "critters": "0.0.16", "cross-env": "^7.0.3", + "csstype": "^3.1.0", "eslint-config-next": "12.1.6", "next-sitemap": "^2.5.14", "postcss": "8.3.5", "prettier-plugin-tailwindcss": "^0.1.5", + "prop-types": "^15.8.1", "tailwindcss": "3.1.6", "tsc-files": "1.1.3" }, diff --git a/yarn.lock b/yarn.lock index 4966f7e9..1e33761e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3404,7 +3404,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@17.0.43": +"@types/react@*", "@types/react@17.0.43", "@types/react@^17.0.2": version "17.0.43" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== @@ -5037,7 +5037,7 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" -csstype@^3.0.2: +csstype@^3.0.2, csstype@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== From e4239d0122b9d1e7269a468a6d48bde5186a9fd9 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 20:13:09 -0700 Subject: [PATCH 503/519] Tweak Firestore user rules to be more robust (#750) --- firestore.rules | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/firestore.rules b/firestore.rules index b0befc85..81ab4eed 100644 --- a/firestore.rules +++ b/firestore.rules @@ -20,17 +20,17 @@ service cloud.firestore { match /users/{userId} { allow read; - allow update: if resource.data.id == request.auth.uid + allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome']); // User referral rules - allow update: if resource.data.id == request.auth.uid + allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId']) // only one referral allowed per user && !("referredByUserId" in resource.data) // user can't refer themselves - && !(resource.data.id == request.resource.data.referredByUserId); + && !(userId == request.resource.data.referredByUserId); // quid pro quos enabled (only once though so nbd) - bc I can't make this work: // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); } @@ -60,8 +60,8 @@ service cloud.firestore { } match /private-users/{userId} { - allow read: if resource.data.id == request.auth.uid || isAdmin(); - allow update: if (resource.data.id == request.auth.uid || isAdmin()) + allow read: if userId == request.auth.uid || isAdmin(); + allow update: if (userId == request.auth.uid || isAdmin()) && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]); } From 0f7f55ec0a7e4f31a172ad2bd0fccef30c82f146 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 20:14:10 -0700 Subject: [PATCH 504/519] Fix embarrassing bug in server auth --- web/lib/firebase/server-auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts index 8e5336c8..ebcb23d4 100644 --- a/web/lib/firebase/server-auth.ts +++ b/web/lib/firebase/server-auth.ts @@ -179,7 +179,7 @@ export const redirectIfLoggedIn = <P>( if (fn == null) { return { props: {} } } else { - const props = fn(ctx) + const props = await fn(ctx) console.debug('Finished getting initial props for rendering.') return props } @@ -203,7 +203,7 @@ export const redirectIfLoggedOut = <P>( if (fn == null) { return { props: {} } } else { - const props = fn(ctx, creds) + const props = await fn(ctx, creds) console.debug('Finished getting initial props for rendering.') return props } From dcc3c61f528a0edb75aa447aadf5ace70f26bc26 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 12 Aug 2022 20:35:08 -0700 Subject: [PATCH 505/519] Only calculate position when tooltip is shown (#755) --- web/components/tooltip.tsx | 40 ++++++++++++++++++++++++++++++-------- web/package.json | 2 +- yarn.lock | 19 ++++++++++++++++-- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/web/components/tooltip.tsx b/web/components/tooltip.tsx index ca77a29b..db6a934f 100644 --- a/web/components/tooltip.tsx +++ b/web/components/tooltip.tsx @@ -6,9 +6,14 @@ import { Placement, shift, useFloating, -} from '@floating-ui/react-dom' + useFocus, + useHover, + useInteractions, + useRole, +} from '@floating-ui/react-dom-interactions' +import { Transition } from '@headlessui/react' import clsx from 'clsx' -import { ReactNode, useRef } from 'react' +import { ReactNode, useRef, useState } from 'react' // See https://floating-ui.com/docs/react-dom @@ -23,8 +28,12 @@ export function Tooltip(props: { const arrowRef = useRef(null) - const { x, y, refs, reference, floating, strategy, middlewareData } = + const [open, setOpen] = useState(false) + + const { x, y, reference, floating, strategy, middlewareData, context } = useFloating({ + open, + onOpenChange: setOpen, whileElementsMounted: autoUpdate, placement, middleware: [ @@ -37,6 +46,11 @@ export function Tooltip(props: { const { x: arrowX, y: arrowY } = middlewareData.arrow ?? {} + const { getReferenceProps, getFloatingProps } = useInteractions([ + useHover(context, { mouseOnly: noTap }), + useFocus(context), + useRole(context, { role: 'tooltip' }), + ]) // which side of tooltip arrow is on. like: if tooltip is top-left, arrow is on bottom of tooltip const arrowSide = { top: 'bottom', @@ -48,18 +62,28 @@ export function Tooltip(props: { return text ? ( <div className="contents"> <div - className={clsx('peer inline-block', className)} + className={clsx('inline-block', className)} ref={reference} tabIndex={noTap ? undefined : 0} - onTouchStart={() => (refs.reference.current as HTMLElement).focus()} + {...getReferenceProps()} > {children} </div> - <div + {/* conditionally render tooltip and fade in/out */} + <Transition + show={open} + enter="transition ease-out duration-200" + enterFrom="opacity-0 " + enterTo="opacity-100" + leave="transition ease-in duration-150" + leaveFrom="opacity-100" + leaveTo="opacity-0" + // div attributes role="tooltip" ref={floating} style={{ position: strategy, top: y ?? 0, left: x ?? 0 }} - className="-z-10 max-w-xs rounded bg-slate-700 px-2 py-1 text-center text-sm text-white opacity-0 transition-opacity peer-hover:z-10 peer-hover:opacity-100 peer-focus:z-10 peer-focus:opacity-100" + className="z-10 max-w-xs rounded bg-slate-700 px-2 py-1 text-center text-sm text-white" + {...getFloatingProps()} > {text} <div @@ -73,7 +97,7 @@ export function Tooltip(props: { [arrowSide]: '-4px', }} /> - </div> + </Transition> </div> ) : ( <>{children}</> diff --git a/web/package.json b/web/package.json index dc7d06f5..bcec0091 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@amplitude/analytics-browser": "0.4.1", - "@floating-ui/react-dom": "1.0.0", + "@floating-ui/react-dom-interactions": "0.9.2", "@headlessui/react": "1.6.1", "@heroicons/react": "1.0.5", "@nivo/core": "0.74.0", diff --git a/yarn.lock b/yarn.lock index 1e33761e..8949f390 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2190,7 +2190,15 @@ dependencies: "@floating-ui/core" "^1.0.1" -"@floating-ui/react-dom@1.0.0": +"@floating-ui/react-dom-interactions@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.9.2.tgz#9a364cc44ecbc242b5218dff0e0d071de115e13a" + integrity sha512-1I0urs4jlGuo4FRukvjtMmdUwxqvgwtTlESEPVwEvFGHXVh1PKkKaPZJ0Dcp9B8DQt4ewQEbwJxsoker2pDYTQ== + dependencies: + "@floating-ui/react-dom" "^1.0.0" + aria-hidden "^1.1.3" + +"@floating-ui/react-dom@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.0.0.tgz#e0975966694433f1f0abffeee5d8e6bb69b7d16e" integrity sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg== @@ -3935,6 +3943,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254" + integrity sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA== + dependencies: + tslib "^1.0.0" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -11314,7 +11329,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== From 456d9398a105b0421696988bb1c4dd0c9fae4844 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Fri, 12 Aug 2022 20:42:58 -0700 Subject: [PATCH 506/519] Revamp a lot of stuff on the user page to make it usably efficient (#751) * Load bets and comments tabs data on user page independently * Implement basic pagination on profile comments list * Tweak server auth to return `null` instead of `undefined` * Switch to SSR for user page * Fix lint * Fix broken contract fetching in user bets list * Tidying --- common/util/array.ts | 19 +++++ web/components/bets-list.tsx | 42 +++++++--- web/components/comments-list.tsx | 66 ++++++++++++---- web/components/user-page.tsx | 132 +++++++------------------------ web/lib/firebase/server-auth.ts | 9 ++- web/pages/[username]/index.tsx | 45 ++++------- 6 files changed, 154 insertions(+), 159 deletions(-) diff --git a/common/util/array.ts b/common/util/array.ts index 2ad86843..8a429262 100644 --- a/common/util/array.ts +++ b/common/util/array.ts @@ -17,3 +17,22 @@ export function buildArray<T>( return array } + +export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) { + if (!xs.length) { + return [] + } + const result = [] + let curr = { key: key(xs[0]), items: [xs[0]] } + for (const x of xs.slice(1)) { + const k = key(x) + if (k !== curr.key) { + result.push(curr) + curr = { key: k, items: [x] } + } else { + curr.items.push(x) + } + } + result.push(curr) + return result +} diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 18349597..b919cccd 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -1,5 +1,14 @@ import Link from 'next/link' -import { groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' +import { + Dictionary, + keyBy, + groupBy, + mapValues, + sortBy, + partition, + sumBy, + uniq, +} from 'lodash' import dayjs from 'dayjs' import { useEffect, useMemo, useState } from 'react' import clsx from 'clsx' @@ -19,6 +28,7 @@ import { Contract, contractPath, getBinaryProbPercent, + getContractFromId, } from 'web/lib/firebase/contracts' import { Row } from './layout/row' import { UserLink } from './user-page' @@ -41,10 +51,12 @@ import { trackLatency } from 'web/lib/firebase/tracking' 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 { filterDefined } from 'common/util/array' import { Pagination } from './pagination' import { LimitOrderTable } from './limit-bets' @@ -52,25 +64,35 @@ type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' const CONTRACTS_PER_PAGE = 50 +const JUNE_1_2022 = new Date('2022-06-01T00:00:00.000Z').valueOf() -export function BetsList(props: { - user: User - bets: Bet[] | undefined - contractsById: { [id: string]: Contract } | undefined - hideBetsBefore?: number -}) { - const { user, bets: allBets, contractsById, hideBetsBefore } = props +export function BetsList(props: { user: User }) { + const { user } = props const signedInUser = useUser() const isYourBets = user.id === signedInUser?.id + const hideBetsBefore = isYourBets ? 0 : JUNE_1_2022 + const userBets = useUserBets(user.id, { includeRedemptions: true }) + const [contractsById, setContractsById] = useState< + Dictionary<Contract> | undefined + >() // Hide bets before 06-01-2022 if this isn't your own profile // NOTE: This means public profits also begin on 06-01-2022 as well. const bets = useMemo( - () => allBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)), - [allBets, hideBetsBefore] + () => userBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)), + [userBets, hideBetsBefore] ) + useEffect(() => { + if (bets) { + const contractIds = uniq(bets.map((b) => b.contractId)) + Promise.all(contractIds.map(getContractFromId)).then((contracts) => { + setContractsById(keyBy(filterDefined(contracts), 'id')) + }) + } + }, [bets]) + const [sort, setSort] = useState<BetSort>('newest') const [filter, setFilter] = useState<BetFilter>('open') const [page, setPage] = useState(0) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index 304a213f..90542d4b 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -1,6 +1,12 @@ +import { useEffect, useState } from 'react' +import { Dictionary, keyBy, uniq } from 'lodash' + import { Comment } from 'common/comment' import { Contract } from 'common/contract' +import { filterDefined, groupConsecutive } from 'common/util/array' import { contractPath } from 'web/lib/firebase/contracts' +import { getUsersComments } from 'web/lib/firebase/comments' +import { getContractFromId } from 'web/lib/firebase/contracts' import { SiteLink } from './site-link' import { Row } from './layout/row' import { Avatar } from './avatar' @@ -8,26 +14,52 @@ import { RelativeTimestamp } from './relative-timestamp' import { UserLink } from './user-page' import { User } from 'common/user' import { Col } from './layout/col' -import { groupBy } from 'lodash' import { Content } from './editor' +import { Pagination } from './pagination' +import { LoadingIndicator } from './loading-indicator' -export function UserCommentsList(props: { - user: User - comments: Comment[] - contractsById: { [id: string]: Contract } -}) { - const { comments, contractsById } = props +const COMMENTS_PER_PAGE = 50 - // we don't show comments in groups here atm, just comments on contracts - const contractComments = comments.filter((c) => c.contractId) - const commentsByContract = groupBy(contractComments, 'contractId') +type ContractComment = Comment & { contractId: string } +export function UserCommentsList(props: { user: User }) { + const { user } = props + const [comments, setComments] = useState<ContractComment[] | undefined>() + const [contracts, setContracts] = useState<Dictionary<Contract> | undefined>() + const [page, setPage] = useState(0) + const start = page * COMMENTS_PER_PAGE + const end = start + COMMENTS_PER_PAGE + + useEffect(() => { + getUsersComments(user.id).then((cs) => { + // we don't show comments in groups here atm, just comments on contracts + setComments(cs.filter((c) => c.contractId) as ContractComment[]) + }) + }, [user.id]) + + useEffect(() => { + if (comments) { + const contractIds = uniq(comments.map((c) => c.contractId)) + Promise.all(contractIds.map(getContractFromId)).then((contracts) => { + setContracts(keyBy(filterDefined(contracts), 'id')) + }) + } + }, [comments]) + + if (comments == null || contracts == null) { + return <LoadingIndicator /> + } + + const pageComments = groupConsecutive( + comments.slice(start, end), + (c) => c.contractId + ) return ( <Col className={'bg-white'}> - {Object.entries(commentsByContract).map(([contractId, comments]) => { - const contract = contractsById[contractId] + {pageComments.map(({ key, items }, i) => { + const contract = contracts[key] return ( - <div key={contractId} className="border-b p-5"> + <div key={start + i} className="border-b p-5"> <SiteLink className="mb-2 block pb-2 font-medium text-indigo-700" href={contractPath(contract)} @@ -35,7 +67,7 @@ export function UserCommentsList(props: { {contract.question} </SiteLink> <Col className="gap-6"> - {comments.map((comment) => ( + {items.map((comment) => ( <ProfileComment key={comment.id} comment={comment} @@ -46,6 +78,12 @@ export function UserCommentsList(props: { </div> ) })} + <Pagination + page={page} + itemsPerPage={COMMENTS_PER_PAGE} + totalItems={comments.length} + setPage={setPage} + /> </Col> ) } diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 6a901b13..2069ef72 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -1,11 +1,11 @@ import clsx from 'clsx' -import { Dictionary, keyBy, uniq } from 'lodash' import { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' import { PencilIcon } from '@heroicons/react/outline' import { User } from 'web/lib/firebase/users' +import { useUser } from 'web/hooks/use-user' import { CreatorContractsList } from './contract/contracts-grid' import { SEO } from './SEO' import { Page } from './page' @@ -18,18 +18,12 @@ import { Row } from './layout/row' import { genHash } from 'common/util/random' import { QueryUncontrolledTabs } from './layout/tabs' import { UserCommentsList } from './comments-list' -import { Comment, getUsersComments } from 'web/lib/firebase/comments' -import { Contract } from 'common/contract' -import { getContractFromId, listContracts } from 'web/lib/firebase/contracts' -import { LoadingIndicator } from './loading-indicator' import { FullscreenConfetti } from 'web/components/fullscreen-confetti' import { BetsList } from './bets-list' import { FollowersButton, FollowingButton } from './following-button' import { UserFollowButton } from './follow-button' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' -import { filterDefined } from 'common/util/array' -import { useUserBets } from 'web/hooks/use-user-bets' import { ReferralsButton } from 'web/components/referrals-button' import { formatMoney } from 'common/util/format' import { ShareIconButton } from 'web/components/share-icon-button' @@ -56,26 +50,13 @@ export function UserLink(props: { } export const TAB_IDS = ['markets', 'comments', 'bets', 'groups'] -const JUNE_1_2022 = new Date('2022-06-01T00:00:00.000Z').valueOf() -export function UserPage(props: { user: User; currentUser?: User }) { - const { user, currentUser } = props +export function UserPage(props: { user: User }) { + const { user } = props const router = useRouter() + const currentUser = useUser() const isCurrentUser = user.id === currentUser?.id const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) - const [usersComments, setUsersComments] = useState<Comment[] | undefined>() - const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>( - 'loading' - ) - const userBets = useUserBets(user.id, { includeRedemptions: true }) - const betCount = - userBets === undefined - ? 0 - : userBets.filter((bet) => !bet.isRedemption && bet.amount !== 0).length - - const [contractsById, setContractsById] = useState< - Dictionary<Contract> | undefined - >() const [showConfetti, setShowConfetti] = useState(false) useEffect(() => { @@ -83,30 +64,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { setShowConfetti(claimedMana) }, [router]) - useEffect(() => { - if (!user) return - getUsersComments(user.id).then(setUsersComments) - listContracts(user.id).then(setUsersContracts) - }, [user]) - - // TODO: display comments on groups - useEffect(() => { - if (usersComments && userBets) { - const uniqueContractIds = uniq([ - ...usersComments.map((comment) => comment.contractId), - ...(userBets?.map((bet) => bet.contractId) ?? []), - ]) - Promise.all( - uniqueContractIds.map((contractId) => - contractId ? getContractFromId(contractId) : undefined - ) - ).then((contracts) => { - const contractsById = keyBy(filterDefined(contracts), 'id') - setContractsById(contractsById) - }) - } - }, [userBets, usersComments]) - const profit = user.profitCached.allTime return ( @@ -163,9 +120,7 @@ export function UserPage(props: { user: User; currentUser?: User }) { </span>{' '} profit </span> - <Spacer h={4} /> - {user.bio && ( <> <div> @@ -174,7 +129,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { <Spacer h={4} /> </> )} - <Col className="flex-wrap gap-2 sm:flex-row sm:items-center sm:gap-4"> <Row className="gap-4"> <FollowingButton user={user} /> @@ -236,7 +190,6 @@ export function UserPage(props: { user: User; currentUser?: User }) { </SiteLink> )} </Col> - <Spacer h={5} /> {currentUser?.id === user.id && ( <Row @@ -259,58 +212,31 @@ export function UserPage(props: { user: User; currentUser?: User }) { </Row> )} <Spacer h={5} /> - - {usersContracts !== 'loading' && contractsById && usersComments ? ( - <QueryUncontrolledTabs - currentPageForAnalytics={'profile'} - labelClassName={'pb-2 pt-1 '} - tabs={[ - { - title: 'Markets', - content: ( - <CreatorContractsList user={currentUser} creator={user} /> - ), - tabIcon: ( - <span className="px-0.5 font-bold"> - {usersContracts.length} - </span> - ), - }, - { - title: 'Comments', - content: ( - <UserCommentsList - user={user} - contractsById={contractsById} - comments={usersComments} - /> - ), - tabIcon: ( - <span className="px-0.5 font-bold"> - {usersComments.length} - </span> - ), - }, - { - title: 'Bets', - content: ( - <div> - <PortfolioValueSection userId={user.id} /> - <BetsList - user={user} - bets={userBets} - hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022} - contractsById={contractsById} - /> - </div> - ), - tabIcon: <span className="px-0.5 font-bold">{betCount}</span>, - }, - ]} - /> - ) : ( - <LoadingIndicator /> - )} + <QueryUncontrolledTabs + currentPageForAnalytics={'profile'} + labelClassName={'pb-2 pt-1 '} + tabs={[ + { + title: 'Markets', + content: ( + <CreatorContractsList user={currentUser} creator={user} /> + ), + }, + { + title: 'Comments', + content: <UserCommentsList user={user} />, + }, + { + title: 'Bets', + content: ( + <> + <PortfolioValueSection userId={user.id} /> + <BetsList user={user} /> + </> + ), + }, + ]} + /> </Col> </Page> ) diff --git a/web/lib/firebase/server-auth.ts b/web/lib/firebase/server-auth.ts index ebcb23d4..7ce8a814 100644 --- a/web/lib/firebase/server-auth.ts +++ b/web/lib/firebase/server-auth.ts @@ -81,7 +81,7 @@ const authAndRefreshTokens = async (ctx: RequestContext) => { // step 0: if you have no refresh token you are logged out if (refresh == null) { console.debug('User is unauthenticated.') - return undefined + return null } console.debug('User may be authenticated; checking cookies.') @@ -107,7 +107,7 @@ const authAndRefreshTokens = async (ctx: RequestContext) => { } catch (e) { // big unexpected problem -- functionally, they are not logged in console.error(e) - return undefined + return null } } @@ -136,9 +136,10 @@ const authAndRefreshTokens = async (ctx: RequestContext) => { } catch (e) { // big unexpected problem -- functionally, they are not logged in console.error(e) - return undefined + return null } } + return null } export const authenticateOnServer = async (ctx: RequestContext) => { @@ -158,7 +159,7 @@ export const authenticateOnServer = async (ctx: RequestContext) => { // definitely not supposed to happen, but let's be maximally robust console.error(e) } - return creds + return creds ?? null } // note that we might want to define these types more generically if we want better diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index 22083c90..bf6e8442 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -1,48 +1,37 @@ import { useRouter } from 'next/router' import React from 'react' -import { getUserByUsername, User } from 'web/lib/firebase/users' +import { + getUserByUsername, + getUserAndPrivateUser, + User, + UserAndPrivateUser, +} from 'web/lib/firebase/users' import { UserPage } from 'web/components/user-page' -import { useUser } from 'web/hooks/use-user' import Custom404 from '../404' import { useTracking } from 'web/hooks/use-tracking' -import { fromPropz, usePropz } from 'web/hooks/use-propz' +import { GetServerSideProps } from 'next' +import { authenticateOnServer } from 'web/lib/firebase/server-auth' -export const getStaticProps = fromPropz(getStaticPropz) -export async function getStaticPropz(props: { params: { username: string } }) { - const { username } = props.params - const user = await getUserByUsername(username) - - return { - props: { - user, - }, - - revalidate: 60, // regenerate after a minute - } -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const creds = await authenticateOnServer(ctx) + const username = ctx.params!.username as string // eslint-disable-line @typescript-eslint/no-non-null-assertion + const [auth, user] = (await Promise.all([ + creds != null ? getUserAndPrivateUser(creds.user.uid) : null, + getUserByUsername(username), + ])) as [UserAndPrivateUser | null, User | null] + return { props: { auth, user } } } export default function UserProfile(props: { user: User | null }) { - props = usePropz(props, getStaticPropz) ?? { user: undefined } const { user } = props const router = useRouter() const { username } = router.query as { username: string } - const currentUser = useUser() useTracking('view user profile', { username }) - if (user === undefined) return <div /> - - return user ? ( - <UserPage user={user} currentUser={currentUser || undefined} /> - ) : ( - <Custom404 /> - ) + return user ? <UserPage user={user} /> : <Custom404 /> } From aeea66491a26ad6381026575202e54d196d24df4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 13 Aug 2022 13:49:25 -0500 Subject: [PATCH 507/519] Group question => market --- web/pages/group/[...slugs]/index.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 0daf74c7..fdd75949 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,5 +1,4 @@ import { debounce, sortBy, take } from 'lodash' -import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon' import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Page } from 'web/components/page' @@ -38,7 +37,6 @@ import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' import { REFERRAL_AMOUNT } from 'common/user' import { ContractSearch } from 'web/components/contract-search' -import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' @@ -557,32 +555,29 @@ function AddContractButton(props: { group: Group; user: User }) { return ( <> <div className={'flex justify-center'}> - <button - className={clsx('btn btn-sm btn-outline')} - onClick={() => setOpen(true)} - > - <PlusSmIcon className="h-6 w-6" aria-hidden="true" /> question - </button> + <Button size="sm" color="gradient" onClick={() => setOpen(true)}> + Add market + </Button> </div> <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> <Col className={' w-full gap-4 rounded-md bg-white'}> <Col className="p-8 pb-0"> <div className={'text-xl text-indigo-700'}> - Add a question to your group + Add a market to your group </div> {contracts.length === 0 ? ( <Col className="items-center justify-center"> <CreateQuestionButton user={user} - overrideText={'New question'} + overrideText={'New market'} className={'w-48 flex-shrink-0 '} query={`?groupId=${group.id}`} /> <div className={'mt-1 text-lg text-gray-600'}> - (or select old questions) + (or select old markets) </div> </Col> ) : ( From 0a9df3ac6b46ea76d6f8ebeda224964b66161d72 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 13 Aug 2022 13:50:26 -0500 Subject: [PATCH 508/519] Group horizontal margin on tabs --- web/pages/group/[...slugs]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index fdd75949..cd4b7344 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -266,7 +266,7 @@ export default function GroupPage(props: { </Col> <Tabs currentPageForAnalytics={groupPath(group.slug)} - className={'mb-0 sm:mb-2'} + className={'mx-2 mb-0 sm:mb-2'} defaultIndex={tabIndex > 0 ? tabIndex : 0} tabs={tabs} /> From 0085ffcb0b61345dc1de593dd06c9748bd2768b9 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 13 Aug 2022 13:15:11 -0700 Subject: [PATCH 509/519] Simplify and fix inefficiencies in contract search component (#756) * Simplify and fix inefficiencies in contract search component * Add react-dom types * Add a clarifying comment * Improve search per some feedback --- web/components/contract-search.tsx | 87 ++++++++++++------------------ web/package.json | 1 + yarn.lock | 7 +++ 3 files changed, 43 insertions(+), 52 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index f9c48b78..464cb7f7 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -13,7 +13,8 @@ import { ContractsGrid, } from './contract/contracts-grid' import { Row } from './layout/row' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useRef, useMemo, useState } from 'react' +import { unstable_batchedUpdates } from 'react-dom' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useFollows } from 'web/hooks/use-follows' import { track, trackCallback } from 'web/lib/service/analytics' @@ -21,7 +22,7 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { Group, NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' -import { range, sortBy } from 'lodash' +import { sortBy } from 'lodash' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { Col } from './layout/col' import clsx from 'clsx' @@ -111,7 +112,6 @@ export function ContractSearch(props: { const selectPill = (pill: string | undefined) => () => { setPillFilter(pill) - setPage(0) track('select search category', { category: pill ?? 'all' }) } @@ -167,79 +167,62 @@ export function ContractSearch(props: { [searchIndexName] ) - const [page, setPage] = useState(0) const [numPages, setNumPages] = useState(1) - const [hitsByPage, setHitsByPage] = useState<{ [page: string]: Contract[] }>( - {} - ) + const [pages, setPages] = useState<Contract[][]>([]) + const requestId = useRef(0) - useEffect(() => { - let wasMostRecentQuery = true - const algoliaIndex = query ? searchIndex : index - - algoliaIndex - .search(query, { + const performQuery = async (freshQuery?: boolean) => { + const id = ++requestId.current + const requestedPage = freshQuery ? 0 : pages.length + if (freshQuery || requestedPage < numPages) { + const algoliaIndex = query ? searchIndex : index + const results = await algoliaIndex.search(query, { facetFilters, numericFilters, - page, + page: requestedPage, hitsPerPage: 20, }) - .then((results) => { - if (!wasMostRecentQuery) return - - if (page === 0) { - setHitsByPage({ - [0]: results.hits as any as Contract[], - }) - } else { - setHitsByPage((hitsByPage) => ({ - ...hitsByPage, - [page]: results.hits, - })) - } - setNumPages(results.nbPages) - }) - return () => { - wasMostRecentQuery = false + // if there's a more recent request, forget about this one + if (id === requestId.current) { + const newPage = results.hits as any as Contract[] + // this spooky looking function is the easiest way to get react to + // batch this and not do two renders. we can throw it out in react 18. + // see https://github.com/reactwg/react-18/discussions/21 + unstable_batchedUpdates(() => { + setNumPages(results.nbPages) + if (freshQuery) { + setPages([newPage]) + } else { + setPages((pages) => [...pages, newPage]) + } + }) + } } - // Note numeric filters are unique based on current time, so can't compare - // them by value. - }, [query, page, index, searchIndex, JSON.stringify(facetFilters), filter]) - - const loadMore = () => { - if (page >= numPages - 1) return - - const haveLoadedCurrentPage = hitsByPage[page] - if (haveLoadedCurrentPage) setPage(page + 1) } - const hits = range(0, page + 1) - .map((p) => hitsByPage[p] ?? []) - .flat() + useEffect(() => { + performQuery(true) + }, [query, index, searchIndex, filter, JSON.stringify(facetFilters)]) - const contracts = hits.filter( - (c) => !additionalFilter?.excludeContractIds?.includes(c.id) - ) + const contracts = pages + .flat() + .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) const showTime = sort === 'close-date' || sort === 'resolve-date' ? sort : undefined const updateQuery = (newQuery: string) => { setQuery(newQuery) - setPage(0) } const selectFilter = (newFilter: filter) => { if (newFilter === filter) return setFilter(newFilter) - setPage(0) track('select search filter', { filter: newFilter }) } const selectSort = (newSort: Sort) => { if (newSort === sort) return - - setPage(0) setSort(newSort) track('select search sort', { sort: newSort }) } @@ -345,8 +328,8 @@ export function ContractSearch(props: { <>You're not following anyone, nor in any of your own groups yet.</> ) : ( <ContractsGrid - contracts={hitsByPage[0] === undefined ? undefined : contracts} - loadMore={loadMore} + contracts={pages.length === 0 ? undefined : contracts} + loadMore={performQuery} showTime={showTime} onContractClick={onContractClick} overrideGridClassName={overrideGridClassName} diff --git a/web/package.json b/web/package.json index bcec0091..3ac3c0a1 100644 --- a/web/package.json +++ b/web/package.json @@ -67,6 +67,7 @@ "@types/lodash": "4.14.178", "@types/node": "16.11.11", "@types/react": "17.0.43", + "@types/react-dom": "17.0.2", "@types/string-similarity": "^4.0.0", "autoprefixer": "10.2.6", "critters": "0.0.16", diff --git a/yarn.lock b/yarn.lock index 8949f390..85b7e0a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3386,6 +3386,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/react-dom@17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.2.tgz#35654cf6c49ae162d5bc90843d5437dc38008d43" + integrity sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg== + dependencies: + "@types/react" "*" + "@types/react-router-config@*": version "5.0.6" resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451" From 69c49679f194b15852dc61f770e453d89800d1ec Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sat, 13 Aug 2022 16:34:03 -0700 Subject: [PATCH 510/519] Move search controls into separate component (#757) * Move search controls into separate component * Fix up typing on pill groups thingy * More precise comparison per James * Make sure `additionalFilter` is passed into controls --- web/components/contract-search.tsx | 397 ++++++++++++++++------------- 1 file changed, 219 insertions(+), 178 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 464cb7f7..98debc9f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import algoliasearch from 'algoliasearch/lite' +import algoliasearch, { SearchIndex } from 'algoliasearch/lite' +import { SearchOptions } from '@algolia/client-search' import { Contract } from 'common/contract' import { User } from 'common/user' @@ -12,6 +13,7 @@ import { ContractHighlightOptions, ContractsGrid, } from './contract/contracts-grid' +import { ShowTime } from './contract/contract-details' import { Row } from './layout/row' import { useEffect, useRef, useMemo, useState } from 'react' import { unstable_batchedUpdates } from 'react-dom' @@ -20,7 +22,7 @@ import { useFollows } from 'web/hooks/use-follows' import { track, trackCallback } from 'web/lib/service/analytics' import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' -import { Group, NEW_USER_GROUP_SLUGS } from 'common/group' +import { NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' import { sortBy } from 'lodash' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' @@ -49,15 +51,25 @@ export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' +type SearchParameters = { + index: SearchIndex + query: string + numericFilters: SearchOptions['numericFilters'] + facetFilters: SearchOptions['facetFilters'] + showTime?: ShowTime +} + +type AdditionalFilter = { + creatorId?: string + tag?: string + excludeContractIds?: string[] + groupSlug?: string +} + export function ContractSearch(props: { user?: User | null querySortOptions?: { defaultFilter?: filter } & QuerySortOptions - additionalFilter?: { - creatorId?: string - tag?: string - excludeContractIds?: string[] - groupSlug?: string - } + additionalFilter?: AdditionalFilter highlightOptions?: ContractHighlightOptions onContractClick?: (contract: Contract) => void hideOrderSelector?: boolean @@ -80,6 +92,106 @@ export function ContractSearch(props: { headerClassName, } = props + const [numPages, setNumPages] = useState(1) + const [pages, setPages] = useState<Contract[][]>([]) + const [showTime, setShowTime] = useState<ShowTime | undefined>() + + const searchParameters = useRef<SearchParameters | undefined>() + const requestId = useRef(0) + + const performQuery = async (freshQuery?: boolean) => { + if (searchParameters.current === undefined) { + return + } + const params = searchParameters.current + const id = ++requestId.current + const requestedPage = freshQuery ? 0 : pages.length + if (freshQuery || requestedPage < numPages) { + const results = await params.index.search(params.query, { + facetFilters: params.facetFilters, + numericFilters: params.numericFilters, + page: requestedPage, + hitsPerPage: 20, + }) + // if there's a more recent request, forget about this one + if (id === requestId.current) { + const newPage = results.hits as any as Contract[] + // this spooky looking function is the easiest way to get react to + // batch this and not do multiple renders. we can throw it out in react 18. + // see https://github.com/reactwg/react-18/discussions/21 + unstable_batchedUpdates(() => { + setShowTime(params.showTime) + setNumPages(results.nbPages) + if (freshQuery) { + setPages([newPage]) + } else { + setPages((pages) => [...pages, newPage]) + } + }) + } + } + } + + const contracts = pages + .flat() + .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) + + if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { + return ( + <ContractSearchFirestore + querySortOptions={querySortOptions} + additionalFilter={additionalFilter} + /> + ) + } + + return ( + <Col className="h-full"> + <ContractSearchControls + className={headerClassName} + additionalFilter={additionalFilter} + hideOrderSelector={hideOrderSelector} + querySortOptions={querySortOptions} + user={user} + onSearchParametersChanged={(params) => { + searchParameters.current = params + performQuery(true) + }} + /> + <ContractsGrid + contracts={pages.length === 0 ? undefined : contracts} + loadMore={performQuery} + showTime={showTime} + onContractClick={onContractClick} + overrideGridClassName={overrideGridClassName} + highlightOptions={highlightOptions} + cardHideOptions={cardHideOptions} + /> + </Col> + ) +} + +function ContractSearchControls(props: { + className?: string + additionalFilter?: AdditionalFilter + hideOrderSelector?: boolean + onSearchParametersChanged: (params: SearchParameters) => void + querySortOptions?: { defaultFilter?: filter } & QuerySortOptions + user?: User | null +}) { + const { + className, + additionalFilter, + hideOrderSelector, + onSearchParametersChanged, + querySortOptions, + user, + } = props + + const { query, setQuery, sort, setSort } = + useQueryAndSortParams(querySortOptions) + + const follows = useFollows(user?.id) const memberGroups = (useMemberGroups(user?.id) ?? []).filter( (group) => !NEW_USER_GROUP_SLUGS.includes(group.slug) ) @@ -93,28 +205,15 @@ export function ContractSearch(props: { (group) => group.contractIds.length ).reverse() - const defaultPillGroups = DEFAULT_CATEGORY_GROUPS as Group[] - - const pillGroups = - memberPillGroups.length > 0 ? memberPillGroups : defaultPillGroups - - const follows = useFollows(user?.id) - - const { query, setQuery, sort, setSort } = - useQueryAndSortParams(querySortOptions) + const pillGroups: { name: string; slug: string }[] = + memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS const [filter, setFilter] = useState<filter>( querySortOptions?.defaultFilter ?? 'open' ) - const pillsEnabled = !additionalFilter && !query const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) - const selectPill = (pill: string | undefined) => () => { - setPillFilter(pill) - track('select search category', { category: pill ?? 'all' }) - } - const additionalFilters = [ additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` @@ -160,57 +259,11 @@ export function ContractSearch(props: { filter === 'closed' ? `closeTime <= ${Date.now()}` : '', ].filter((f) => f) - const indexName = `${indexPrefix}contracts-${sort}` - const index = useMemo(() => searchClient.initIndex(indexName), [indexName]) - const searchIndex = useMemo( - () => searchClient.initIndex(searchIndexName), - [searchIndexName] - ) - - const [numPages, setNumPages] = useState(1) - const [pages, setPages] = useState<Contract[][]>([]) - const requestId = useRef(0) - - const performQuery = async (freshQuery?: boolean) => { - const id = ++requestId.current - const requestedPage = freshQuery ? 0 : pages.length - if (freshQuery || requestedPage < numPages) { - const algoliaIndex = query ? searchIndex : index - const results = await algoliaIndex.search(query, { - facetFilters, - numericFilters, - page: requestedPage, - hitsPerPage: 20, - }) - // if there's a more recent request, forget about this one - if (id === requestId.current) { - const newPage = results.hits as any as Contract[] - // this spooky looking function is the easiest way to get react to - // batch this and not do two renders. we can throw it out in react 18. - // see https://github.com/reactwg/react-18/discussions/21 - unstable_batchedUpdates(() => { - setNumPages(results.nbPages) - if (freshQuery) { - setPages([newPage]) - } else { - setPages((pages) => [...pages, newPage]) - } - }) - } - } + const selectPill = (pill: string | undefined) => () => { + setPillFilter(pill) + track('select search category', { category: pill ?? 'all' }) } - useEffect(() => { - performQuery(true) - }, [query, index, searchIndex, filter, JSON.stringify(facetFilters)]) - - const contracts = pages - .flat() - .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) - - const showTime = - sort === 'close-date' || sort === 'resolve-date' ? sort : undefined - const updateQuery = (newQuery: string) => { setQuery(newQuery) } @@ -227,115 +280,103 @@ export function ContractSearch(props: { track('select search sort', { sort: newSort }) } - if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { - return ( - <ContractSearchFirestore - querySortOptions={querySortOptions} - additionalFilter={additionalFilter} - /> - ) - } + const indexName = `${indexPrefix}contracts-${sort}` + const index = useMemo(() => searchClient.initIndex(indexName), [indexName]) + const searchIndex = useMemo( + () => searchClient.initIndex(searchIndexName), + [searchIndexName] + ) + + useEffect(() => { + onSearchParametersChanged({ + index: query ? searchIndex : index, + query: query, + numericFilters: numericFilters, + facetFilters: facetFilters, + showTime: + sort === 'close-date' || sort === 'resolve-date' ? sort : undefined, + }) + }, [query, index, searchIndex, filter, JSON.stringify(facetFilters)]) return ( - <Col className="h-full"> - <Col - className={clsx( - 'bg-base-200 sticky top-0 z-20 gap-3 pb-3', - headerClassName - )} - > - <Row className="gap-1 sm:gap-2"> - <input - type="text" - value={query} - onChange={(e) => updateQuery(e.target.value)} - onBlur={trackCallback('search', { query })} - placeholder={'Search'} - className="input input-bordered w-full" - /> - {!query && ( - <select - className="select select-bordered" - value={filter} - onChange={(e) => selectFilter(e.target.value as filter)} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> - )} - {!hideOrderSelector && !query && ( - <select - className="select select-bordered" - value={sort} - onChange={(e) => selectSort(e.target.value as Sort)} - > - {sortOptions.map((option) => ( - <option key={option.value} value={option.value}> - {option.label} - </option> - ))} - </select> - )} - </Row> - - {pillsEnabled && ( - <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> - <PillButton - key={'all'} - selected={pillFilter === undefined} - onSelect={selectPill(undefined)} - > - All - </PillButton> - <PillButton - key={'personal'} - selected={pillFilter === 'personal'} - onSelect={selectPill('personal')} - > - {user ? 'For you' : 'Featured'} - </PillButton> - - {user && ( - <PillButton - key={'your-bets'} - selected={pillFilter === 'your-bets'} - onSelect={selectPill('your-bets')} - > - Your bets - </PillButton> - )} - - {pillGroups.map(({ name, slug }) => { - return ( - <PillButton - key={slug} - selected={pillFilter === slug} - onSelect={selectPill(slug)} - > - {name} - </PillButton> - ) - })} - </Row> - )} - </Col> - - {filter === 'personal' && - (follows ?? []).length === 0 && - memberGroupSlugs.length === 0 ? ( - <>You're not following anyone, nor in any of your own groups yet.</> - ) : ( - <ContractsGrid - contracts={pages.length === 0 ? undefined : contracts} - loadMore={performQuery} - showTime={showTime} - onContractClick={onContractClick} - overrideGridClassName={overrideGridClassName} - highlightOptions={highlightOptions} - cardHideOptions={cardHideOptions} + <Col + className={clsx('bg-base-200 sticky top-0 z-20 gap-3 pb-3', className)} + > + <Row className="gap-1 sm:gap-2"> + <input + type="text" + value={query} + onChange={(e) => updateQuery(e.target.value)} + onBlur={trackCallback('search', { query })} + placeholder={'Search'} + className="input input-bordered w-full" /> + {!query && ( + <select + className="select select-bordered" + value={filter} + onChange={(e) => selectFilter(e.target.value as filter)} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + )} + {!hideOrderSelector && !query && ( + <select + className="select select-bordered" + value={sort} + onChange={(e) => selectSort(e.target.value as Sort)} + > + {sortOptions.map((option) => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </select> + )} + </Row> + + {!additionalFilter && !query && ( + <Row className="scrollbar-hide items-start gap-2 overflow-x-auto"> + <PillButton + key={'all'} + selected={pillFilter === undefined} + onSelect={selectPill(undefined)} + > + All + </PillButton> + <PillButton + key={'personal'} + selected={pillFilter === 'personal'} + onSelect={selectPill('personal')} + > + {user ? 'For you' : 'Featured'} + </PillButton> + + {user && ( + <PillButton + key={'your-bets'} + selected={pillFilter === 'your-bets'} + onSelect={selectPill('your-bets')} + > + Your bets + </PillButton> + )} + + {pillGroups.map(({ name, slug }) => { + return ( + <PillButton + key={slug} + selected={pillFilter === slug} + onSelect={selectPill(slug)} + > + {name} + </PillButton> + ) + })} + </Row> )} </Col> ) From 0b711be4802c73652892011c990aa8297f3c97bf Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 14 Aug 2022 01:05:17 -0700 Subject: [PATCH 511/519] Clean up a bunch of markup and CSS on contract cards (#753) * Remove random unnecessary top-level divs * Remove wrapper in MiscDetails * Remove another wrapper in ContractCard * Fix a bunch of weird CSS stuff --- web/components/contract/contract-card.tsx | 197 ++++++++++------------ web/components/contract/quick-bet.tsx | 2 +- 2 files changed, 94 insertions(+), 105 deletions(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index ac1a2fa2..b4f20a40 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -66,115 +66,104 @@ export function ContractCard(props: { !hideQuickBet return ( - <div> - <Col - className={clsx( - 'relative gap-3 rounded-lg bg-white py-4 pl-6 pr-5 shadow-md hover:cursor-pointer hover:bg-gray-100', - className - )} - > - <Row> - <Col className="relative flex-1 gap-3 pr-1"> - <div - className={clsx( - 'peer absolute -left-6 -top-4 -bottom-4 z-10', - hideQuickBet ? '-right-20' : 'right-0' - )} - > - {onClick ? ( - <a - className="absolute top-0 left-0 right-0 bottom-0" - href={contractPath(contract)} - onClick={(e) => { - // Let the browser handle the link click (opens in new tab). - if (e.ctrlKey || e.metaKey) return + <Row + className={clsx( + 'relative gap-3 self-start rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100', + className + )} + > + <Col className="group relative flex-1 gap-3 py-4 pl-6"> + {onClick ? ( + <a + className="absolute top-0 left-0 right-0 bottom-0" + href={contractPath(contract)} + onClick={(e) => { + // Let the browser handle the link click (opens in new tab). + if (e.ctrlKey || e.metaKey) return - e.preventDefault() - track('click market card', { - slug: contract.slug, - contractId: contract.id, - }) - onClick() - }} - /> - ) : ( - <Link href={contractPath(contract)}> - <a - onClick={trackCallback('click market card', { - slug: contract.slug, - contractId: contract.id, - })} - className="absolute top-0 left-0 right-0 bottom-0" - /> - </Link> - )} - </div> - <AvatarDetails contract={contract} /> - <p - className="break-words font-semibold text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2" - style={{ /* For iOS safari */ wordBreak: 'break-word' }} - > - {question} - </p> - - {(outcomeType === 'FREE_RESPONSE' || - outcomeType === 'MULTIPLE_CHOICE') && - (resolution ? ( - <FreeResponseOutcomeLabel - contract={contract} - resolution={resolution} - truncate={'long'} - /> - ) : ( - <FreeResponseTopAnswer contract={contract} truncate="long" /> - ))} - - <MiscDetails - contract={contract} - showHotVolume={showHotVolume} - showTime={showTime} - hideGroupLink={hideGroupLink} + e.preventDefault() + track('click market card', { + slug: contract.slug, + contractId: contract.id, + }) + onClick() + }} + /> + ) : ( + <Link href={contractPath(contract)}> + <a + onClick={trackCallback('click market card', { + slug: contract.slug, + contractId: contract.id, + })} + className="absolute top-0 left-0 right-0 bottom-0" + /> + </Link> + )} + <AvatarDetails contract={contract} /> + <p + className="break-words font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2" + style={{ /* For iOS safari */ wordBreak: 'break-word' }} + > + {question} + </p> + + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && + (resolution ? ( + <FreeResponseOutcomeLabel + contract={contract} + resolution={resolution} + truncate={'long'} /> - </Col> - {showQuickBet ? ( - <QuickBet contract={contract} user={user} /> ) : ( - <Col className="m-auto pl-2"> - {outcomeType === 'BINARY' && ( - <BinaryResolutionOrChance - className="items-center" - contract={contract} - /> - )} + <FreeResponseTopAnswer contract={contract} truncate="long" /> + ))} - {outcomeType === 'PSEUDO_NUMERIC' && ( - <PseudoNumericResolutionOrExpectation - className="items-center" - contract={contract} - /> - )} - - {outcomeType === 'NUMERIC' && ( - <NumericResolutionOrExpectation - className="items-center" - contract={contract} - /> - )} - - {(outcomeType === 'FREE_RESPONSE' || - outcomeType === 'MULTIPLE_CHOICE') && ( - <FreeResponseResolutionOrChance - className="self-end text-gray-600" - contract={contract} - truncate="long" - /> - )} - <ProbBar contract={contract} /> - </Col> - )} - </Row> + <MiscDetails + contract={contract} + showHotVolume={showHotVolume} + showTime={showTime} + hideGroupLink={hideGroupLink} + /> </Col> - </div> + {showQuickBet ? ( + <QuickBet contract={contract} user={user} /> + ) : ( + <> + {outcomeType === 'BINARY' && ( + <BinaryResolutionOrChance + className="items-center self-center pr-5" + contract={contract} + /> + )} + + {outcomeType === 'PSEUDO_NUMERIC' && ( + <PseudoNumericResolutionOrExpectation + className="items-center self-center pr-5" + contract={contract} + /> + )} + + {outcomeType === 'NUMERIC' && ( + <NumericResolutionOrExpectation + className="items-center self-center pr-5" + contract={contract} + /> + )} + + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && ( + <FreeResponseResolutionOrChance + className="items-center self-center pr-5 text-gray-600" + contract={contract} + truncate="long" + /> + )} + <ProbBar contract={contract} /> + </> + )} + </Row> ) } diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 482aea47..92cee018 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -138,7 +138,7 @@ export function QuickBet(props: { return ( <Col className={clsx( - 'relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle' + 'relative min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle' // Use this for colored QuickBet panes // `bg-opacity-10 bg-${color}` )} From 4e1fae5b5f2e75bd379d4688bf8761c270b1d5d8 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 14 Aug 2022 20:51:10 -0500 Subject: [PATCH 512/519] Require a whole percentage for limitProb in back end --- functions/src/place-bet.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 7501309a..8fb5179d 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -30,7 +30,15 @@ const bodySchema = z.object({ const binarySchema = z.object({ outcome: z.enum(['YES', 'NO']), - limitProb: z.number().gte(0.001).lte(0.999).optional(), + limitProb: z + .number() + .gte(0.001) + .lte(0.999) + .refine( + (p) => Math.round(p * 100) === p * 100, + 'limitProb must be in increments of 0.01 (i.e. whole percentage points)' + ) + .optional(), }) const freeResponseSchema = z.object({ From b57c84bbd9861aff25bae1f58fb8dc8fc74054ae Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Sun, 14 Aug 2022 23:55:11 -0500 Subject: [PATCH 513/519] notifications title/seo --- web/pages/notifications.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index c875dbf2..a729aca1 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -40,6 +40,7 @@ import { safeLocalStorage } from 'web/lib/util/local' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { SiteLink } from 'web/components/site-link' import { NotificationSettings } from 'web/components/NotificationSettings' +import { SEO } from 'web/components/SEO' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' @@ -68,6 +69,8 @@ export default function Notifications(props: { <Page> <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> <Title text={'Notifications'} className={'hidden md:block'} /> + <SEO title="Notifications" description="Manifold user notifications" /> + <div> <Tabs currentPageForAnalytics={'notifications'} From 5d14d79e6e7eab47f530b97ff8b67979841013c7 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 15 Aug 2022 00:03:05 -0500 Subject: [PATCH 514/519] share dialog: remove native sharer; use toast for embed --- web/components/contract/share-modal.tsx | 17 ++++-------- web/components/share-embed-button.tsx | 36 ++++++++----------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index c462e78b..e1805364 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -51,17 +51,10 @@ export function ShareModal(props: { color="gradient" className={'mb-2 flex max-w-xs self-center'} onClick={() => { - if (window.navigator.share) { - window.navigator.share({ - url: shareUrl, - title: contract.question, - }) - } else { - copyToClipboard(shareUrl) - toast.success('Link copied!', { - icon: linkIcon, - }) - } + copyToClipboard(shareUrl) + toast.success('Link copied!', { + icon: linkIcon, + }) track('copy share link') }} > @@ -73,7 +66,7 @@ export function ShareModal(props: { className="self-start" tweetText={getTweetText(contract, shareUrl)} /> - <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> + <ShareEmbedButton contract={contract} /> <DuplicateContractButton contract={contract} /> </Row> </Col> diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 8678299b..cfbe78f0 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -1,12 +1,12 @@ -import React, { Fragment } from 'react' +import React from 'react' import { CodeIcon } from '@heroicons/react/outline' -import { Menu, Transition } from '@headlessui/react' +import { Menu } from '@headlessui/react' +import toast from 'react-hot-toast' import { Contract } from 'common/contract' import { contractPath } from 'web/lib/firebase/contracts' import { DOMAIN } from 'common/envs/constants' import { copyToClipboard } from 'web/lib/util/copy' -import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' export function embedCode(contract: Contract) { @@ -16,11 +16,10 @@ export function embedCode(contract: Contract) { return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` } -export function ShareEmbedButton(props: { - contract: Contract - toastClassName?: string -}) { - const { contract, toastClassName } = props +export function ShareEmbedButton(props: { contract: Contract }) { + const { contract } = props + + const codeIcon = <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> return ( <Menu @@ -28,6 +27,9 @@ export function ShareEmbedButton(props: { className="relative z-10 flex-shrink-0" onMouseUp={() => { copyToClipboard(embedCode(contract)) + toast.success('Embed code copied!', { + icon: codeIcon, + }) track('copy embed code') }} > @@ -39,25 +41,9 @@ export function ShareEmbedButton(props: { color: '#9ca3af', // text-gray-400 }} > - <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> + {codeIcon} Embed </Menu.Button> - - <Transition - as={Fragment} - enter="transition ease-out duration-100" - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leave="transition ease-in duration-75" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" - > - <Menu.Items> - <Menu.Item> - <ToastClipboard className={toastClassName} /> - </Menu.Item> - </Menu.Items> - </Transition> </Menu> ) } From 972f215f0c2703a3c9d35b33a5bbc5c1a87a219b Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Sun, 14 Aug 2022 22:09:25 -0700 Subject: [PATCH 515/519] Rewrite `useQueryAndSortParams` machinery to be faster/simpler/better (#758) * Rewrite useQueryAndSortParams machinery to be faster/simpler/better * Politely debounce Algolia querying * Tidy some stuff up * Style changes suggested by James --- web/components/contract-search.tsx | 88 +++++++++----- web/components/contract/contracts-grid.tsx | 7 +- web/components/editor/market-modal.tsx | 1 - web/hooks/use-sort-and-query-params.tsx | 133 +++++++-------------- web/pages/contract-search-firestore.tsx | 14 +-- web/pages/group/[...slugs]/index.tsx | 8 +- web/pages/home.tsx | 9 +- web/pages/tag/[tag].tsx | 7 +- 8 files changed, 117 insertions(+), 150 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 98debc9f..54b30f3f 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -4,11 +4,7 @@ import { SearchOptions } from '@algolia/client-search' import { Contract } from 'common/contract' import { User } from 'common/user' -import { - QuerySortOptions, - Sort, - useQueryAndSortParams, -} from '../hooks/use-sort-and-query-params' +import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, ContractsGrid, @@ -24,11 +20,25 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { NEW_USER_GROUP_SLUGS } from 'common/group' import { PillButton } from './buttons/pill-button' -import { sortBy } from 'lodash' +import { debounce, sortBy } from 'lodash' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { Col } from './layout/col' +import { safeLocalStorage } from 'web/lib/util/local' import clsx from 'clsx' +// TODO: this obviously doesn't work with SSR, common sense would suggest +// that we should save things like this in cookies so the server has them + +const MARKETS_SORT = 'markets_sort' + +function setSavedSort(s: Sort) { + safeLocalStorage()?.setItem(MARKETS_SORT, s) +} + +function getSavedSort() { + return safeLocalStorage()?.getItem(MARKETS_SORT) as Sort | null | undefined +} + const searchClient = algoliasearch( 'GJQPAYENIF', '75c28fc084a80e1129d427d470cf41a3' @@ -47,7 +57,6 @@ const sortOptions = [ { label: 'Close date', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, ] -export const DEFAULT_SORT = 'score' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' @@ -68,7 +77,8 @@ type AdditionalFilter = { export function ContractSearch(props: { user?: User | null - querySortOptions?: { defaultFilter?: filter } & QuerySortOptions + defaultSort?: Sort + defaultFilter?: filter additionalFilter?: AdditionalFilter highlightOptions?: ContractHighlightOptions onContractClick?: (contract: Contract) => void @@ -79,10 +89,13 @@ export function ContractSearch(props: { hideQuickBet?: boolean } headerClassName?: string + useQuerySortLocalStorage?: boolean + useQuerySortUrlParams?: boolean }) { const { user, - querySortOptions, + defaultSort, + defaultFilter, additionalFilter, onContractClick, overrideGridClassName, @@ -90,6 +103,8 @@ export function ContractSearch(props: { cardHideOptions, highlightOptions, headerClassName, + useQuerySortLocalStorage, + useQuerySortUrlParams, } = props const [numPages, setNumPages] = useState(1) @@ -132,31 +147,33 @@ export function ContractSearch(props: { } } + const onSearchParametersChanged = useRef( + debounce((params) => { + searchParameters.current = params + performQuery(true) + }, 100) + ).current + const contracts = pages .flat() .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { - return ( - <ContractSearchFirestore - querySortOptions={querySortOptions} - additionalFilter={additionalFilter} - /> - ) + return <ContractSearchFirestore additionalFilter={additionalFilter} /> } return ( <Col className="h-full"> <ContractSearchControls className={headerClassName} + defaultSort={defaultSort} + defaultFilter={defaultFilter} additionalFilter={additionalFilter} hideOrderSelector={hideOrderSelector} - querySortOptions={querySortOptions} + useQuerySortLocalStorage={useQuerySortLocalStorage} + useQuerySortUrlParams={useQuerySortUrlParams} user={user} - onSearchParametersChanged={(params) => { - searchParameters.current = params - performQuery(true) - }} + onSearchParametersChanged={onSearchParametersChanged} /> <ContractsGrid contracts={pages.length === 0 ? undefined : contracts} @@ -173,23 +190,40 @@ export function ContractSearch(props: { function ContractSearchControls(props: { className?: string + defaultSort?: Sort + defaultFilter?: filter additionalFilter?: AdditionalFilter hideOrderSelector?: boolean onSearchParametersChanged: (params: SearchParameters) => void - querySortOptions?: { defaultFilter?: filter } & QuerySortOptions + useQuerySortLocalStorage?: boolean + useQuerySortUrlParams?: boolean user?: User | null }) { const { className, + defaultSort, + defaultFilter, additionalFilter, hideOrderSelector, onSearchParametersChanged, - querySortOptions, + useQuerySortLocalStorage, + useQuerySortUrlParams, user, } = props - const { query, setQuery, sort, setSort } = - useQueryAndSortParams(querySortOptions) + const savedSort = useQuerySortLocalStorage ? getSavedSort() : null + const initialSort = savedSort ?? defaultSort ?? 'score' + const querySortOpts = { useUrl: !!useQuerySortUrlParams } + const [sort, setSort] = useSort(initialSort, querySortOpts) + const [query, setQuery] = useQuery('', querySortOpts) + const [filter, setFilter] = useState<filter>(defaultFilter ?? 'open') + const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) + + useEffect(() => { + if (useQuerySortLocalStorage) { + setSavedSort(sort) + } + }, [sort]) const follows = useFollows(user?.id) const memberGroups = (useMemberGroups(user?.id) ?? []).filter( @@ -208,12 +242,6 @@ function ContractSearchControls(props: { const pillGroups: { name: string; slug: string }[] = memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS - const [filter, setFilter] = useState<filter>( - querySortOptions?.defaultFilter ?? 'open' - ) - - const [pillFilter, setPillFilter] = useState<string | undefined>(undefined) - const additionalFilters = [ additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index f62c3c85..05c66d56 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -106,11 +106,8 @@ export function CreatorContractsList(props: { return ( <ContractSearch user={user} - querySortOptions={{ - defaultSort: 'newest', - defaultFilter: 'all', - shouldLoadFromStorage: false, - }} + defaultSort="newest" + defaultFilter="all" additionalFilter={{ creatorId: creator.id, }} diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx index 0486b9e9..85b7a978 100644 --- a/web/components/editor/market-modal.tsx +++ b/web/components/editor/market-modal.tsx @@ -69,7 +69,6 @@ export function MarketModal(props: { 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' } cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} - querySortOptions={{ disableQueryString: true }} highlightOptions={{ contractIds: contracts.map((c) => c.id), highlightClassName: diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index e917e4af..0a2834d0 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -1,9 +1,5 @@ -import { debounce } from 'lodash' -import { useRouter } from 'next/router' -import { useEffect, useMemo, useState } from 'react' -import { DEFAULT_SORT } from 'web/components/contract-search' - -const MARKETS_SORT = 'markets_sort' +import { useState } from 'react' +import { NextRouter, useRouter } from 'next/router' export type Sort = | 'newest' @@ -15,92 +11,55 @@ export type Sort = | 'last-updated' | 'score' -export function getSavedSort() { - // TODO: this obviously doesn't work with SSR, common sense would suggest - // that we should save things like this in cookies so the server has them - if (typeof window !== 'undefined') { - return localStorage.getItem(MARKETS_SORT) as Sort | null - } else { - return null +type UpdatedQueryParams = { [k: string]: string } +type QuerySortOpts = { useUrl: boolean } + +function withURLParams(location: Location, params: UpdatedQueryParams) { + const newParams = new URLSearchParams(location.search) + for (const [k, v] of Object.entries(params)) { + if (!v) { + newParams.delete(k) + } else { + newParams.set(k, v) + } } + const newUrl = new URL(location.href) + newUrl.search = newParams.toString() + return newUrl } -export interface QuerySortOptions { - defaultSort?: Sort - shouldLoadFromStorage?: boolean - /** Use normal react state instead of url query string */ - disableQueryString?: boolean +function updateURL(params: UpdatedQueryParams) { + // see relevant discussion here https://github.com/vercel/next.js/discussions/18072 + const url = withURLParams(window.location, params).toString() + const updatedState = { ...window.history.state, as: url, url } + window.history.replaceState(updatedState, '', url) } -export function useQueryAndSortParams({ - defaultSort = DEFAULT_SORT, - shouldLoadFromStorage = true, - disableQueryString, -}: QuerySortOptions = {}) { +function getStringURLParam(router: NextRouter, k: string) { + const v = router.query[k] + return typeof v === 'string' ? v : null +} + +export function useQuery(defaultQuery: string, opts?: QuerySortOpts) { + const useUrl = opts?.useUrl ?? false const router = useRouter() - - const { s: sort, q: query } = router.query as { - q?: string - s?: Sort - } - - const setSort = (sort: Sort | undefined) => { - router.replace({ query: { ...router.query, s: sort } }, undefined, { - shallow: true, - }) - if (shouldLoadFromStorage) { - localStorage.setItem(MARKETS_SORT, sort || '') - } - } - - const [queryState, setQueryState] = useState(query) - - useEffect(() => { - setQueryState(query) - }, [query]) - - // Debounce router query update. - const pushQuery = useMemo( - () => - debounce((query: string | undefined) => { - const queryObj = { ...router.query, q: query } - if (!query) delete queryObj.q - router.replace({ query: queryObj }, undefined, { - shallow: true, - }) - }, 100), - [router] - ) - - const setQuery = (query: string | undefined) => { - setQueryState(query) - if (!disableQueryString) { - pushQuery(query) - } - } - - useEffect(() => { - // If there's no sort option, then set the one from localstorage - if (router.isReady && !sort && shouldLoadFromStorage) { - const localSort = localStorage.getItem(MARKETS_SORT) as Sort - if (localSort && localSort !== defaultSort) { - // Use replace to not break navigating back. - router.replace( - { query: { ...router.query, s: localSort } }, - undefined, - { shallow: true } - ) - } - } - }) - - // use normal state if querydisableQueryString - const [sortState, setSortState] = useState(defaultSort) - - return { - sort: disableQueryString ? sortState : sort ?? defaultSort, - query: queryState ?? '', - setSort: disableQueryString ? setSortState : setSort, - setQuery, + const initialQuery = useUrl ? getStringURLParam(router, 'q') : null + const [query, setQuery] = useState(initialQuery ?? defaultQuery) + if (!useUrl) { + return [query, setQuery] as const + } else { + return [query, (q: string) => (setQuery(q), updateURL({ q }))] as const + } +} + +export function useSort(defaultSort: Sort, opts?: QuerySortOpts) { + const useUrl = opts?.useUrl ?? false + const router = useRouter() + const initialSort = useUrl ? (getStringURLParam(router, 's') as Sort) : null + const [sort, setSort] = useState(initialSort ?? defaultSort) + if (!useUrl) { + return [sort, setSort] as const + } else { + return [sort, (s: Sort) => (setSort(s), updateURL({ s }))] as const } } diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index f56c82d1..ec480269 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -3,16 +3,11 @@ import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { ContractsGrid } from 'web/components/contract/contracts-grid' import { useContracts } from 'web/hooks/use-contracts' -import { - QuerySortOptions, - Sort, - useQueryAndSortParams, -} from 'web/hooks/use-sort-and-query-params' +import { Sort, useQuery, useSort } from 'web/hooks/use-sort-and-query-params' const MAX_CONTRACTS_RENDERED = 100 export default function ContractSearchFirestore(props: { - querySortOptions?: QuerySortOptions additionalFilter?: { creatorId?: string tag?: string @@ -21,10 +16,9 @@ export default function ContractSearchFirestore(props: { } }) { const contracts = useContracts() - const { querySortOptions, additionalFilter } = props - - const { query, setQuery, sort, setSort } = - useQueryAndSortParams(querySortOptions) + const { additionalFilter } = props + const [query, setQuery] = useQuery('', { useUrl: true }) + const [sort, setSort] = useSort('score', { useUrl: true }) let matches = (contracts ?? []).filter((c) => searchInAny( diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index cd4b7344..c5255974 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -31,7 +31,6 @@ import { CreateQuestionButton } from 'web/components/create-question-button' import React, { useState } from 'react' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' -import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' @@ -196,11 +195,8 @@ export default function GroupPage(props: { const questionsTab = ( <ContractSearch user={user} - querySortOptions={{ - shouldLoadFromStorage: true, - defaultSort: getSavedSort() ?? 'newest', - defaultFilter: 'open', - }} + defaultSort={'newest'} + defaultFilter={'open'} additionalFilter={{ groupSlug: group.slug }} /> ) diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 839a08f3..b11c0cf9 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -4,8 +4,7 @@ import { PlusSmIcon } from '@heroicons/react/solid' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' -import { getSavedSort } from 'web/hooks/use-sort-and-query-params' -import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search' +import { ContractSearch } from 'web/components/contract-search' import { Contract } from 'common/contract' import { User } from 'common/user' import { ContractPageContent } from './[username]/[contractSlug]' @@ -35,10 +34,8 @@ const Home = (props: { auth: { user: User } }) => { <Col className="mx-auto w-full p-2"> <ContractSearch user={user} - querySortOptions={{ - shouldLoadFromStorage: true, - defaultSort: getSavedSort() ?? DEFAULT_SORT, - }} + useQuerySortLocalStorage={true} + useQuerySortUrlParams={true} onContractClick={(c) => { // Show contract without navigating to contract page. setContract(c) diff --git a/web/pages/tag/[tag].tsx b/web/pages/tag/[tag].tsx index c1dce29e..f2554f49 100644 --- a/web/pages/tag/[tag].tsx +++ b/web/pages/tag/[tag].tsx @@ -15,11 +15,8 @@ export default function TagPage() { <Title text={`#${tag}`} /> <ContractSearch user={user} - querySortOptions={{ - defaultSort: 'newest', - defaultFilter: 'all', - shouldLoadFromStorage: true, - }} + defaultSort="newest" + defaultFilter="all" additionalFilter={{ tag }} /> </Page> From c80f82a3f7f4c543131095e6bfaa45a4c23c30a2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 15 Aug 2022 11:06:42 -0500 Subject: [PATCH 516/519] Home page hack: discard NextJS router state --- web/pages/home.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/pages/home.tsx b/web/pages/home.tsx index b11c0cf9..1fd163ea 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -101,13 +101,19 @@ const useContractPage = () => { window.history.pushState = function () { // eslint-disable-next-line prefer-rest-params - pushState.apply(history, arguments as any) + const args = [...(arguments as any)] as any + // Discard NextJS router state. + args[0] = null + pushState.apply(history, args) updateContract() } window.history.replaceState = function () { // eslint-disable-next-line prefer-rest-params - replaceState.apply(history, arguments as any) + const args = [...(arguments as any)] as any + // Discard NextJS router state. + args[0] = null + replaceState.apply(history, args) updateContract() } From 5c4946144901840d4110ac436331d2eb8c54ffeb Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Mon, 15 Aug 2022 11:10:40 -0500 Subject: [PATCH 517/519] new welcome email --- functions/src/email-templates/welcome.html | 1091 +++++--------------- 1 file changed, 277 insertions(+), 814 deletions(-) diff --git a/functions/src/email-templates/welcome.html b/functions/src/email-templates/welcome.html index 58527080..74bd6a94 100644 --- a/functions/src/email-templates/welcome.html +++ b/functions/src/email-templates/welcome.html @@ -1,824 +1,287 @@ <!DOCTYPE html> -<html - xmlns="http://www.w3.org/1999/xhtml" - xmlns:v="urn:schemas-microsoft-com:vml" - xmlns:o="urn:schemas-microsoft-com:office:office" -> - <head> - <title>Welcome to Manifold Markets - - - - - - - + + + + + + - - - + + - + + + - - - -
- -
-
+ + +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

+ This e-mail has been sent to {{name}}, + click here to unsubscribe. +

+
+
+
+
+ +
+
+ + + + diff --git a/functions/src/emails.ts b/functions/src/emails.ts index a29f982c..b7469e9f 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -165,7 +165,6 @@ export const sendWelcomeEmail = async ( ) } -// TODO: use manalinks to give out M$500 export const sendOneWeekBonusEmail = async ( user: User, privateUser: PrivateUser @@ -185,12 +184,12 @@ export const sendOneWeekBonusEmail = async ( await sendTemplateEmail( privateUser.email, - 'Manifold one week anniversary gift', + 'Manifold Markets one week anniversary gift', 'one-week', { name: firstName, unsubscribeLink, - manalink: '', // TODO + manalink: 'https://manifold.markets/link/lj4JbBvE', }, { from: 'David from Manifold ', diff --git a/functions/src/index.ts b/functions/src/index.ts index b8f3eedb..76e54f1c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -27,6 +27,25 @@ export * from './on-delete-group' export * from './score-contracts' // v2 +export * from './health' +export * from './transact' +export * from './change-user-info' +export * from './create-user' +export * from './create-answer' +export * from './place-bet' +export * from './cancel-bet' +export * from './sell-bet' +export * from './sell-shares' +export * from './claim-manalink' +export * from './create-contract' +export * from './add-liquidity' +export * from './withdraw-liquidity' +export * from './create-group' +export * from './resolve-market' +export * from './unsubscribe' +export * from './stripe' +export * from './mana-bonus-email' + import { health } from './health' import { transact } from './transact' import { changeuserinfo } from './change-user-info' diff --git a/functions/src/mana-bonus-email.ts b/functions/src/mana-bonus-email.ts new file mode 100644 index 00000000..29a7e6e0 --- /dev/null +++ b/functions/src/mana-bonus-email.ts @@ -0,0 +1,42 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' +import * as dayjs from 'dayjs' + +import { getPrivateUser } from './utils' +import { sendOneWeekBonusEmail } from './emails' +import { User } from 'common/user' + +export const manabonusemail = functions + .runWith({ secrets: ['MAILGUN_KEY'] }) + .pubsub.schedule('0 9 * * 1-7') + .onRun(async () => { + await sendOneWeekEmails() + }) + +const firestore = admin.firestore() + +async function sendOneWeekEmails() { + const oneWeekAgo = dayjs().subtract(1, 'week').valueOf() + const twoWeekAgo = dayjs().subtract(2, 'weeks').valueOf() + + const userDocs = await firestore + .collection('users') + .where('createdTime', '<=', oneWeekAgo) + .get() + + for (const user of userDocs.docs.map((d) => d.data() as User)) { + if (user.createdTime < twoWeekAgo) continue + + const privateUser = await getPrivateUser(user.id) + if (!privateUser || privateUser.manaBonusEmailSent) continue + + await firestore + .collection('private-users') + .doc(user.id) + .update({ manaBonusEmailSent: true }) + + console.log('sending m$ bonus email to', user.username) + await sendOneWeekBonusEmail(user, privateUser) + return + } +} diff --git a/functions/src/send-email.ts b/functions/src/send-email.ts index f97234f6..d081997f 100644 --- a/functions/src/send-email.ts +++ b/functions/src/send-email.ts @@ -26,9 +26,10 @@ export const sendTemplateEmail = ( subject: string, templateId: string, templateData: Record, - options?: { from: string } + options?: Partial ) => { - const data = { + const data: mailgun.messages.SendTemplateData = { + ...options, from: options?.from ?? 'Manifold Markets ', to, subject, @@ -36,6 +37,7 @@ export const sendTemplateEmail = ( 'h:X-Mailgun-Variables': JSON.stringify(templateData), } const mg = initMailgun() + return mg.messages().send(data, (error) => { if (error) console.log('Error sending email', error) else console.log('Sent template email', templateId, to, subject) diff --git a/yarn.lock b/yarn.lock index 9334b737..bbf8d3ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5144,6 +5144,11 @@ dayjs@1.10.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== +dayjs@1.11.4: + version "1.11.4" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e" + integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g== + debug@2, debug@2.6.9, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From 798253f887fa6a946500f434ca7207d1d61ea00c Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 4 Aug 2022 15:27:02 -0600 Subject: [PATCH 411/519] Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros --- common/bet.ts | 1 + common/challenge.ts | 63 +++ common/notification.ts | 2 + firestore.rules | 11 + functions/src/accept-challenge.ts | 164 +++++++ functions/src/create-notification.ts | 33 ++ functions/src/index.ts | 3 + og-image/README.md | 47 +- og-image/api/_lib/challenge-template.ts | 203 +++++++++ og-image/api/_lib/parser.ts | 14 + og-image/api/_lib/template.ts | 2 +- og-image/api/_lib/types.ts | 39 +- og-image/api/index.ts | 46 +- web/components/SEO.tsx | 28 +- web/components/bet-panel.tsx | 5 +- web/components/button.tsx | 18 +- .../challenges/accept-challenge-button.tsx | 125 ++++++ .../challenges/create-challenge-button.tsx | 255 +++++++++++ .../contract/contract-card-preview.tsx | 36 ++ web/components/contract/contract-overview.tsx | 51 ++- web/components/copy-link-button.tsx | 3 +- web/components/feed/contract-activity.tsx | 5 +- web/components/feed/feed-bets.tsx | 23 +- web/components/nav/sidebar.tsx | 69 ++- .../portfolio/portfolio-value-section.tsx | 29 +- web/components/share-market-button.tsx | 18 + web/components/share-market.tsx | 28 -- web/components/sign-up-prompt.tsx | 14 +- web/hooks/use-bets.ts | 20 +- web/hooks/use-save-referral.ts | 4 +- web/hooks/use-user.ts | 2 +- web/lib/firebase/api.ts | 4 + web/lib/firebase/challenges.ts | 150 +++++++ web/lib/firebase/contracts.ts | 7 + web/pages/[username]/[contractSlug].tsx | 179 ++++++-- .../[contractSlug]/[challengeSlug].tsx | 403 ++++++++++++++++++ web/pages/challenges/index.tsx | 300 +++++++++++++ web/pages/embed/[username]/[contractSlug].tsx | 9 +- web/pages/group/[...slugs]/index.tsx | 2 +- web/pages/link/[slug].tsx | 2 +- web/pages/notifications.tsx | 13 + 41 files changed, 2233 insertions(+), 197 deletions(-) create mode 100644 common/challenge.ts create mode 100644 functions/src/accept-challenge.ts create mode 100644 og-image/api/_lib/challenge-template.ts create mode 100644 web/components/challenges/accept-challenge-button.tsx create mode 100644 web/components/challenges/create-challenge-button.tsx create mode 100644 web/components/contract/contract-card-preview.tsx create mode 100644 web/components/share-market-button.tsx delete mode 100644 web/components/share-market.tsx create mode 100644 web/lib/firebase/challenges.ts create mode 100644 web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx create mode 100644 web/pages/challenges/index.tsx diff --git a/common/bet.ts b/common/bet.ts index d5072c0f..56e050a7 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -26,6 +26,7 @@ export type Bet = { isAnte?: boolean isLiquidityProvision?: boolean isRedemption?: boolean + challengeSlug?: string } & Partial export type NumericBet = Bet & { diff --git a/common/challenge.ts b/common/challenge.ts new file mode 100644 index 00000000..1a227f94 --- /dev/null +++ b/common/challenge.ts @@ -0,0 +1,63 @@ +export type Challenge = { + // The link to send: https://manifold.markets/challenges/username/market-slug/{slug} + // Also functions as the unique id for the link. + slug: string + + // The user that created the challenge. + creatorId: string + creatorUsername: string + creatorName: string + creatorAvatarUrl?: string + + // Displayed to people claiming the challenge + message: string + + // How much to put up + creatorAmount: number + + // YES or NO for now + creatorOutcome: string + + // Different than the creator + acceptorOutcome: string + acceptorAmount: number + + // The probability the challenger thinks + creatorOutcomeProb: number + + contractId: string + contractSlug: string + contractQuestion: string + contractCreatorUsername: string + + createdTime: number + // If null, the link is valid forever + expiresTime: number | null + + // How many times the challenge can be used + maxUses: number + + // Used for simpler caching + acceptedByUserIds: string[] + // Successful redemptions of the link + acceptances: Acceptance[] + + // TODO: will have to fill this on resolve contract + isResolved: boolean + resolutionOutcome?: string +} + +export type Acceptance = { + // User that accepted the challenge + userId: string + userUsername: string + userName: string + userAvatarUrl: string + + // The ID of the successful bet that tracks the money moved + betId: string + + createdTime: number +} + +export const CHALLENGES_ENABLED = true diff --git a/common/notification.ts b/common/notification.ts index 5fd4236b..fa4cd90a 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -37,6 +37,7 @@ export type notification_source_types = | 'group' | 'user' | 'bonus' + | 'challenge' export type notification_source_update_types = | 'created' @@ -64,3 +65,4 @@ export type notification_reason_types = | 'tip_received' | 'bet_fill' | 'user_joined_from_your_group_invite' + | 'challenge_accepted' diff --git a/firestore.rules b/firestore.rules index 05721dcf..b0befc85 100644 --- a/firestore.rules +++ b/firestore.rules @@ -39,6 +39,17 @@ service cloud.firestore { allow read; } + match /{somePath=**}/challenges/{challengeId}{ + allow read; + } + + match /contracts/{contractId}/challenges/{challengeId}{ + allow read; + allow create: if request.auth.uid == request.resource.data.creatorId; + // allow update if there have been no claims yet and if the challenge is still open + allow update: if request.auth.uid == resource.data.creatorId; + } + match /users/{userId}/follows/{followUserId} { allow read; allow write: if request.auth.uid == userId; diff --git a/functions/src/accept-challenge.ts b/functions/src/accept-challenge.ts new file mode 100644 index 00000000..fa98c8c6 --- /dev/null +++ b/functions/src/accept-challenge.ts @@ -0,0 +1,164 @@ +import { z } from 'zod' +import { APIError, newEndpoint, validate } from './api' +import { log } from './utils' +import { Contract, CPMMBinaryContract } from '../../common/contract' +import { User } from '../../common/user' +import * as admin from 'firebase-admin' +import { FieldValue } from 'firebase-admin/firestore' +import { removeUndefinedProps } from '../../common/util/object' +import { Acceptance, Challenge } from '../../common/challenge' +import { CandidateBet } from '../../common/new-bet' +import { createChallengeAcceptedNotification } from './create-notification' +import { noFees } from '../../common/fees' +import { formatMoney, formatPercent } from '../../common/util/format' + +const bodySchema = z.object({ + contractId: z.string(), + challengeSlug: z.string(), + outcomeType: z.literal('BINARY'), + closeTime: z.number().gte(Date.now()), +}) +const firestore = admin.firestore() + +export const acceptchallenge = newEndpoint({}, async (req, auth) => { + const { challengeSlug, contractId } = validate(bodySchema, req.body) + + const result = await firestore.runTransaction(async (trans) => { + const contractDoc = firestore.doc(`contracts/${contractId}`) + const userDoc = firestore.doc(`users/${auth.uid}`) + const challengeDoc = firestore.doc( + `contracts/${contractId}/challenges/${challengeSlug}` + ) + const [contractSnap, userSnap, challengeSnap] = await trans.getAll( + contractDoc, + userDoc, + challengeDoc + ) + if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') + if (!userSnap.exists) throw new APIError(400, 'User not found.') + if (!challengeSnap.exists) throw new APIError(400, 'Challenge not found.') + + const anyContract = contractSnap.data() as Contract + const user = userSnap.data() as User + const challenge = challengeSnap.data() as Challenge + + if (challenge.acceptances.length > 0) + throw new APIError(400, 'Challenge already accepted.') + + const creatorDoc = firestore.doc(`users/${challenge.creatorId}`) + const creatorSnap = await trans.get(creatorDoc) + if (!creatorSnap.exists) throw new APIError(400, 'User not found.') + const creator = creatorSnap.data() as User + + const { + creatorAmount, + acceptorOutcome, + creatorOutcome, + creatorOutcomeProb, + acceptorAmount, + } = challenge + + if (user.balance < acceptorAmount) + throw new APIError(400, 'Insufficient balance.') + + const contract = anyContract as CPMMBinaryContract + const shares = (1 / creatorOutcomeProb) * creatorAmount + const createdTime = Date.now() + const probOfYes = + creatorOutcome === 'YES' ? creatorOutcomeProb : 1 - creatorOutcomeProb + + log( + 'Creating challenge bet for', + user.username, + shares, + acceptorOutcome, + 'shares', + 'at', + formatPercent(creatorOutcomeProb), + 'for', + formatMoney(acceptorAmount) + ) + + const yourNewBet: CandidateBet = removeUndefinedProps({ + orderAmount: acceptorAmount, + amount: acceptorAmount, + shares, + isCancelled: false, + contractId: contract.id, + outcome: acceptorOutcome, + probBefore: probOfYes, + probAfter: probOfYes, + loanAmount: 0, + createdTime, + fees: noFees, + challengeSlug: challenge.slug, + }) + + const yourNewBetDoc = contractDoc.collection('bets').doc() + trans.create(yourNewBetDoc, { + id: yourNewBetDoc.id, + userId: user.id, + ...yourNewBet, + }) + + trans.update(userDoc, { balance: FieldValue.increment(-yourNewBet.amount) }) + + const creatorNewBet: CandidateBet = removeUndefinedProps({ + orderAmount: creatorAmount, + amount: creatorAmount, + shares, + isCancelled: false, + contractId: contract.id, + outcome: creatorOutcome, + probBefore: probOfYes, + probAfter: probOfYes, + loanAmount: 0, + createdTime, + fees: noFees, + challengeSlug: challenge.slug, + }) + const creatorBetDoc = contractDoc.collection('bets').doc() + trans.create(creatorBetDoc, { + id: creatorBetDoc.id, + userId: creator.id, + ...creatorNewBet, + }) + + trans.update(creatorDoc, { + balance: FieldValue.increment(-creatorNewBet.amount), + }) + + const volume = contract.volume + yourNewBet.amount + creatorNewBet.amount + trans.update(contractDoc, { volume }) + + trans.update( + challengeDoc, + removeUndefinedProps({ + acceptedByUserIds: [user.id], + acceptances: [ + { + userId: user.id, + betId: yourNewBetDoc.id, + createdTime, + amount: acceptorAmount, + userUsername: user.username, + userName: user.name, + userAvatarUrl: user.avatarUrl, + } as Acceptance, + ], + }) + ) + + await createChallengeAcceptedNotification( + user, + creator, + challenge, + acceptorAmount, + contract + ) + log('Done, sent notification.') + return yourNewBetDoc + }) + + return { betId: result.id } +}) diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 7cc05760..83568535 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -16,6 +16,7 @@ import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' import { Group, GROUP_CHAT_SLUG } from '../../common/group' +import { Challenge } from '../../common/challenge' const firestore = admin.firestore() type user_to_reason_texts = { @@ -478,3 +479,35 @@ export const createReferralNotification = async ( } const groupPath = (groupSlug: string) => `/group/${groupSlug}` + +export const createChallengeAcceptedNotification = async ( + challenger: User, + challengeCreator: User, + challenge: Challenge, + acceptedAmount: number, + contract: Contract +) => { + const notificationRef = firestore + .collection(`/users/${challengeCreator.id}/notifications`) + .doc() + const notification: Notification = { + id: notificationRef.id, + userId: challengeCreator.id, + reason: 'challenge_accepted', + createdTime: Date.now(), + isSeen: false, + sourceId: challenge.slug, + sourceType: 'challenge', + sourceUpdateType: 'updated', + sourceUserName: challenger.name, + sourceUserUsername: challenger.username, + sourceUserAvatarUrl: challenger.avatarUrl, + sourceText: acceptedAmount.toString(), + sourceContractCreatorUsername: contract.creatorUsername, + sourceContractTitle: contract.question, + sourceContractSlug: contract.slug, + sourceContractId: contract.id, + sourceSlug: `/challenges/${challengeCreator.username}/${challenge.contractSlug}/${challenge.slug}`, + } + return await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 76e54f1c..125cdea4 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -64,6 +64,7 @@ import { resolvemarket } from './resolve-market' import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' +import { acceptchallenge } from './accept-challenge' const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { return onRequest(opts, handler as any) @@ -87,6 +88,7 @@ const unsubscribeFunction = toCloudFunction(unsubscribe) const stripeWebhookFunction = toCloudFunction(stripewebhook) const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) const getCurrentUserFunction = toCloudFunction(getcurrentuser) +const acceptChallenge = toCloudFunction(acceptchallenge) export { healthFunction as health, @@ -108,4 +110,5 @@ export { stripeWebhookFunction as stripewebhook, createCheckoutSessionFunction as createcheckoutsession, getCurrentUserFunction as getcurrentuser, + acceptChallenge as acceptchallenge, } diff --git a/og-image/README.md b/og-image/README.md index 7d0d2f92..6ecc4e82 100644 --- a/og-image/README.md +++ b/og-image/README.md @@ -1,32 +1,35 @@ +# Installing +1. `yarn install` +2. `yarn start` +3. `Y` to `Set up and develop “~path/to/the/repo/manifold”? [Y/n]` +4. `Manifold Markets` to `Which scope should contain your project? [Y/n] ` +5. `Y` to `Link to existing project? [Y/n] ` +6. `opengraph-image` to `What’s the name of your existing project?` + # Quickstart -1. To get started: `yarn install` -2. To test locally: `yarn start` +1. To test locally: `yarn start` The local image preview is broken for some reason; but the service works. E.g. try `http://localhost:3000/manifold.png` -3. To deploy: push to Github - -For more info, see Contributing.md - -- note2: You may have to configure Vercel the first time: - - ``` - $ yarn start - yarn run v1.22.10 - $ cd .. && vercel dev - Vercel CLI 23.1.2 dev (beta) — https://vercel.com/feedback - ? Set up and develop “~/Code/mantic”? [Y/n] y - ? Which scope should contain your project? Mantic Markets - ? Found project “mantic/mantic”. Link to it? [Y/n] n - ? Link to different existing project? [Y/n] y - ? What’s the name of your existing project? manifold-og-image - ``` - -- note2: (Not `dev` because that's reserved for Vercel) -- note3: (Or `cd .. && vercel --prod`, I think) +2. To deploy: push to Github +- note: (Not `dev` because that's reserved for Vercel) +- note2: (Or `cd .. && vercel --prod`, I think) +For more info, see Contributing.md (Everything below is from the original repo) +# Development +- Code of interest is contained in the `api/_lib` directory, i.e. `template.ts` is the page that renders the UI. +- Edit `parseRequest(req: IncomingMessage)` in `parser.ts` to add/edit query parameters. +- Note: When testing a remote branch on vercel, the og-image previews that apps load will point to +`https://manifold-og-image.vercel.app/m.png?question=etc.`, (see relevant code in `SEO.tsx`) and not your remote branch. +You have to find your opengraph-image branch's url and replace the part before `m.png` with it. + - You can also preview the image locally, e.g. `http://localhost:3000/m.png?question=etc.` + - Every time you change the template code you'll have to change the query parameter slightly as the image will likely be cached. +- You can find your remote branch's opengraph-image url by click `Visit Preview` on Github: +![](../../../../../Desktop/Screen Shot 2022-08-01 at 2.56.42 PM.png) + + # [Open Graph Image as a Service](https://og-image.vercel.app) diff --git a/og-image/api/_lib/challenge-template.ts b/og-image/api/_lib/challenge-template.ts new file mode 100644 index 00000000..6dc43ac1 --- /dev/null +++ b/og-image/api/_lib/challenge-template.ts @@ -0,0 +1,203 @@ +import { sanitizeHtml } from './sanitizer' +import { ParsedRequest } from './types' + +function getCss(theme: string, fontSize: string) { + let background = 'white' + let foreground = 'black' + let radial = 'lightgray' + + if (theme === 'dark') { + background = 'black' + foreground = 'white' + radial = 'dimgray' + } + // To use Readex Pro: `font-family: 'Readex Pro', sans-serif;` + return ` + @import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@400;700&display=swap'); + + body { + background: ${background}; + background-image: radial-gradient(circle at 25px 25px, ${radial} 2%, transparent 0%), radial-gradient(circle at 75px 75px, ${radial} 2%, transparent 0%); + background-size: 100px 100px; + height: 100vh; + font-family: "Readex Pro", sans-serif; + } + + code { + color: #D400FF; + font-family: 'Vera'; + white-space: pre-wrap; + letter-spacing: -5px; + } + + code:before, code:after { + content: '\`'; + } + + .logo-wrapper { + display: flex; + align-items: center; + align-content: center; + justify-content: center; + justify-items: center; + } + + .logo { + margin: 0 75px; + } + + .plus { + color: #BBB; + font-family: Times New Roman, Verdana; + font-size: 100px; + } + + .spacer { + margin: 150px; + } + + .emoji { + height: 1em; + width: 1em; + margin: 0 .05em 0 .1em; + vertical-align: -0.1em; + } + + .heading { + font-family: 'Major Mono Display', monospace; + font-size: ${sanitizeHtml(fontSize)}; + font-style: normal; + color: ${foreground}; + line-height: 1.8; + } + + .font-major-mono { + font-family: "Major Mono Display", monospace; + } + + .text-primary { + color: #11b981; + } + ` +} + +export function getChallengeHtml(parsedReq: ParsedRequest) { + const { + theme, + fontSize, + question, + creatorName, + creatorAvatarUrl, + challengerAmount, + challengerOutcome, + creatorAmount, + creatorOutcome, + acceptedName, + acceptedAvatarUrl, + } = parsedReq + const MAX_QUESTION_CHARS = 78 + const truncatedQuestion = + question.length > MAX_QUESTION_CHARS + ? question.slice(0, MAX_QUESTION_CHARS) + '...' + : question + const hideAvatar = creatorAvatarUrl ? '' : 'hidden' + const hideAcceptedAvatar = acceptedAvatarUrl ? '' : 'hidden' + const accepted = acceptedName !== '' + return ` + + + + Generated Image + + + + + +
+ + +
+
+ ${truncatedQuestion} +
+
+
+ + +
+

${creatorName}

+ +
+
+
${'M$' + creatorAmount}
+
${'on'}
+
${creatorOutcome}
+
+
+ + +
+ VS +
+
+ + +
+

You

+ +
+ +
+

${acceptedName}

+ +
+
+
${'M$' + challengerAmount}
+
${'on'}
+
${challengerOutcome}
+
+
+
+ +
+
+ +
+ + + +` +} diff --git a/og-image/api/_lib/parser.ts b/og-image/api/_lib/parser.ts index b8163719..1a0863bd 100644 --- a/og-image/api/_lib/parser.ts +++ b/og-image/api/_lib/parser.ts @@ -20,6 +20,14 @@ export function parseRequest(req: IncomingMessage) { creatorName, creatorUsername, creatorAvatarUrl, + + // Challenge attributes: + challengerAmount, + challengerOutcome, + creatorAmount, + creatorOutcome, + acceptedName, + acceptedAvatarUrl, } = query || {} if (Array.isArray(fontSize)) { @@ -67,6 +75,12 @@ export function parseRequest(req: IncomingMessage) { creatorName: getString(creatorName) || 'Manifold Markets', creatorUsername: getString(creatorUsername) || 'ManifoldMarkets', creatorAvatarUrl: getString(creatorAvatarUrl) || '', + challengerAmount: getString(challengerAmount) || '', + challengerOutcome: getString(challengerOutcome) || '', + creatorAmount: getString(creatorAmount) || '', + creatorOutcome: getString(creatorOutcome) || '', + acceptedName: getString(acceptedName) || '', + acceptedAvatarUrl: getString(acceptedAvatarUrl) || '', } parsedRequest.images = getDefaultImages(parsedRequest.images) return parsedRequest diff --git a/og-image/api/_lib/template.ts b/og-image/api/_lib/template.ts index a6b0336c..1fe54554 100644 --- a/og-image/api/_lib/template.ts +++ b/og-image/api/_lib/template.ts @@ -126,7 +126,7 @@ export function getHtml(parsedReq: ParsedRequest) { - +
Internal Error

Sorry, there was a problem

"); - console.error(e); + res.statusCode = 500 + res.setHeader('Content-Type', 'text/html') + res.end('

Internal Error

Sorry, there was a problem

') + console.error(e) } } diff --git a/web/components/SEO.tsx b/web/components/SEO.tsx index 11e24c99..b1e0ca5f 100644 --- a/web/components/SEO.tsx +++ b/web/components/SEO.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react' import Head from 'next/head' +import { Challenge } from 'common/challenge' export type OgCardProps = { question: string @@ -10,7 +11,16 @@ export type OgCardProps = { creatorAvatarUrl?: string } -function buildCardUrl(props: OgCardProps) { +function buildCardUrl(props: OgCardProps, challenge?: Challenge) { + const { + creatorAmount, + acceptances, + acceptorAmount, + creatorOutcome, + acceptorOutcome, + } = challenge || {} + const { userName, userAvatarUrl } = acceptances?.[0] ?? {} + const probabilityParam = props.probability === undefined ? '' @@ -20,6 +30,12 @@ function buildCardUrl(props: OgCardProps) { ? '' : `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}` + const challengeUrlParams = challenge + ? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` + + `&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` + + `&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}` + : '' + // URL encode each of the props, then add them as query params return ( `https://manifold-og-image.vercel.app/m.png` + @@ -28,7 +44,8 @@ function buildCardUrl(props: OgCardProps) { `&metadata=${encodeURIComponent(props.metadata)}` + `&creatorName=${encodeURIComponent(props.creatorName)}` + creatorAvatarUrlParam + - `&creatorUsername=${encodeURIComponent(props.creatorUsername)}` + `&creatorUsername=${encodeURIComponent(props.creatorUsername)}` + + challengeUrlParams ) } @@ -38,8 +55,9 @@ export function SEO(props: { url?: string children?: ReactNode ogCardProps?: OgCardProps + challenge?: Challenge }) { - const { title, description, url, children, ogCardProps } = props + const { title, description, url, children, ogCardProps, challenge } = props return ( @@ -71,13 +89,13 @@ export function SEO(props: { <> diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index aea38c86..c0f7ff94 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -16,8 +16,7 @@ import { import { getBinaryBetStats, getBinaryCpmmBetInfo } from 'common/new-bet' import { User } from 'web/lib/firebase/users' import { Bet, LimitBet } from 'common/bet' -import { APIError, placeBet } from 'web/lib/firebase/api' -import { sellShares } from 'web/lib/firebase/api' +import { APIError, placeBet, sellShares } from 'web/lib/firebase/api' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { @@ -351,7 +350,7 @@ function BuyPanel(props: { {user && (
- - - - - -
- -
- - - - - - -
- - - - - - -
- -
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-

- Hi {{name}}, thanks for joining Manifold - Markets!

We can't wait to see what questions you - will ask! -

-

- As a gift M$1000 has been credited to your - account - the equivalent of 10 USD. - -

-

- Click the buttons to see what you can do with - it! -

-
-
-
- -
- - - - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - -
- - - -
-
- - - - - - -
- - - -
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-

- If you have any questions or feedback we'd - love to hear from you in our Discord server! -

-

-

- Looking forward to seeing you, -

-

- David from Manifold -

-
-

-
-
-
- -
-
- -
- - - - - - -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-

- This e-mail has been sent to {{name}}, - click here to unsubscribe. -

-
-
-
-
- -
-
- + } + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+

+ Hi {{name}},

+
+
+
+

+ Welcome! Manifold Markets is a play-money prediction market platform where you can bet on + anything, from elections to Elon Musk to scientific papers to the NBA.

+
+
+
+

+ +

+
+
+

+
+ + + + +
+ + + + +
+ + Explore markets + +
+
+
+
+

+ +

 

+

Cheers,

+

David from Manifold

+

 

+
+
+
+ +
- - + +
+ + + +
+ +
+ + + + + {onClick ? ( )} - +

))} - - + + + + {showQuickBet ? ( diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 4a9d40af..71998b9d 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -34,6 +34,7 @@ import { ContractGroupsList } from 'web/components/groups/contract-groups-list' import { SiteLink } from 'web/components/site-link' import { groupPath } from 'web/lib/firebase/groups' import { insertContent } from '../editor/utils' +import clsx from 'clsx' export type ShowTime = 'resolve-date' | 'close-date' @@ -83,9 +84,8 @@ export function MiscDetails(props: { {!hideGroupLink && groupLinks && groupLinks.length > 0 && ( - {groupLinks[0].name} )} @@ -93,18 +93,24 @@ export function MiscDetails(props: { ) } -export function AvatarDetails(props: { contract: Contract }) { - const { contract } = props +export function AvatarDetails(props: { + contract: Contract + className?: string + short?: boolean +}) { + const { contract, short, className } = props const { creatorName, creatorUsername } = contract return ( - + - + ) } diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 2069ef72..f412e38b 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -34,16 +34,25 @@ export function UserLink(props: { username: string showUsername?: boolean className?: string - justFirstName?: boolean + short?: boolean }) { - const { name, username, showUsername, className, justFirstName } = props - + const { name, username, showUsername, className, short } = props + const firstName = name.split(' ')[0] + const maxLength = 10 + const shortName = + firstName.length >= 3 + ? firstName.length < maxLength + ? firstName + : firstName.substring(0, maxLength - 3) + '...' + : name.length > maxLength + ? name.substring(0, maxLength) + '...' + : name return ( - {justFirstName ? name.split(' ')[0] : name} + {short ? shortName : name} {showUsername && ` (@${username})`} ) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index a729aca1..7d06c481 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -440,7 +440,7 @@ function IncomeNotificationItem(props: { name={sourceUserName || ''} username={sourceUserUsername || ''} className={'mr-1 flex-shrink-0'} - justFirstName={true} + short={true} /> ))} {getReasonForShowingIncomeNotification(false)} {' on'} @@ -609,7 +609,7 @@ function NotificationItem(props: { name={sourceUserName || ''} username={sourceUserUsername || ''} className={'mr-0 flex-shrink-0'} - justFirstName={true} + short={true} />

@@ -681,7 +681,7 @@ function NotificationItem(props: { name={sourceUserName || ''} username={sourceUserUsername || ''} className={'relative mr-1 flex-shrink-0'} - justFirstName={true} + short={true} /> )} {getReasonForShowingNotification(
+ + + +
+
+ + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

This e-mail has been sent to {{name}}, click here to unsubscribe.

+
+
+
+
+
+
+
+ +
+ + + + + + \ No newline at end of file From 2ff2d6c1fc68355b3aea8dbe802770a8af9c4664 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 15 Aug 2022 14:26:18 -0500 Subject: [PATCH 518/519] Scroll to top for fresh query --- web/components/contract-search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 54b30f3f..11d65a13 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -139,6 +139,7 @@ export function ContractSearch(props: { setNumPages(results.nbPages) if (freshQuery) { setPages([newPage]) + window.scrollTo(0, 0) } else { setPages((pages) => [...pages, newPage]) } From 428d9a369200f25270080196099152570ffd410b Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 15 Aug 2022 13:49:33 -0600 Subject: [PATCH 519/519] Move avatar to below card on mobile --- web/components/contract/contract-card.tsx | 27 +++++++++++++------- web/components/contract/contract-details.tsx | 18 ++++++++----- web/components/user-page.tsx | 17 +++++++++--- web/pages/notifications.tsx | 6 ++--- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index b4f20a40..c054abab 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -72,7 +72,7 @@ export function ContractCard(props: { className )} > -