diff --git a/common/fold.ts b/common/fold.ts
index c9df3389..46be7499 100644
--- a/common/fold.ts
+++ b/common/fold.ts
@@ -2,6 +2,7 @@ export type Fold = {
id: string
slug: string
name: string
+ about: string
curatorId: string // User id
createdTime: number
diff --git a/functions/src/create-fold.ts b/functions/src/create-fold.ts
index eb75a762..a653fa93 100644
--- a/functions/src/create-fold.ts
+++ b/functions/src/create-fold.ts
@@ -12,6 +12,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
name: string
+ about: string
tags: string[]
},
context
@@ -22,7 +23,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
const creator = await getUser(userId)
if (!creator) return { status: 'error', message: 'User not found' }
- const { name, tags } = data
+ const { name, about, tags } = data
if (!name || typeof name !== 'string')
return { status: 'error', message: 'Name must be a non-empty string' }
@@ -35,7 +36,9 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
creator.username,
'named',
name,
- 'on',
+ 'about',
+ about,
+ 'tags',
tags
)
@@ -48,6 +51,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
curatorId: userId,
slug,
name,
+ about,
tags,
createdTime: Date.now(),
contractIds: [],
diff --git a/web/components/edit-fold-button.tsx b/web/components/edit-fold-button.tsx
new file mode 100644
index 00000000..3ac2504d
--- /dev/null
+++ b/web/components/edit-fold-button.tsx
@@ -0,0 +1,128 @@
+import { useState } from 'react'
+import _ from 'lodash'
+import clsx from 'clsx'
+import { PencilIcon } from '@heroicons/react/outline'
+
+import { Fold } from '../../common/fold'
+import { parseWordsAsTags } from '../../common/util/parse'
+import { updateFold } from '../lib/firebase/folds'
+import { toCamelCase } from '../lib/util/format'
+import { Spacer } from './layout/spacer'
+import { TagsList } from './tags-list'
+
+export function EditFoldButton(props: { fold: Fold }) {
+ const { fold } = props
+ const [name, setName] = useState(fold.name)
+ const [about, setAbout] = useState(fold.about ?? '')
+
+ const initialOtherTags =
+ fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? ''
+
+ const [otherTags, setOtherTags] = useState(initialOtherTags)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags)
+
+ const saveDisabled =
+ name === fold.name &&
+ _.isEqual(tags, fold.tags) &&
+ about === (fold.about ?? '')
+
+ const onSubmit = async () => {
+ setIsSubmitting(true)
+
+ await updateFold(fold, {
+ name,
+ about,
+ tags,
+ })
+
+ setIsSubmitting(false)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ setName(e.target.value || '')}
+ />
+
+
+
+
+
+
+
+ setAbout(e.target.value || '')}
+ />
+
+
+
+
+
+
+
+ setOtherTags(e.target.value || '')}
+ />
+
+
+
+
`#${tag}`)} noLink />
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/components/fold-back.tsx b/web/components/fold-back.tsx
deleted file mode 100644
index 374e13e3..00000000
--- a/web/components/fold-back.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ArrowCircleLeftIcon } from '@heroicons/react/outline'
-import { Fold } from '../../common/fold'
-import { foldPath } from '../lib/firebase/folds'
-import { SiteLink } from './site-link'
-
-export function FoldBack(props: { fold: Fold }) {
- const { fold } = props
- return (
-
- {' '}
- {fold.name}
-
- )
-}
diff --git a/web/components/layout/col.tsx b/web/components/layout/col.tsx
index 128b13f4..d5f005ca 100644
--- a/web/components/layout/col.tsx
+++ b/web/components/layout/col.tsx
@@ -1,7 +1,16 @@
import clsx from 'clsx'
+import { CSSProperties } from 'react'
-export function Col(props: { children?: any; className?: string }) {
- const { children, className } = props
+export function Col(props: {
+ children?: any
+ className?: string
+ style?: CSSProperties
+}) {
+ const { children, className, style } = props
- return {children}
+ return (
+
+ {children}
+
+ )
}
diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx
index 27cf1c27..5ab792b1 100644
--- a/web/components/leaderboard.tsx
+++ b/web/components/leaderboard.tsx
@@ -1,3 +1,4 @@
+import clsx from 'clsx'
import { User } from '../../common/user'
import { Row } from './layout/row'
import { SiteLink } from './site-link'
@@ -10,10 +11,11 @@ export function Leaderboard(props: {
header: string
renderCell: (user: User) => any
}[]
+ className?: string
}) {
- const { title, users, columns } = props
+ const { title, users, columns, className } = props
return (
-
+
@@ -40,7 +42,7 @@ export function Leaderboard(props: {
width={32}
height={32}
/>
- {user.name}
+ {user.name}
diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts
index 1776e46f..a0fc7de8 100644
--- a/web/lib/firebase/api-call.ts
+++ b/web/lib/firebase/api-call.ts
@@ -11,7 +11,7 @@ export const cloudFunction = (name: string) =>
export const createContract = cloudFunction('createContract')
export const createFold = cloudFunction<
- { name: string; tags: string[] },
+ { name: string; about: string; tags: string[] },
{ status: 'error' | 'success'; message?: string; fold?: Fold }
>('createFold')
diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx
new file mode 100644
index 00000000..c73be9b8
--- /dev/null
+++ b/web/pages/fold/[...slugs]/index.tsx
@@ -0,0 +1,312 @@
+import _ from 'lodash'
+import Link from 'next/link'
+
+import { Fold } from '../../../../common/fold'
+import { Comment } from '../../../../common/comment'
+import { Page } from '../../../components/page'
+import { Title } from '../../../components/title'
+import { Bet, listAllBets } from '../../../lib/firebase/bets'
+import { listAllComments } from '../../../lib/firebase/comments'
+import { Contract } from '../../../lib/firebase/contracts'
+import {
+ foldPath,
+ getFoldBySlug,
+ getFoldContracts,
+} from '../../../lib/firebase/folds'
+import { ActivityFeed, findActiveContracts } from '../../activity'
+import { TagsList } from '../../../components/tags-list'
+import { Row } from '../../../components/layout/row'
+import { UserLink } from '../../../components/user-page'
+import { getUser, User } from '../../../lib/firebase/users'
+import { Spacer } from '../../../components/layout/spacer'
+import { Col } from '../../../components/layout/col'
+import { useUser } from '../../../hooks/use-user'
+import { useFold } from '../../../hooks/use-fold'
+import { SearchableGrid } from '../../../components/contracts-list'
+import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params'
+import { useRouter } from 'next/router'
+import clsx from 'clsx'
+import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
+import { Leaderboard } from '../../../components/leaderboard'
+import { formatMoney } from '../../../lib/util/format'
+import { EditFoldButton } from '../../../components/edit-fold-button'
+
+export async function getStaticProps(props: { params: { slugs: string[] } }) {
+ const { slugs } = props.params
+
+ const fold = await getFoldBySlug(slugs[0])
+ const curatorPromise = fold ? getUser(fold.curatorId) : null
+
+ const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
+ const contractComments = await Promise.all(
+ contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
+ )
+
+ let activeContracts = findActiveContracts(
+ contracts,
+ _.flatten(contractComments),
+ 365
+ )
+ const [resolved, unresolved] = _.partition(
+ activeContracts,
+ ({ isResolved }) => isResolved
+ )
+ activeContracts = [...unresolved, ...resolved]
+
+ const activeContractBets = await Promise.all(
+ activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
+ )
+ const activeContractComments = activeContracts.map(
+ (contract) =>
+ contractComments[contracts.findIndex((c) => c.id === contract.id)]
+ )
+
+ const curator = await curatorPromise
+
+ const bets = await Promise.all(
+ contracts.map((contract) => listAllBets(contract.id))
+ )
+
+ const creatorScores = scoreCreators(contracts, bets)
+ const [topCreators, topCreatorScores] = await toUserScores(creatorScores)
+
+ const traderScores = scoreTraders(contracts, bets)
+ const [topTraders, topTraderScores] = await toUserScores(traderScores)
+
+ return {
+ props: {
+ fold,
+ curator,
+ contracts,
+ activeContracts,
+ activeContractBets,
+ activeContractComments,
+ topTraders,
+ topTraderScores,
+ topCreators,
+ topCreatorScores,
+ },
+
+ revalidate: 60, // regenerate after a minute
+ }
+}
+
+async function toUserScores(userScores: { [userId: string]: number }) {
+ const topUserPairs = _.take(
+ _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
+ 10
+ )
+ const topUsers = await Promise.all(
+ topUserPairs.map(([userId]) => getUser(userId))
+ )
+ const topUserScores = topUserPairs.map(([_, score]) => score)
+ return [topUsers, topUserScores] as const
+}
+
+export async function getStaticPaths() {
+ return { paths: [], fallback: 'blocking' }
+}
+
+export default function FoldPage(props: {
+ fold: Fold
+ curator: User
+ contracts: Contract[]
+ activeContracts: Contract[]
+ activeContractBets: Bet[][]
+ activeContractComments: Comment[][]
+ topTraders: User[]
+ topTraderScores: number[]
+ topCreators: User[]
+ topCreatorScores: number[]
+ params: { tab: string }
+}) {
+ const {
+ curator,
+ contracts,
+ activeContracts,
+ activeContractBets,
+ activeContractComments,
+ topTraders,
+ topTraderScores,
+ topCreators,
+ topCreatorScores,
+ } = props
+
+ const router = useRouter()
+ const { slugs } = router.query as { slugs: string[] }
+ const page =
+ (slugs[1] as 'markets' | 'leaderboards' | undefined) ?? 'activity'
+
+ const fold = useFold(props.fold.id) ?? props.fold
+ const { curatorId } = fold
+
+ const { query, setQuery, sort, setSort } = useQueryAndSortParams({
+ defaultSort: 'most-traded',
+ })
+
+ const user = useUser()
+ const isCurator = user?.id === curatorId
+
+ return (
+
+
+
+
+
+ {isCurator && }
+
+
+
+
+ {page === 'activity' && (
+
+
+
+
+
+
+
+
+
+ )}
+
+ {page === 'markets' && (
+
+
+
+ )}
+
+ {page === 'leaderboards' && (
+
+
+
+ )}
+
+
+
+ )
+}
+
+function FoldOverview(props: { fold: Fold; curator: User }) {
+ const { fold, curator } = props
+ const { about, tags } = fold
+
+ return (
+
+
+ About community
+
+
+
+ Curated by
+
+
+
+ {about && (
+ <>
+
+ {about}
+ >
+ )}
+
+
+
+ `#${tag}`)} />
+
+
+ )
+}
+
+function FoldLeaderboards(props: {
+ topTraders: User[]
+ topTraderScores: number[]
+ topCreators: User[]
+ topCreatorScores: number[]
+}) {
+ const { topTraders, topTraderScores, topCreators, topCreatorScores } = props
+ return (
+ <>
+
+ formatMoney(topTraderScores[topTraders.indexOf(user)]),
+ },
+ ]}
+ />
+
+ formatMoney(topCreatorScores[topCreators.indexOf(user)]),
+ },
+ ]}
+ />
+ >
+ )
+}
diff --git a/web/pages/fold/[foldSlug]/edit.tsx b/web/pages/fold/[foldSlug]/edit.tsx
deleted file mode 100644
index c89992ed..00000000
--- a/web/pages/fold/[foldSlug]/edit.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import clsx from 'clsx'
-import _ from 'lodash'
-import { ArrowCircleLeftIcon } from '@heroicons/react/solid'
-import { useState } from 'react'
-import { Fold } from '../../../../common/fold'
-import { parseWordsAsTags } from '../../../../common/util/parse'
-import { Col } from '../../../components/layout/col'
-import { Spacer } from '../../../components/layout/spacer'
-import { Page } from '../../../components/page'
-import { TagsList } from '../../../components/tags-list'
-import {
- foldPath,
- getFoldBySlug,
- updateFold,
-} from '../../../lib/firebase/folds'
-import Custom404 from '../../404'
-import { SiteLink } from '../../../components/site-link'
-import { toCamelCase } from '../../../lib/util/format'
-import { useFold } from '../../../hooks/use-fold'
-
-export async function getStaticProps(props: { params: { foldSlug: string } }) {
- const { foldSlug } = props.params
-
- const fold = await getFoldBySlug(foldSlug)
-
- return {
- props: { fold },
-
- revalidate: 60, // regenerate after a minute
- }
-}
-
-export async function getStaticPaths() {
- return { paths: [], fallback: 'blocking' }
-}
-
-export default function EditFoldPage(props: { fold: Fold | null }) {
- const fold = useFold(props.fold?.id ?? '') ?? props.fold
- const [name, setName] = useState(fold?.name ?? '')
-
- const initialOtherTags =
- fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? ''
-
- const [otherTags, setOtherTags] = useState(initialOtherTags)
- const [isSubmitting, setIsSubmitting] = useState(false)
-
- if (!fold) return
-
- const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags)
-
- const saveDisabled =
- !name || (name === fold.name && _.isEqual(tags, fold.tags))
-
- const onSubmit = async () => {
- setIsSubmitting(true)
-
- await updateFold(fold, {
- name,
- tags,
- })
-
- setIsSubmitting(false)
- }
-
- return (
-
-
-
-
- {' '}
- {fold.name}
-
-
-
-
-
-
-
- setName(e.target.value || '')}
- />
-
-
-
-
-
-
-
- setOtherTags(e.target.value || '')}
- />
-
-
-
- `#${tag}`)} noLink />
-
-
-
-
-
-
- )
-}
diff --git a/web/pages/fold/[foldSlug]/index.tsx b/web/pages/fold/[foldSlug]/index.tsx
deleted file mode 100644
index 593a8199..00000000
--- a/web/pages/fold/[foldSlug]/index.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import _ from 'lodash'
-import { Fold } from '../../../../common/fold'
-import { Comment } from '../../../../common/comment'
-import { Page } from '../../../components/page'
-import { Title } from '../../../components/title'
-import { Bet, listAllBets } from '../../../lib/firebase/bets'
-import { listAllComments } from '../../../lib/firebase/comments'
-import { Contract } from '../../../lib/firebase/contracts'
-import {
- foldPath,
- getFoldBySlug,
- getFoldContracts,
-} from '../../../lib/firebase/folds'
-import { ActivityFeed, findActiveContracts } from '../../activity'
-import { TagsList } from '../../../components/tags-list'
-import { Row } from '../../../components/layout/row'
-import { UserLink } from '../../../components/user-page'
-import { getUser, User } from '../../../lib/firebase/users'
-import { Spacer } from '../../../components/layout/spacer'
-import { Col } from '../../../components/layout/col'
-import { SiteLink } from '../../../components/site-link'
-import { useUser } from '../../../hooks/use-user'
-import { useFold } from '../../../hooks/use-fold'
-
-export async function getStaticProps(props: { params: { foldSlug: string } }) {
- const { foldSlug } = props.params
-
- const fold = await getFoldBySlug(foldSlug)
- const curatorPromise = fold ? getUser(fold.curatorId) : null
-
- const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
- const contractComments = await Promise.all(
- contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
- )
-
- let activeContracts = findActiveContracts(
- contracts,
- _.flatten(contractComments),
- 365
- )
- const [resolved, unresolved] = _.partition(
- activeContracts,
- ({ isResolved }) => isResolved
- )
- activeContracts = [...unresolved, ...resolved]
-
- const activeContractBets = await Promise.all(
- activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
- )
- const activeContractComments = activeContracts.map(
- (contract) =>
- contractComments[contracts.findIndex((c) => c.id === contract.id)]
- )
-
- const curator = await curatorPromise
-
- return {
- props: {
- fold,
- curator,
- activeContracts,
- activeContractBets,
- activeContractComments,
- },
-
- revalidate: 60, // regenerate after a minute
- }
-}
-
-export async function getStaticPaths() {
- return { paths: [], fallback: 'blocking' }
-}
-
-export default function FoldPage(props: {
- fold: Fold
- curator: User
- activeContracts: Contract[]
- activeContractBets: Bet[][]
- activeContractComments: Comment[][]
-}) {
- const {
- curator,
- activeContracts,
- activeContractBets,
- activeContractComments,
- } = props
-
- const fold = useFold(props.fold.id) ?? props.fold
- const { tags, curatorId } = fold
-
- const user = useUser()
- const isCurator = user?.id === curatorId
-
- return (
-
-
-
-
-
-
-
- Markets
-
- •
-
- Leaderboards
-
- •
-
- Curated by
-
-
- {isCurator && (
- <>
- •
-
- Edit
-
- >
- )}
-
-
-
-
- `#${tag}`)} />
-
-
-
-
-
-
-
- )
-}
diff --git a/web/pages/fold/[foldSlug]/leaderboards.tsx b/web/pages/fold/[foldSlug]/leaderboards.tsx
deleted file mode 100644
index ea89a5ed..00000000
--- a/web/pages/fold/[foldSlug]/leaderboards.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import _ from 'lodash'
-import { ArrowCircleLeftIcon } from '@heroicons/react/solid'
-
-import { Col } from '../../../components/layout/col'
-import { Leaderboard } from '../../../components/leaderboard'
-import { Page } from '../../../components/page'
-import { SiteLink } from '../../../components/site-link'
-import { formatMoney } from '../../../lib/util/format'
-import {
- foldPath,
- getFoldBySlug,
- getFoldContracts,
-} from '../../../lib/firebase/folds'
-import { Fold } from '../../../../common/fold'
-import { Spacer } from '../../../components/layout/spacer'
-import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
-import { getUser, User } from '../../../lib/firebase/users'
-import { listAllBets } from '../../../lib/firebase/bets'
-
-export async function getStaticProps(props: { params: { foldSlug: string } }) {
- const { foldSlug } = props.params
-
- const fold = await getFoldBySlug(foldSlug)
- const contracts = fold ? await getFoldContracts(fold) : []
- const bets = await Promise.all(
- contracts.map((contract) => listAllBets(contract.id))
- )
-
- const creatorScores = scoreCreators(contracts, bets)
- const [topCreators, topCreatorScores] = await toUserScores(creatorScores)
-
- const traderScores = scoreTraders(contracts, bets)
- const [topTraders, topTraderScores] = await toUserScores(traderScores)
-
- return {
- props: { fold, topTraders, topTraderScores, topCreators, topCreatorScores },
-
- revalidate: 60, // regenerate after 60 seconds
- }
-}
-
-export async function getStaticPaths() {
- return { paths: [], fallback: 'blocking' }
-}
-
-async function toUserScores(userScores: { [userId: string]: number }) {
- const topUserPairs = _.take(
- _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
- 10
- )
- const topUsers = await Promise.all(
- topUserPairs.map(([userId]) => getUser(userId))
- )
- const topUserScores = topUserPairs.map(([_, score]) => score)
- return [topUsers, topUserScores] as const
-}
-
-export default function Leaderboards(props: {
- fold: Fold
- topTraders: User[]
- topTraderScores: number[]
- topCreators: User[]
- topCreatorScores: number[]
-}) {
- const { fold, topTraders, topTraderScores, topCreators, topCreatorScores } =
- props
- return (
-
-
- {' '}
- {fold.name}
-
-
-
-
-
-
- formatMoney(topTraderScores[topTraders.indexOf(user)]),
- },
- ]}
- />
-
- formatMoney(topCreatorScores[topCreators.indexOf(user)]),
- },
- ]}
- />
-
-
- )
-}
diff --git a/web/pages/fold/[foldSlug]/markets.tsx b/web/pages/fold/[foldSlug]/markets.tsx
deleted file mode 100644
index 667cb68a..00000000
--- a/web/pages/fold/[foldSlug]/markets.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import _ from 'lodash'
-import { Contract } from '../../../../common/contract'
-import { Fold } from '../../../../common/fold'
-import { SearchableGrid } from '../../../components/contracts-list'
-import { FoldBack } from '../../../components/fold-back'
-import { Spacer } from '../../../components/layout/spacer'
-import { Page } from '../../../components/page'
-import { SEO } from '../../../components/SEO'
-import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params'
-import { getFoldBySlug, getFoldContracts } from '../../../lib/firebase/folds'
-
-export async function getStaticProps(props: { params: { foldSlug: string } }) {
- const { foldSlug } = props.params
-
- const fold = await getFoldBySlug(foldSlug)
- const contracts = fold ? await getFoldContracts(fold) : []
-
- return {
- props: {
- fold,
- contracts,
- },
-
- revalidate: 60, // regenerate after a minute
- }
-}
-
-export async function getStaticPaths() {
- return { paths: [], fallback: 'blocking' }
-}
-
-export default function Markets(props: { fold: Fold; contracts: Contract[] }) {
- const { fold, contracts } = props
- const { query, setQuery, sort, setSort } = useQueryAndSortParams({
- defaultSort: 'most-traded',
- })
-
- return (
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/web/pages/folds.tsx b/web/pages/folds.tsx
index 7d86832a..d6be19e9 100644
--- a/web/pages/folds.tsx
+++ b/web/pages/folds.tsx
@@ -103,6 +103,7 @@ export default function Folds(props: {
function CreateFoldButton() {
const [name, setName] = useState('')
+ const [about, setAbout] = useState('')
const [otherTags, setOtherTags] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -120,6 +121,7 @@ function CreateFoldButton() {
const result = await createFold({
name,
tags,
+ about,
}).then((r) => r.data || {})
if (result.fold) {
@@ -145,7 +147,7 @@ function CreateFoldButton() {
}}
submitBtn={{
label: 'Create',
- className: clsx(name ? 'btn-primary' : 'btn-disabled'),
+ className: clsx(name && about ? 'btn-primary' : 'btn-disabled'),
}}
onSubmit={onSubmit}
>
@@ -175,37 +177,50 @@ function CreateFoldButton() {
- {name && (
- <>
-
-
+
+
-
+ setAbout(e.target.value || '')}
+ />
+
-
-
+
- setOtherTags(e.target.value || '')}
- />
-
+
+
-
+
- `#${tag}`)}
- noLink
- />
- >
- )}
+
+
+
+ setOtherTags(e.target.value || '')}
+ />
+
+
+
+
+ `#${tag}`)}
+ noLink
+ />
)