From 4e1fae5b5f2e75bd379d4688bf8761c270b1d5d8 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 14 Aug 2022 20:51:10 -0500 Subject: [PATCH 1/9] 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 Date: Sun, 14 Aug 2022 23:55:11 -0500 Subject: [PATCH 2/9] 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: {
+ <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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 - - - - - - - + + + + + + - - - + + - + + + - - - -
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- -
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-

- 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( From 34e8138e5096e1ad8a4944566328703bd080c645 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Mon, 15 Aug 2022 16:33:02 -0700 Subject: [PATCH 9/9] Show placeholder when avatarUrl errors --- web/components/avatar.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 19b6066e..6ca06cbb 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -1,6 +1,6 @@ import Router from 'next/router' import clsx from 'clsx' -import { MouseEvent } from 'react' +import { MouseEvent, useState } from 'react' import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid' export function Avatar(props: { @@ -10,7 +10,8 @@ export function Avatar(props: { size?: number | 'xs' | 'sm' className?: string }) { - const { username, avatarUrl, noLink, size, className } = props + const { username, noLink, size, className } = props + const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl) const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10 const onClick = @@ -35,6 +36,11 @@ export function Avatar(props: { src={avatarUrl} onClick={onClick} alt={username} + onError={() => { + // If the image doesn't load, clear the avatarUrl to show the default + // Mostly for localhost, when getting a 403 from googleusercontent + setAvatarUrl('') + }} /> ) : (
+ + + +
+
+ + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

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 7/9] 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 8/9] 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 )} > -