contract slugs

This commit is contained in:
mantikoros 2021-12-17 17:16:42 -06:00
parent 756f31b1b7
commit 03a3df261c
7 changed files with 65 additions and 28 deletions

View File

@ -1,5 +1,7 @@
export type Contract = { export type Contract = {
id: string // Chosen by creator; must be unique id: string
slug: string // auto-generated; must be unique
creatorId: string creatorId: string
creatorName: string creatorName: string

View File

@ -8,7 +8,7 @@ import { User } from '../lib/firebase/users'
import { formatMoney, formatPercent } from '../lib/util/format' import { formatMoney, formatPercent } from '../lib/util/format'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { Contract, getContract, path } from '../lib/firebase/contracts' import { Contract, getContractFromId, path } from '../lib/firebase/contracts'
import { Row } from './layout/row' import { Row } from './layout/row'
import { UserLink } from './user-page' import { UserLink } from './user-page'
import { import {
@ -29,9 +29,11 @@ export function BetsList(props: { user: User }) {
const contractIds = _.uniq(loadedBets.map((bet) => bet.contractId)) const contractIds = _.uniq(loadedBets.map((bet) => bet.contractId))
let disposed = false let disposed = false
Promise.all(contractIds.map((id) => getContract(id))).then((contracts) => { Promise.all(contractIds.map((id) => getContractFromId(id))).then(
if (!disposed) setContracts(contracts.filter(Boolean) as Contract[]) (contracts) => {
}) if (!disposed) setContracts(contracts.filter(Boolean) as Contract[])
}
)
return () => { return () => {
disposed = true disposed = true

View File

@ -1,5 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Contract, listenForContract } from '../lib/firebase/contracts' import {
Contract,
getContractFromSlug,
listenForContract,
} from '../lib/firebase/contracts'
export const useContract = (contractId: string) => { export const useContract = (contractId: string) => {
const [contract, setContract] = useState<Contract | null | 'loading'>( const [contract, setContract] = useState<Contract | null | 'loading'>(
@ -14,14 +18,20 @@ export const useContract = (contractId: string) => {
} }
export const useContractWithPreload = ( export const useContractWithPreload = (
contractId: string, slug: string,
initial: Contract | null initial: Contract | null
) => { ) => {
const [contract, setContract] = useState<Contract | null>(initial) const [contract, setContract] = useState<Contract | null>(initial)
const [contractId, setContractId] = useState<string | undefined | null>(
initial?.id
)
useEffect(() => { useEffect(() => {
if (contractId) return listenForContract(contractId, setContract) if (contractId) return listenForContract(contractId, setContract)
}, [contractId])
if (contractId !== null)
getContractFromSlug(slug).then((c) => setContractId(c?.id || null))
}, [contractId, slug])
return contract return contract
} }

View File

@ -16,7 +16,9 @@ import {
import dayjs from 'dayjs' import dayjs from 'dayjs'
export type Contract = { export type Contract = {
id: string // Chosen by creator; must be unique id: string
slug: string // auto-generated; must be unique
creatorId: string creatorId: string
creatorName: string creatorName: string
@ -42,7 +44,7 @@ export function path(contract: Contract) {
// For now, derive username from creatorName // For now, derive username from creatorName
// Fix this when users can change their own names // Fix this when users can change their own names
const username = contract.creatorName.replace(/\s+/g, '') const username = contract.creatorName.replace(/\s+/g, '')
return `/${username}/${contract.id}` return `/${username}/${contract.slug}`
} }
export function compute(contract: Contract) { export function compute(contract: Contract) {
@ -66,13 +68,28 @@ export async function setContract(contract: Contract) {
await setDoc(docRef, contract) await setDoc(docRef, contract)
} }
export async function getContract(contractId: string) { export async function pushNewContract(contract: Omit<Contract, 'id'>) {
const newContractRef = doc(contractCollection)
const fullContract: Contract = { ...contract, id: newContractRef.id }
await setDoc(newContractRef, fullContract)
return fullContract
}
export async function getContractFromId(contractId: string) {
const docRef = doc(db, 'contracts', contractId) const docRef = doc(db, 'contracts', contractId)
const result = await getDoc(docRef) const result = await getDoc(docRef)
return result.exists() ? (result.data() as Contract) : undefined return result.exists() ? (result.data() as Contract) : undefined
} }
export async function getContractFromSlug(slug: string) {
const q = query(contractCollection, where('slug', '==', slug))
const snapshot = await getDocs(q)
return snapshot.empty ? undefined : (snapshot.docs[0].data() as Contract)
}
export async function deleteContract(contractId: string) { export async function deleteContract(contractId: string) {
const docRef = doc(db, 'contracts', contractId) const docRef = doc(db, 'contracts', contractId)
await deleteDoc(docRef) await deleteDoc(docRef)

View File

@ -1,4 +1,9 @@
import { Contract, getContract, setContract } from '../firebase/contracts' import {
Contract,
getContractFromSlug,
pushNewContract,
setContract,
} from '../firebase/contracts'
import { User } from '../firebase/users' import { User } from '../firebase/users'
import { randomString } from '../util/random-string' import { randomString } from '../util/random-string'
import { slugify } from '../util/slugify' import { slugify } from '../util/slugify'
@ -10,16 +15,18 @@ export async function createContract(
initialProb: number, initialProb: number,
creator: User creator: User
) { ) {
const slug = slugify(question).substr(0, 35) const proposedSlug = slugify(question).substring(0, 35)
const preexistingContract = await getContract(slug) const preexistingContract = await getContractFromSlug(proposedSlug)
const contractId = preexistingContract ? slug + '-' + randomString() : slug const slug = preexistingContract
? proposedSlug + '-' + randomString()
: proposedSlug
const { startYes, startNo } = calcStartPool(initialProb) const { startYes, startNo } = calcStartPool(initialProb)
const contract: Contract = { const contract: Omit<Contract, 'id'> = {
id: contractId, slug,
outcomeType: 'BINARY', outcomeType: 'BINARY',
creatorId: creator.id, creatorId: creator.id,
@ -38,9 +45,7 @@ export async function createContract(
lastUpdatedTime: Date.now(), lastUpdatedTime: Date.now(),
} }
await setContract(contract) return await pushNewContract(contract)
return contract
} }
export function calcStartPool(initialProb: number, initialCapital = 100) { export function calcStartPool(initialProb: number, initialCapital = 100) {

View File

@ -13,17 +13,17 @@ import { useBets } from '../../hooks/use-bets'
import { Title } from '../../components/title' import { Title } from '../../components/title'
import { Spacer } from '../../components/layout/spacer' import { Spacer } from '../../components/layout/spacer'
import { User } from '../../lib/firebase/users' import { User } from '../../lib/firebase/users'
import { Contract, getContract } from '../../lib/firebase/contracts' import { Contract, getContractFromSlug } from '../../lib/firebase/contracts'
import { SEO } from '../../components/SEO' import { SEO } from '../../components/SEO'
export async function getStaticProps(props: { params: any }) { export async function getStaticProps(props: { params: any }) {
const { username, contractId } = props.params const { username, slug } = props.params
const contract = (await getContract(contractId)) || null const contract = (await getContractFromSlug(slug)) || null
return { return {
props: { props: {
username, username,
contractId, slug,
contract, contract,
}, },
@ -37,12 +37,12 @@ export async function getStaticPaths() {
export default function ContractPage(props: { export default function ContractPage(props: {
contract: Contract | null contract: Contract | null
contractId: string slug: string
username: string username: string
}) { }) {
const user = useUser() const user = useUser()
const contract = useContractWithPreload(props.contractId, props.contract) const contract = useContractWithPreload(props.slug, props.contract)
if (!contract) { if (!contract) {
return <div>Contract not found...</div> return <div>Contract not found...</div>
@ -56,7 +56,7 @@ export default function ContractPage(props: {
<SEO <SEO
title={contract.question} title={contract.question}
description={contract.description} description={contract.description}
url={`/${props.username}/${props.contractId}`} url={`/${props.username}/${props.slug}`}
/> />
<Header /> <Header />

View File

@ -6,6 +6,7 @@ import { Header } from '../components/header'
import { Spacer } from '../components/layout/spacer' import { Spacer } from '../components/layout/spacer'
import { Title } from '../components/title' import { Title } from '../components/title'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { path } from '../lib/firebase/contracts'
import { createContract } from '../lib/service/create-contract' import { createContract } from '../lib/service/create-contract'
// Allow user to create a new contract // Allow user to create a new contract
@ -33,7 +34,7 @@ export default function NewContract() {
initialProb, initialProb,
creator creator
) )
await router.push(`contract/${contract.id}`) await router.push(path(contract))
} }
const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...` const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...`