Folds (#34)
* Fold type, fold page, query for fold contracts * Tsconfig: target esnext, nounused locals: false * Store tags in field on contract. Script to update contract tags * Show tags on fold page * Load all fold comments server-side to serve better feed * Fix the annoying firebase already initialized error! * Add links to /edit and /leaderboards for fold * Page with list of folds * UI for creating a fold * Create a fold * Edit fold page
This commit is contained in:
parent
5be6a75e4b
commit
60f68b178d
|
@ -9,6 +9,7 @@ export type Contract = {
|
||||||
|
|
||||||
question: string
|
question: string
|
||||||
description: string // More info about what the contract is about
|
description: string // More info about what the contract is about
|
||||||
|
tags: string[]
|
||||||
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
|
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
|
||||||
// outcomes: ['YES', 'NO']
|
// outcomes: ['YES', 'NO']
|
||||||
visibility: 'public' | 'unlisted'
|
visibility: 'public' | 'unlisted'
|
||||||
|
|
17
common/fold.ts
Normal file
17
common/fold.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export type Fold = {
|
||||||
|
id: string
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
curatorId: string // User id
|
||||||
|
createdTime: number
|
||||||
|
|
||||||
|
tags: string[]
|
||||||
|
|
||||||
|
contractIds: string[]
|
||||||
|
excludedContractIds: string[]
|
||||||
|
|
||||||
|
// Invariant: exactly one of the following is defined.
|
||||||
|
// Default: creatorIds: undefined, excludedCreatorIds: []
|
||||||
|
creatorIds?: string[]
|
||||||
|
excludedCreatorIds?: string[]
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { calcStartPool } from './antes'
|
||||||
|
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
import { parseTags } from './util/parse'
|
||||||
|
|
||||||
export function getNewContract(
|
export function getNewContract(
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -28,6 +29,7 @@ export function getNewContract(
|
||||||
|
|
||||||
question: question.trim(),
|
question: question.trim(),
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
|
tags: parseTags(`${question} ${description}`),
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
|
|
||||||
mechanism: 'dpm-2',
|
mechanism: 'dpm-2',
|
||||||
|
|
21
common/util/parse.ts
Normal file
21
common/util/parse.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export function parseTags(text: string) {
|
||||||
|
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||||
|
const matches = (text.match(regex) || []).map((match) =>
|
||||||
|
match.trim().substring(1)
|
||||||
|
)
|
||||||
|
const tagSet = new Set(matches)
|
||||||
|
const uniqueTags: string[] = []
|
||||||
|
tagSet.forEach((tag) => uniqueTags.push(tag))
|
||||||
|
return uniqueTags
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWordsAsTags(text: string) {
|
||||||
|
const regex = /(?:^|\s)(?:[a-z0-9_]+)/gi
|
||||||
|
const matches = (text.match(regex) || [])
|
||||||
|
.map((match) => match.trim())
|
||||||
|
.filter((tag) => tag)
|
||||||
|
const tagSet = new Set(matches)
|
||||||
|
const uniqueTags: string[] = []
|
||||||
|
tagSet.forEach((tag) => uniqueTags.push(tag))
|
||||||
|
return uniqueTags
|
||||||
|
}
|
|
@ -35,5 +35,9 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /folds/{foldId} {
|
||||||
|
allow read;
|
||||||
|
allow update: if request.auth.uid == resource.data.curatorId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
81
functions/src/create-fold.ts
Normal file
81
functions/src/create-fold.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
import { getUser } from './utils'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { slugify } from '../../common/util/slugify'
|
||||||
|
import { randomString } from '../../common/util/random'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
|
||||||
|
export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
name: string
|
||||||
|
tags: string[]
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const creator = await getUser(userId)
|
||||||
|
if (!creator) return { status: 'error', message: 'User not found' }
|
||||||
|
|
||||||
|
const { name, tags } = data
|
||||||
|
|
||||||
|
if (!name || typeof name !== 'string')
|
||||||
|
return { status: 'error', message: 'Name must be a non-empty string' }
|
||||||
|
|
||||||
|
if (!_.isArray(tags))
|
||||||
|
return { status: 'error', message: 'Tags must be an array of strings' }
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'creating fold for',
|
||||||
|
creator.username,
|
||||||
|
'named',
|
||||||
|
name,
|
||||||
|
'on',
|
||||||
|
tags
|
||||||
|
)
|
||||||
|
|
||||||
|
const slug = await getSlug(name)
|
||||||
|
|
||||||
|
const foldRef = firestore.collection('folds').doc()
|
||||||
|
|
||||||
|
const fold: Fold = {
|
||||||
|
id: foldRef.id,
|
||||||
|
curatorId: userId,
|
||||||
|
slug,
|
||||||
|
name,
|
||||||
|
tags,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
contractIds: [],
|
||||||
|
excludedContractIds: [],
|
||||||
|
excludedCreatorIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await foldRef.create(fold)
|
||||||
|
|
||||||
|
return { status: 'success', fold }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getSlug = async (name: string) => {
|
||||||
|
const proposedSlug = slugify(name)
|
||||||
|
|
||||||
|
const preexistingFold = await getFoldFromSlug(proposedSlug)
|
||||||
|
|
||||||
|
return preexistingFold ? proposedSlug + '-' + randomString() : proposedSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export async function getFoldFromSlug(slug: string) {
|
||||||
|
const snap = await firestore
|
||||||
|
.collection('folds')
|
||||||
|
.where('slug', '==', slug)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ export * from './stripe'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
|
export * from './create-fold'
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
export * from './update-contract-metrics'
|
export * from './update-contract-metrics'
|
||||||
export * from './update-user-metrics'
|
export * from './update-user-metrics'
|
||||||
|
|
49
functions/src/scripts/update-contract-tags.ts
Normal file
49
functions/src/scripts/update-contract-tags.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
// Generate your own private key, and set the path below:
|
||||||
|
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
|
// James:
|
||||||
|
const serviceAccount = require('/Users/jahooma/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
|
||||||
|
// Stephen:
|
||||||
|
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount),
|
||||||
|
})
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
import { Contract } from '../../../common/contract'
|
||||||
|
import { parseTags } from '../../../common/util/parse'
|
||||||
|
import { getValues } from '../utils'
|
||||||
|
|
||||||
|
async function updateContractTags() {
|
||||||
|
console.log('Updating contracts tags')
|
||||||
|
|
||||||
|
const contracts = await getValues<Contract>(firestore.collection('contracts'))
|
||||||
|
|
||||||
|
console.log('Loaded', contracts.length, 'contracts')
|
||||||
|
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||||
|
|
||||||
|
const tags = _.uniq([
|
||||||
|
...parseTags(contract.question + contract.description),
|
||||||
|
...(contract.tags ?? []),
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Updating tags',
|
||||||
|
contract.slug,
|
||||||
|
'from',
|
||||||
|
contract.tags,
|
||||||
|
'to',
|
||||||
|
tags
|
||||||
|
)
|
||||||
|
|
||||||
|
await contractRef.update({
|
||||||
|
tags,
|
||||||
|
} as Partial<Contract>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) updateContractTags().then(() => process.exit())
|
|
@ -2,7 +2,6 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noUnusedLocals": true,
|
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
@ -3,17 +3,17 @@ import Link from 'next/link'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
import { formatMoney } from '../lib/util/format'
|
import { formatMoney } from '../lib/util/format'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
import { Linkify } from './linkify'
|
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
contractPath,
|
contractPath,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { parseTags } from '../lib/util/parse'
|
import { parseTags } from '../../common/util/parse'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { TrendingUpIcon, ClockIcon } from '@heroicons/react/solid'
|
import { TrendingUpIcon, ClockIcon } from '@heroicons/react/solid'
|
||||||
import { DateTimeTooltip } from './datetime-tooltip'
|
import { DateTimeTooltip } from './datetime-tooltip'
|
||||||
|
import { TagsList } from './tags-list'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -196,14 +196,7 @@ export function ContractDetails(props: { contract: Contract }) {
|
||||||
{tags.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">•</div>
|
<div className="hidden sm:block">•</div>
|
||||||
|
<TagsList tags={tags} />
|
||||||
<Row className="gap-2 flex-wrap">
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<div key={tag} className="bg-gray-100 px-1">
|
|
||||||
<Linkify text={tag} gray />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
import { User } from '../lib/firebase/users'
|
import { User } from '../lib/firebase/users'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
import { parseTags } from '../lib/util/parse'
|
import { parseTags } from '../../common/util/parse'
|
||||||
import { ContractCard } from './contract-card'
|
import { ContractCard } from './contract-card'
|
||||||
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||||
|
|
||||||
|
|
58
web/components/leaderboard.tsx
Normal file
58
web/components/leaderboard.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { SiteLink } from './site-link'
|
||||||
|
import { Title } from './title'
|
||||||
|
|
||||||
|
export function Leaderboard(props: {
|
||||||
|
title: string
|
||||||
|
users: User[]
|
||||||
|
columns: {
|
||||||
|
header: string
|
||||||
|
renderCell: (user: User) => any
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
|
const { title, users, columns } = props
|
||||||
|
return (
|
||||||
|
<div className="max-w-xl w-full px-1">
|
||||||
|
<Title text={title} />
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table table-zebra table-compact text-gray-500 w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="p-2">
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<th key={column.header}>{column.header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user, index) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>
|
||||||
|
<SiteLink className="relative" href={`/${user.username}`}>
|
||||||
|
<Row className="items-center gap-4">
|
||||||
|
<Image
|
||||||
|
className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50"
|
||||||
|
src={user.avatarUrl || ''}
|
||||||
|
alt=""
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<div>{user.name}</div>
|
||||||
|
</Row>
|
||||||
|
</SiteLink>
|
||||||
|
</td>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<td key={column.header}>{column.renderCell(user)}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -57,6 +57,17 @@ function NavOptions(props: { user: User | null; themeClasses: string }) {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* <Link href="/folds">
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'text-base hidden md:block whitespace-nowrap',
|
||||||
|
themeClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Folds
|
||||||
|
</a>
|
||||||
|
</Link> */}
|
||||||
|
|
||||||
<Link href="/markets">
|
<Link href="/markets">
|
||||||
<a
|
<a
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
15
web/components/tags-list.tsx
Normal file
15
web/components/tags-list.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { Linkify } from './linkify'
|
||||||
|
|
||||||
|
export function TagsList(props: { tags: string[] }) {
|
||||||
|
const { tags } = props
|
||||||
|
return (
|
||||||
|
<Row className="gap-2 flex-wrap text-sm text-gray-500">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<div key={tag} className="bg-gray-100 px-1">
|
||||||
|
<Linkify text={tag} gray />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,20 @@
|
||||||
import { getFunctions, httpsCallable } from 'firebase/functions'
|
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||||
|
import { Fold } from '../../../common/fold'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
import { randomString } from '../../../common/util/random'
|
import { randomString } from '../../../common/util/random'
|
||||||
|
|
||||||
const functions = getFunctions()
|
const functions = getFunctions()
|
||||||
|
|
||||||
export const cloudFunction = (name: string) => httpsCallable(functions, name)
|
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
|
||||||
|
httpsCallable<RequestData, ResponseData>(functions, name)
|
||||||
|
|
||||||
export const createContract = cloudFunction('createContract')
|
export const createContract = cloudFunction('createContract')
|
||||||
|
|
||||||
|
export const createFold = cloudFunction<
|
||||||
|
{ name: string; tags: string[] },
|
||||||
|
{ status: 'error' | 'success'; message?: string; fold?: Fold }
|
||||||
|
>('createFold')
|
||||||
|
|
||||||
export const placeBet = cloudFunction('placeBet')
|
export const placeBet = cloudFunction('placeBet')
|
||||||
|
|
||||||
export const resolveMarket = cloudFunction('resolveMarket')
|
export const resolveMarket = cloudFunction('resolveMarket')
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
setDoc,
|
setDoc,
|
||||||
query,
|
query,
|
||||||
collectionGroup,
|
collectionGroup,
|
||||||
getDocs,
|
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
@ -75,9 +74,7 @@ const recentCommentsQuery = query(
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function getRecentComments() {
|
export async function getRecentComments() {
|
||||||
const snapshot = await getDocs(recentCommentsQuery)
|
return getValues<Comment>(recentCommentsQuery)
|
||||||
const comments = snapshot.docs.map((doc) => doc.data() as Comment)
|
|
||||||
return comments
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForRecentComments(
|
export function listenForRecentComments(
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { createRNG, shuffle } from '../../../common/util/random'
|
||||||
export type { Contract }
|
export type { Contract }
|
||||||
|
|
||||||
export function contractPath(contract: Contract) {
|
export function contractPath(contract: Contract) {
|
||||||
// For now, derive username from creatorName
|
|
||||||
return `/${contract.creatorUsername}/${contract.slug}`
|
return `/${contract.creatorUsername}/${contract.slug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ export function contractMetrics(contract: Contract) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getFirestore(app)
|
const db = getFirestore(app)
|
||||||
const contractCollection = collection(db, 'contracts')
|
export const contractCollection = collection(db, 'contracts')
|
||||||
|
|
||||||
// Push contract to Firestore
|
// Push contract to Firestore
|
||||||
export async function setContract(contract: Contract) {
|
export async function setContract(contract: Contract) {
|
||||||
|
|
71
web/lib/firebase/folds.ts
Normal file
71
web/lib/firebase/folds.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { collection, doc, query, updateDoc, where } from 'firebase/firestore'
|
||||||
|
import { Fold } from '../../../common/fold'
|
||||||
|
import { Contract, contractCollection } from './contracts'
|
||||||
|
import { db } from './init'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
|
||||||
|
const foldCollection = collection(db, 'folds')
|
||||||
|
|
||||||
|
export function foldPath(fold: Fold, subpath?: 'edit' | 'leaderboards') {
|
||||||
|
return `/fold/${fold.slug}${subpath ? `/${subpath}` : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateFold(fold: Fold, updates: Partial<Fold>) {
|
||||||
|
return updateDoc(doc(foldCollection, fold.id), updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAllFolds() {
|
||||||
|
return getValues<Fold>(foldCollection)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFoldBySlug(slug: string) {
|
||||||
|
const q = query(foldCollection, where('slug', '==', slug))
|
||||||
|
const folds = await getValues<Fold>(q)
|
||||||
|
|
||||||
|
return folds.length === 0 ? null : folds[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFoldContracts(fold: Fold) {
|
||||||
|
const {
|
||||||
|
tags,
|
||||||
|
contractIds,
|
||||||
|
excludedContractIds,
|
||||||
|
creatorIds,
|
||||||
|
excludedCreatorIds,
|
||||||
|
} = fold
|
||||||
|
|
||||||
|
const [tagsContracts, includedContracts] = await Promise.all([
|
||||||
|
// TODO: if tags.length > 10, execute multiple parallel queries
|
||||||
|
tags.length > 0
|
||||||
|
? getValues<Contract>(
|
||||||
|
query(contractCollection, where('tags', 'array-contains-any', tags))
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
|
||||||
|
// TODO: if contractIds.length > 10, execute multiple parallel queries
|
||||||
|
contractIds.length > 0
|
||||||
|
? getValues<Contract>(
|
||||||
|
query(contractCollection, where('id', 'in', contractIds))
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
])
|
||||||
|
|
||||||
|
const excludedContractsSet = new Set(excludedContractIds)
|
||||||
|
|
||||||
|
const creatorSet = creatorIds ? new Set(creatorIds) : undefined
|
||||||
|
const excludedCreatorSet = excludedCreatorIds
|
||||||
|
? new Set(excludedCreatorIds)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const approvedContracts = tagsContracts.filter((contract) => {
|
||||||
|
const { id, creatorId } = contract
|
||||||
|
|
||||||
|
if (excludedContractsSet.has(id)) return false
|
||||||
|
if (creatorSet && !creatorSet.has(creatorId)) return false
|
||||||
|
if (excludedCreatorSet && excludedCreatorSet.has(creatorId)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...approvedContracts, ...includedContracts]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { getFirestore } from '@firebase/firestore'
|
import { getFirestore } from '@firebase/firestore'
|
||||||
import { initializeApp } from 'firebase/app'
|
import { initializeApp, getApps, getApp } from 'firebase/app'
|
||||||
|
|
||||||
// TODO: Reenable this when we have a way to set the Firebase db in dev
|
// TODO: Reenable this when we have a way to set the Firebase db in dev
|
||||||
// export const isProd = process.env.NODE_ENV === 'production'
|
// export const isProd = process.env.NODE_ENV === 'production'
|
||||||
|
@ -26,5 +26,6 @@ const firebaseConfig = isProd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Firebase
|
// Initialize Firebase
|
||||||
export const app = initializeApp(firebaseConfig)
|
export const app = getApps().length ? getApp() : initializeApp(firebaseConfig)
|
||||||
|
|
||||||
export const db = getFirestore(app)
|
export const db = getFirestore(app)
|
||||||
|
|
|
@ -8,8 +8,8 @@ import {
|
||||||
DocumentReference,
|
DocumentReference,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
|
||||||
export const getValue = async <T>(collectionName: string, docName: string) => {
|
export const getValue = async <T>(doc: DocumentReference) => {
|
||||||
const snap = await getDoc(doc(db, collectionName, docName))
|
const snap = await getDoc(doc)
|
||||||
return snap.exists() ? (snap.data() as T) : null
|
return snap.exists() ? (snap.data() as T) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
export function parseTags(text: string) {
|
|
||||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
|
||||||
const matches = (text.match(regex) || []).map((match) =>
|
|
||||||
match.trim().substring(1)
|
|
||||||
)
|
|
||||||
return _.uniqBy(matches, (tag) => tag.toLowerCase())
|
|
||||||
}
|
|
|
@ -23,7 +23,9 @@ import { Bet, listAllBets } from '../../lib/firebase/bets'
|
||||||
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
||||||
import Custom404 from '../404'
|
import Custom404 from '../404'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: any }) {
|
export async function getStaticProps(props: {
|
||||||
|
params: { username: string; contractSlug: string }
|
||||||
|
}) {
|
||||||
const { username, contractSlug } = props.params
|
const { username, contractSlug } = props.params
|
||||||
const contract = (await getContractFromSlug(contractSlug)) || null
|
const contract = (await getContractFromSlug(contractSlug)) || null
|
||||||
const contractId = contract?.id
|
const contractId = contract?.id
|
||||||
|
|
|
@ -2,8 +2,6 @@ import _ from 'lodash'
|
||||||
import { ContractFeed } from '../components/contract-feed'
|
import { ContractFeed } from '../components/contract-feed'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { useRecentComments } from '../hooks/use-comments'
|
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
|
||||||
import { Contract } from '../lib/firebase/contracts'
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
import { Comment } from '../lib/firebase/comments'
|
import { Comment } from '../lib/firebase/comments'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
|
@ -69,18 +67,13 @@ export function ActivityFeed(props: {
|
||||||
contractBets: Bet[][]
|
contractBets: Bet[][]
|
||||||
contractComments: Comment[][]
|
contractComments: Comment[][]
|
||||||
}) {
|
}) {
|
||||||
const { contractBets, contractComments } = props
|
const { contracts, contractBets, contractComments } = props
|
||||||
const contracts = useContracts() ?? props.contracts
|
|
||||||
const recentComments = useRecentComments()
|
|
||||||
const activeContracts = recentComments
|
|
||||||
? findActiveContracts(contracts, recentComments)
|
|
||||||
: props.contracts
|
|
||||||
|
|
||||||
return contracts.length > 0 ? (
|
return contracts.length > 0 ? (
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-3xl">
|
<Col className="w-full max-w-3xl">
|
||||||
<Col className="w-full bg-white self-center divide-gray-300 divide-y">
|
<Col className="w-full bg-white self-center divide-gray-300 divide-y">
|
||||||
{activeContracts.map((contract, i) => (
|
{contracts.map((contract, i) => (
|
||||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||||
<ContractFeed
|
<ContractFeed
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
116
web/pages/fold/[foldSlug]/edit.tsx
Normal file
116
web/pages/fold/[foldSlug]/edit.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
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 } = props
|
||||||
|
|
||||||
|
const [name, setName] = useState(fold?.name ?? '')
|
||||||
|
const [tags, setTags] = useState(fold?.tags.join(', ') ?? '')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
if (!fold) return <Custom404 />
|
||||||
|
|
||||||
|
const saveDisabled =
|
||||||
|
!name ||
|
||||||
|
!tags ||
|
||||||
|
(name === fold.name && _.isEqual(parseWordsAsTags(tags), fold.tags))
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
await updateFold(fold, { name, tags: parseWordsAsTags(tags) })
|
||||||
|
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Col className="items-center">
|
||||||
|
<Col className="max-w-2xl w-full px-2 sm:px-0">
|
||||||
|
<SiteLink href={foldPath(fold)}>
|
||||||
|
<ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '}
|
||||||
|
{fold.name}
|
||||||
|
</SiteLink>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">Fold name</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
placeholder="Your fold name"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">Tags</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
placeholder="Politics, Economics, Rationality"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={tags}
|
||||||
|
onChange={(e) => setTags(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
<TagsList tags={parseWordsAsTags(tags)} />
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn self-end',
|
||||||
|
saveDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
|
isSubmitting && 'loading'
|
||||||
|
)}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
128
web/pages/fold/[foldSlug]/index.tsx
Normal file
128
web/pages/fold/[foldSlug]/index.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
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) : []
|
||||||
|
const contractComments = await Promise.all(
|
||||||
|
contracts.map((contract) => listAllComments(contract.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeContracts = findActiveContracts(
|
||||||
|
contracts,
|
||||||
|
_.flatten(contractComments)
|
||||||
|
)
|
||||||
|
const activeContractBets = await Promise.all(
|
||||||
|
activeContracts.map((contract) => listAllBets(contract.id))
|
||||||
|
)
|
||||||
|
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 {
|
||||||
|
fold,
|
||||||
|
curator,
|
||||||
|
activeContracts,
|
||||||
|
activeContractBets,
|
||||||
|
activeContractComments,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const { tags, curatorId } = fold
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const isCurator = user?.id === curatorId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Col className="items-center">
|
||||||
|
<Col className="max-w-3xl w-full">
|
||||||
|
<Title text={fold.name} />
|
||||||
|
|
||||||
|
<Row className="items-center gap-2 mb-2 flex-wrap">
|
||||||
|
{isCurator && (
|
||||||
|
<>
|
||||||
|
<SiteLink className="text-sm " href={foldPath(fold, 'edit')}>
|
||||||
|
Edit
|
||||||
|
</SiteLink>
|
||||||
|
<div className="text-gray-500">•</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SiteLink className="text-sm" href={foldPath(fold, 'leaderboards')}>
|
||||||
|
Leaderboards
|
||||||
|
</SiteLink>
|
||||||
|
<div className="text-gray-500">•</div>
|
||||||
|
<Row>
|
||||||
|
<div className="text-sm text-gray-500 mr-1">Curated by</div>
|
||||||
|
<UserLink
|
||||||
|
className="text-sm text-neutral"
|
||||||
|
name={curator.name}
|
||||||
|
username={curator.username}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<TagsList tags={tags.map((tag) => `#${tag}`)} />
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<ActivityFeed
|
||||||
|
contracts={activeContracts}
|
||||||
|
contractBets={activeContractBets}
|
||||||
|
contractComments={activeContractComments}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
64
web/pages/fold/[foldSlug]/leaderboards.tsx
Normal file
64
web/pages/fold/[foldSlug]/leaderboards.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
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 } from '../../../lib/firebase/folds'
|
||||||
|
import { Fold } from '../../../../common/fold'
|
||||||
|
import { Spacer } from '../../../components/layout/spacer'
|
||||||
|
|
||||||
|
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 Leaderboards(props: { fold: Fold }) {
|
||||||
|
const { fold } = props
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<SiteLink href={foldPath(fold)}>
|
||||||
|
<ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '}
|
||||||
|
{fold.name}
|
||||||
|
</SiteLink>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<Col className="items-center lg:flex-row gap-10">
|
||||||
|
<Leaderboard
|
||||||
|
title="🏅 Top traders"
|
||||||
|
users={[]}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Total profit',
|
||||||
|
renderCell: (user) => formatMoney(user.totalPnLCached),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Leaderboard
|
||||||
|
title="🏅 Top creators"
|
||||||
|
users={[]}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Market volume',
|
||||||
|
renderCell: (user) => formatMoney(user.creatorVolumeCached),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
158
web/pages/folds.tsx
Normal file
158
web/pages/folds.tsx
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
|
import { ConfirmationButton } from '../components/confirmation-button'
|
||||||
|
import { Col } from '../components/layout/col'
|
||||||
|
import { Row } from '../components/layout/row'
|
||||||
|
import { Spacer } from '../components/layout/spacer'
|
||||||
|
import { Page } from '../components/page'
|
||||||
|
import { SiteLink } from '../components/site-link'
|
||||||
|
import { TagsList } from '../components/tags-list'
|
||||||
|
import { Title } from '../components/title'
|
||||||
|
import { UserLink } from '../components/user-page'
|
||||||
|
import { useUser } from '../hooks/use-user'
|
||||||
|
import { createFold } from '../lib/firebase/api-call'
|
||||||
|
import { foldPath, listAllFolds } from '../lib/firebase/folds'
|
||||||
|
import { getUser, User } from '../lib/firebase/users'
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
const folds = await listAllFolds().catch((_) => [])
|
||||||
|
|
||||||
|
const curators = await Promise.all(
|
||||||
|
folds.map((fold) => getUser(fold.curatorId))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
folds,
|
||||||
|
curators,
|
||||||
|
},
|
||||||
|
|
||||||
|
revalidate: 60, // regenerate after a minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Folds(props: { folds: Fold[]; curators: User[] }) {
|
||||||
|
const { folds, curators } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Col className="items-center">
|
||||||
|
<Col className="max-w-2xl w-full px-2 sm:px-0">
|
||||||
|
<Row className="justify-between items-center">
|
||||||
|
<Title text="Folds" />
|
||||||
|
{user && <CreateFoldButton />}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Col className="gap-4">
|
||||||
|
{folds.map((fold, index) => (
|
||||||
|
<Row key={fold.id} className="items-center gap-2">
|
||||||
|
<SiteLink href={foldPath(fold)}>{fold.name}</SiteLink>
|
||||||
|
<div />
|
||||||
|
<div className="text-sm text-gray-500">12 followers</div>
|
||||||
|
<div className="text-gray-500">•</div>
|
||||||
|
<Row>
|
||||||
|
<div className="text-sm text-gray-500 mr-1">Curated by</div>
|
||||||
|
<UserLink
|
||||||
|
className="text-sm text-neutral"
|
||||||
|
name={curators[index].name}
|
||||||
|
username={curators[index].username}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateFoldButton() {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [tags, setTags] = useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const result = await createFold({
|
||||||
|
name,
|
||||||
|
tags: parseWordsAsTags(tags),
|
||||||
|
}).then((r) => r.data || {})
|
||||||
|
|
||||||
|
if (result.fold) await router.push(foldPath(result.fold))
|
||||||
|
else console.log(result.status, result.message)
|
||||||
|
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationButton
|
||||||
|
id="create-fold"
|
||||||
|
openModelBtn={{
|
||||||
|
label: 'Create a fold',
|
||||||
|
className: clsx(
|
||||||
|
isSubmitting ? 'loading btn-disabled' : 'btn-primary',
|
||||||
|
'btn-sm'
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
submitBtn={{
|
||||||
|
label: 'Create',
|
||||||
|
className: clsx(name && tags ? 'btn-primary' : 'btn-disabled'),
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Title className="!mt-0" text="Create a fold" />
|
||||||
|
|
||||||
|
<Col className="text-gray-500 gap-1">
|
||||||
|
<div>A fold is a view of markets that match selected tags.</div>
|
||||||
|
<div>You can further include or exclude individual markets.</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">Fold name</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
placeholder="Your fold name"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">Tags</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
placeholder="Politics, Economics, Rationality"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={tags}
|
||||||
|
onChange={(e) => setTags(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
<TagsList tags={parseWordsAsTags(tags)} />
|
||||||
|
</form>
|
||||||
|
</ConfirmationButton>
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ import {
|
||||||
listAllComments,
|
listAllComments,
|
||||||
} from '../lib/firebase/comments'
|
} from '../lib/firebase/comments'
|
||||||
import { Bet, listAllBets } from '../lib/firebase/bets'
|
import { Bet, listAllBets } from '../lib/firebase/bets'
|
||||||
|
import { useContracts } from '../hooks/use-contracts'
|
||||||
|
import { useRecentComments } from '../hooks/use-comments'
|
||||||
import FeedCreate, { FeedPromo } from '../components/feed-create'
|
import FeedCreate, { FeedPromo } from '../components/feed-create'
|
||||||
import { Spacer } from '../components/layout/spacer'
|
import { Spacer } from '../components/layout/spacer'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
|
@ -51,12 +53,13 @@ const Home = (props: {
|
||||||
activeContractComments: Comment[][]
|
activeContractComments: Comment[][]
|
||||||
hotContracts: Contract[]
|
hotContracts: Contract[]
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { activeContractBets, activeContractComments, hotContracts } = props
|
||||||
activeContracts,
|
|
||||||
activeContractBets,
|
const contracts = useContracts() ?? props.activeContracts
|
||||||
activeContractComments,
|
const recentComments = useRecentComments()
|
||||||
hotContracts,
|
const activeContracts = recentComments
|
||||||
} = props
|
? findActiveContracts(contracts, recentComments)
|
||||||
|
: props.activeContracts
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import Image from 'next/image'
|
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { Row } from '../components/layout/row'
|
import { Leaderboard } from '../components/leaderboard'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { SiteLink } from '../components/site-link'
|
|
||||||
import { Title } from '../components/title'
|
|
||||||
import { getTopCreators, getTopTraders, User } from '../lib/firebase/users'
|
import { getTopCreators, getTopTraders, User } from '../lib/firebase/users'
|
||||||
import { formatMoney } from '../lib/util/format'
|
import { formatMoney } from '../lib/util/format'
|
||||||
|
|
||||||
|
@ -57,56 +54,3 @@ export default function Leaderboards(props: {
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Leaderboard(props: {
|
|
||||||
title: string
|
|
||||||
users: User[]
|
|
||||||
columns: {
|
|
||||||
header: string
|
|
||||||
renderCell: (user: User) => any
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
const { title, users, columns } = props
|
|
||||||
return (
|
|
||||||
<div className="max-w-xl w-full px-1">
|
|
||||||
<Title text={title} />
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="table table-zebra table-compact text-gray-500 w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="p-2">
|
|
||||||
<th>#</th>
|
|
||||||
<th>Name</th>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<th key={column.header}>{column.header}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{users.map((user, index) => (
|
|
||||||
<tr key={user.id}>
|
|
||||||
<td>{index + 1}</td>
|
|
||||||
<td>
|
|
||||||
<SiteLink className="relative" href={`/${user.username}`}>
|
|
||||||
<Row className="items-center gap-4">
|
|
||||||
<Image
|
|
||||||
className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50"
|
|
||||||
src={user.avatarUrl || ''}
|
|
||||||
alt=""
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
<div>{user.name}</div>
|
|
||||||
</Row>
|
|
||||||
</SiteLink>
|
|
||||||
</td>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<td key={column.header}>{column.renderCell(user)}</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "esnext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user