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( querySortOptions?.defaultFilter ?? 'open' @@ -257,87 +256,90 @@ export function ContractSearch(props: { } return ( - - - updateQuery(e.target.value)} - onBlur={trackCallback('search', { query })} - placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} - className="input input-bordered w-full" - /> - {!query && ( - + + selectSort(e.target.value as Sort)} - > - {sortOptions.map((option) => ( - - ))} - - )} - - - - - {pillsEnabled && ( - - - All - - - {user ? 'For you' : 'Featured'} - - - {user && ( - + + updateQuery(e.target.value)} + onBlur={trackCallback('search', { query })} + placeholder={showPlaceHolder ? `Search ${filter} markets` : ''} + className="input input-bordered w-full" + /> + {!query && ( + + )} + {!hideOrderSelector && !query && ( + )} - - {pillGroups.map(({ name, slug }) => { - return ( - - {name} - - ) - })} - )} - + {pillsEnabled && ( + + + All + + + {user ? 'For you' : 'Featured'} + + + {user && ( + + Your bets + + )} + + {pillGroups.map(({ name, slug }) => { + return ( + + {name} + + ) + })} + + )} + {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: {
{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, `

${editTimestamp()}

`) + editor?.commands.focus('end') + insertContent(editor, `

${editTimestamp()}

`) }} > 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, `

Close date updated to ${formattedCloseDate}

` ) 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: { {/* Toolbar, with buttons for images and embeds */}
-
+
-
+
+
+
+
{/* 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([]) + 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 ( + + + +
Embed a market
+ + {!loading && ( + + {contracts.length > 0 && ( + + )} + + + )} +
+ + {loading && ( +
+ +
+ )} + +
+ c.id), + highlightClassName: + '!bg-indigo-100 outline outline-2 outline-indigo-300', + }} + additionalFilter={{}} /* hide pills */ + headerClassName="bg-white" + /> +
+ +
+ ) +} 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 = `` - - copyToClipboard(embedCode) + return `` } 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