Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jahooma authored Jan 21, 2022
1 parent 5be6a75 commit 60f68b1
Show file tree
Hide file tree
Showing 30 changed files with 832 additions and 106 deletions.
1 change: 1 addition & 0 deletions common/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Contract = {

question: string
description: string // More info about what the contract is about
tags: string[]
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
// outcomes: ['YES', 'NO']
visibility: 'public' | 'unlisted'
Expand Down
17 changes: 17 additions & 0 deletions common/fold.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions common/new-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { calcStartPool } from './antes'

import { Contract } from './contract'
import { User } from './user'
import { parseTags } from './util/parse'

export function getNewContract(
id: string,
Expand All @@ -28,6 +29,7 @@ export function getNewContract(

question: question.trim(),
description: description.trim(),
tags: parseTags(`${question} ${description}`),
visibility: 'public',

mechanism: 'dpm-2',
Expand Down
21 changes: 21 additions & 0 deletions common/util/parse.ts
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@ service cloud.firestore {
allow read;
}

match /folds/{foldId} {
allow read;
allow update: if request.auth.uid == resource.data.curatorId;
}
}
}
81 changes: 81 additions & 0 deletions functions/src/create-fold.ts
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './stripe'
export * from './sell-bet'
export * from './create-contract'
export * from './create-user'
export * from './create-fold'
export * from './unsubscribe'
export * from './update-contract-metrics'
export * from './update-user-metrics'
49 changes: 49 additions & 0 deletions functions/src/scripts/update-contract-tags.ts
Original file line number Diff line number Diff line change
@@ -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())
1 change: 0 additions & 1 deletion functions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
Expand Down
13 changes: 3 additions & 10 deletions web/components/contract-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import Link from 'next/link'
import { Row } from '../components/layout/row'
import { formatMoney } from '../lib/util/format'
import { UserLink } from './user-page'
import { Linkify } from './linkify'
import {
Contract,
contractMetrics,
contractPath,
} from '../lib/firebase/contracts'
import { Col } from './layout/col'
import { parseTags } from '../lib/util/parse'
import { parseTags } from '../../common/util/parse'
import dayjs from 'dayjs'
import { TrendingUpIcon, ClockIcon } from '@heroicons/react/solid'
import { DateTimeTooltip } from './datetime-tooltip'
import { TagsList } from './tags-list'

export function ContractCard(props: {
contract: Contract
Expand Down Expand Up @@ -196,14 +196,7 @@ export function ContractDetails(props: { contract: Contract }) {
{tags.length > 0 && (
<>
<div className="hidden sm:block"></div>

<Row className="gap-2 flex-wrap">
{tags.map((tag) => (
<div key={tag} className="bg-gray-100 px-1">
<Linkify text={tag} gray />
</div>
))}
</Row>
<TagsList tags={tags} />
</>
)}
</Col>
Expand Down
2 changes: 1 addition & 1 deletion web/components/contracts-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { User } from '../lib/firebase/users'
import { Col } from './layout/col'
import { SiteLink } from './site-link'
import { parseTags } from '../lib/util/parse'
import { parseTags } from '../../common/util/parse'
import { ContractCard } from './contract-card'
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'

Expand Down
58 changes: 58 additions & 0 deletions web/components/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
11 changes: 11 additions & 0 deletions web/components/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ function NavOptions(props: { user: User | null; themeClasses: string }) {
</Link>
)}

{/* <Link href="/folds">
<a
className={clsx(
'text-base hidden md:block whitespace-nowrap',
themeClasses
)}
>
Folds
</a>
</Link> */}

<Link href="/markets">
<a
className={clsx(
Expand Down
15 changes: 15 additions & 0 deletions web/components/tags-list.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
9 changes: 8 additions & 1 deletion web/lib/firebase/api-call.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { getFunctions, httpsCallable } from 'firebase/functions'
import { Fold } from '../../../common/fold'
import { User } from '../../../common/user'
import { randomString } from '../../../common/util/random'

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 createFold = cloudFunction<
{ name: string; tags: string[] },
{ status: 'error' | 'success'; message?: string; fold?: Fold }
>('createFold')

export const placeBet = cloudFunction('placeBet')

export const resolveMarket = cloudFunction('resolveMarket')
Expand Down
Loading

0 comments on commit 60f68b1

Please sign in to comment.