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>
This commit is contained in:
parent
4e8b94a28c
commit
dc95587cca
|
@ -2,6 +2,7 @@ import { EnvConfig, PROD_CONFIG } from './prod'
|
||||||
|
|
||||||
export const DEV_CONFIG: EnvConfig = {
|
export const DEV_CONFIG: EnvConfig = {
|
||||||
...PROD_CONFIG,
|
...PROD_CONFIG,
|
||||||
|
domain: 'dev.manifold.markets',
|
||||||
firebaseConfig: {
|
firebaseConfig: {
|
||||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||||
|
|
|
@ -3,14 +3,17 @@ import algoliasearch from 'algoliasearch/lite'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { User } from 'common/user'
|
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 {
|
import {
|
||||||
ContractHighlightOptions,
|
ContractHighlightOptions,
|
||||||
ContractsGrid,
|
ContractsGrid,
|
||||||
} from './contract/contracts-grid'
|
} from './contract/contracts-grid'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Spacer } from './layout/spacer'
|
|
||||||
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
import { track, trackCallback } from 'web/lib/service/analytics'
|
import { track, trackCallback } from 'web/lib/service/analytics'
|
||||||
|
@ -21,6 +24,7 @@ import { PillButton } from './buttons/pill-button'
|
||||||
import { range, sortBy } from 'lodash'
|
import { range, sortBy } from 'lodash'
|
||||||
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
'GJQPAYENIF',
|
'GJQPAYENIF',
|
||||||
|
@ -45,12 +49,8 @@ export const DEFAULT_SORT = 'score'
|
||||||
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
|
||||||
export function ContractSearch(props: {
|
export function ContractSearch(props: {
|
||||||
user: User | null | undefined
|
user?: User | null
|
||||||
querySortOptions?: {
|
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions
|
||||||
defaultSort: Sort
|
|
||||||
defaultFilter?: filter
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}
|
|
||||||
additionalFilter?: {
|
additionalFilter?: {
|
||||||
creatorId?: string
|
creatorId?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
@ -66,6 +66,7 @@ export function ContractSearch(props: {
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
}
|
}
|
||||||
|
headerClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
@ -77,6 +78,7 @@ export function ContractSearch(props: {
|
||||||
showPlaceHolder,
|
showPlaceHolder,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
|
headerClassName,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
||||||
|
@ -99,11 +101,8 @@ export function ContractSearch(props: {
|
||||||
|
|
||||||
const follows = useFollows(user?.id)
|
const follows = useFollows(user?.id)
|
||||||
|
|
||||||
const { shouldLoadFromStorage, defaultSort } = querySortOptions ?? {}
|
const { query, setQuery, sort, setSort } =
|
||||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
useQueryAndSortParams(querySortOptions)
|
||||||
defaultSort,
|
|
||||||
shouldLoadFromStorage,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [filter, setFilter] = useState<filter>(
|
const [filter, setFilter] = useState<filter>(
|
||||||
querySortOptions?.defaultFilter ?? 'open'
|
querySortOptions?.defaultFilter ?? 'open'
|
||||||
|
@ -257,7 +256,13 @@ export function ContractSearch(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<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">
|
<Row className="gap-1 sm:gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -294,8 +299,6 @@ export function ContractSearch(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Spacer h={3} />
|
|
||||||
|
|
||||||
{pillsEnabled && (
|
{pillsEnabled && (
|
||||||
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
||||||
<PillButton
|
<PillButton
|
||||||
|
@ -336,8 +339,7 @@ export function ContractSearch(props: {
|
||||||
})}
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
</Col>
|
||||||
<Spacer h={3} />
|
|
||||||
|
|
||||||
{filter === 'personal' &&
|
{filter === 'personal' &&
|
||||||
(follows ?? []).length === 0 &&
|
(follows ?? []).length === 0 &&
|
||||||
|
|
|
@ -76,7 +76,8 @@ export function ContractCard(props: {
|
||||||
<Col className="relative flex-1 gap-3 pr-1">
|
<Col className="relative flex-1 gap-3 pr-1">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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 ? (
|
{onClick ? (
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { Editor, Content as ContentType } from '@tiptap/react'
|
import { Editor, Content as ContentType } from '@tiptap/react'
|
||||||
import { appendToEditor } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
|
|
||||||
export function ContractDescription(props: {
|
export function ContractDescription(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -95,7 +95,8 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
appendToEditor(editor, `<p>${editTimestamp()}</p>`)
|
editor?.commands.focus('end')
|
||||||
|
insertContent(editor, `<p>${editTimestamp()}</p>`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit description
|
Edit description
|
||||||
|
@ -127,7 +128,7 @@ function EditQuestion(props: {
|
||||||
|
|
||||||
function joinContent(oldContent: ContentType, newContent: string) {
|
function joinContent(oldContent: ContentType, newContent: string) {
|
||||||
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
||||||
appendToEditor(editor, newContent)
|
insertContent(editor, newContent)
|
||||||
return editor.getJSON()
|
return editor.getJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { Col } from 'web/components/layout/col'
|
||||||
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { appendToEditor } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
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 formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
||||||
|
|
||||||
const editor = new Editor({ content, extensions: exhibitExts })
|
const editor = new Editor({ content, extensions: exhibitExts })
|
||||||
appendToEditor(
|
editor.commands.focus('end')
|
||||||
|
insertContent(
|
||||||
editor,
|
editor,
|
||||||
`<br><p>Close date updated to ${formattedCloseDate}</p>`
|
`<br><p>Close date updated to ${formattedCloseDate}</p>`
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,12 +21,18 @@ import { useUsers } from 'web/hooks/use-users'
|
||||||
import { mentionSuggestion } from './editor/mention-suggestion'
|
import { mentionSuggestion } from './editor/mention-suggestion'
|
||||||
import { DisplayMention } from './editor/mention'
|
import { DisplayMention } from './editor/mention'
|
||||||
import Iframe from 'common/util/tiptap-iframe'
|
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 { Modal } from './layout/modal'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
import { MarketModal } from './editor/market-modal'
|
||||||
|
import { insertContent } from './editor/utils'
|
||||||
|
|
||||||
const DisplayImage = Image.configure({
|
const DisplayImage = Image.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -105,7 +111,7 @@ export function useTextEditor(props: {
|
||||||
// If the pasted content is iframe code, directly inject it
|
// If the pasted content is iframe code, directly inject it
|
||||||
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
||||||
if (isValidIframe(text)) {
|
if (isValidIframe(text)) {
|
||||||
editor.chain().insertContent(text).run()
|
insertContent(editor, text)
|
||||||
return true // Prevent the code from getting pasted as 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 { editor, upload, children } = props
|
||||||
const [iframeOpen, setIframeOpen] = useState(false)
|
const [iframeOpen, setIframeOpen] = useState(false)
|
||||||
|
const [marketOpen, setMarketOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -148,16 +155,15 @@ export function TextEditor(props: {
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{/* Toolbar, with buttons for images and embeds */}
|
{/* Toolbar, with buttons for images and embeds */}
|
||||||
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
<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
|
<FileUploadButton
|
||||||
onFiles={upload.mutate}
|
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"
|
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" />
|
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
<span className="sr-only">Upload an image</span>
|
|
||||||
</FileUploadButton>
|
</FileUploadButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="tooltip flex items-center" data-tip="Add embed">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIframeOpen(true)}
|
onClick={() => setIframeOpen(true)}
|
||||||
|
@ -169,7 +175,23 @@ export function TextEditor(props: {
|
||||||
setOpen={setIframeOpen}
|
setOpen={setIframeOpen}
|
||||||
/>
|
/>
|
||||||
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Spacer that also focuses editor on click */}
|
{/* Spacer that also focuses editor on click */}
|
||||||
|
@ -229,7 +251,7 @@ function IframeModal(props: {
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (editor && valid) {
|
if (editor && valid) {
|
||||||
editor.chain().insertContent(embedCode).run()
|
insertContent(editor, embedCode)
|
||||||
setInput('')
|
setInput('')
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
86
web/components/editor/market-modal.tsx
Normal file
86
web/components/editor/market-modal.tsx
Normal file
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
import { Editor, Content } from '@tiptap/react'
|
import { Editor, Content } from '@tiptap/react'
|
||||||
|
|
||||||
export function appendToEditor(editor: Editor | null, content: Content) {
|
export function insertContent(editor: Editor | null, ...contents: Content[]) {
|
||||||
editor
|
if (!editor) {
|
||||||
?.chain()
|
return
|
||||||
.focus('end')
|
}
|
||||||
.createParagraphNear()
|
|
||||||
.insertContent(content)
|
let e = editor.chain()
|
||||||
.run()
|
for (const content of contents) {
|
||||||
|
e = e.createParagraphNear().insertContent(content)
|
||||||
|
}
|
||||||
|
e.run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,11 @@ import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
function copyEmbedCode(contract: Contract) {
|
export function embedCode(contract: Contract) {
|
||||||
const title = contract.question
|
const title = contract.question
|
||||||
const src = `https://${DOMAIN}/embed${contractPath(contract)}`
|
const src = `https://${DOMAIN}/embed${contractPath(contract)}`
|
||||||
|
|
||||||
const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
|
return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
|
||||||
|
|
||||||
copyToClipboard(embedCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareEmbedButton(props: {
|
export function ShareEmbedButton(props: {
|
||||||
|
@ -29,7 +27,7 @@ export function ShareEmbedButton(props: {
|
||||||
as="div"
|
as="div"
|
||||||
className="relative z-10 flex-shrink-0"
|
className="relative z-10 flex-shrink-0"
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
copyEmbedCode(contract)
|
copyToClipboard(embedCode(contract))
|
||||||
track('copy embed code')
|
track('copy embed code')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -25,12 +25,18 @@ export function getSavedSort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useQueryAndSortParams(options?: {
|
export interface QuerySortOptions {
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
}) {
|
/** Use normal react state instead of url query string */
|
||||||
const { defaultSort = DEFAULT_SORT, shouldLoadFromStorage = true } =
|
disableQueryString?: boolean
|
||||||
options ?? {}
|
}
|
||||||
|
|
||||||
|
export function useQueryAndSortParams({
|
||||||
|
defaultSort = DEFAULT_SORT,
|
||||||
|
shouldLoadFromStorage = true,
|
||||||
|
disableQueryString,
|
||||||
|
}: QuerySortOptions = {}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const { s: sort, q: query } = router.query as {
|
const { s: sort, q: query } = router.query as {
|
||||||
|
@ -68,8 +74,10 @@ export function useQueryAndSortParams(options?: {
|
||||||
|
|
||||||
const setQuery = (query: string | undefined) => {
|
const setQuery = (query: string | undefined) => {
|
||||||
setQueryState(query)
|
setQueryState(query)
|
||||||
|
if (!disableQueryString) {
|
||||||
pushQuery(query)
|
pushQuery(query)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there's no sort option, then set the one from localstorage
|
// If there's no sort option, then set the one from localstorage
|
||||||
|
@ -86,10 +94,13 @@ export function useQueryAndSortParams(options?: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// use normal state if querydisableQueryString
|
||||||
|
const [sortState, setSortState] = useState(defaultSort)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sort: sort ?? defaultSort,
|
sort: disableQueryString ? sortState : sort ?? defaultSort,
|
||||||
query: queryState ?? '',
|
query: queryState ?? '',
|
||||||
setSort,
|
setSort: disableQueryString ? setSortState : setSort,
|
||||||
setQuery,
|
setQuery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { sortBy } from 'lodash'
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
import { useContracts } from 'web/hooks/use-contracts'
|
import { useContracts } from 'web/hooks/use-contracts'
|
||||||
import {
|
import {
|
||||||
|
QuerySortOptions,
|
||||||
Sort,
|
Sort,
|
||||||
useQueryAndSortParams,
|
useQueryAndSortParams,
|
||||||
} from 'web/hooks/use-sort-and-query-params'
|
} from 'web/hooks/use-sort-and-query-params'
|
||||||
|
@ -11,10 +12,7 @@ import {
|
||||||
const MAX_CONTRACTS_RENDERED = 100
|
const MAX_CONTRACTS_RENDERED = 100
|
||||||
|
|
||||||
export default function ContractSearchFirestore(props: {
|
export default function ContractSearchFirestore(props: {
|
||||||
querySortOptions?: {
|
querySortOptions?: QuerySortOptions
|
||||||
defaultSort: Sort
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}
|
|
||||||
additionalFilter?: {
|
additionalFilter?: {
|
||||||
creatorId?: string
|
creatorId?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
|
Loading…
Reference in New Issue
Block a user