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 { + 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({ + 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/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(adminFirestore.collection('groups')) const contracts = await getValues( 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' }, ] 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 ? ( 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 = /^$/.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 }, }, }) diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index 00a7c625..acf97d1b 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,11 @@ 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 { manaToUSD } from 'common/util/format'>>>>>>> main import { track } from 'web/lib/service/analytics' +import { SEO } from 'web/components/SEO' export default function CharityPageWrapper() { const router = useRouter() @@ -63,6 +66,11 @@ function CharityPage(props: { charity: Charity }) { /> } > + {showConfetti && ( + @@ -128,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={[ { diff --git a/web/pages/create.tsx b/web/pages/create.tsx index bfea2dfc..9fa340f5 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/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/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/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> diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 7ee13172..45c484c4 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() @@ -78,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={[ { @@ -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 /> 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` 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">