Merge branch 'main' into compress_images
This commit is contained in:
commit
3f5a2c2970
|
@ -12,7 +12,9 @@ export class APIError extends Error {
|
|||
}
|
||||
|
||||
export function getFunctionUrl(name: string) {
|
||||
if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||
if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) {
|
||||
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
|
||||
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||
const { projectId, region } = ENV_CONFIG.firebaseConfig
|
||||
return `http://localhost:5001/${projectId}/${region}/${name}`
|
||||
} else {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Text } from '@tiptap/extension-text'
|
|||
// other tiptap extensions
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Mention } from '@tiptap/extension-mention'
|
||||
import Iframe from './tiptap-iframe'
|
||||
|
||||
export function parseTags(text: string) {
|
||||
|
@ -81,9 +82,9 @@ export const exhibitExts = [
|
|||
|
||||
Image,
|
||||
Link,
|
||||
Mention,
|
||||
Iframe,
|
||||
]
|
||||
// export const exhibitExts = [StarterKit as unknown as Extension, Image]
|
||||
|
||||
export function richTextToString(text?: JSONContent) {
|
||||
return !text ? '' : generateText(text, exhibitExts)
|
||||
|
|
45
dev.sh
Executable file
45
dev.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
|
||||
ENV=${1:-dev}
|
||||
case $ENV in
|
||||
dev)
|
||||
FIREBASE_PROJECT=dev
|
||||
NEXT_ENV=DEV
|
||||
EMULATOR=false ;;
|
||||
prod)
|
||||
FIREBASE_PROJECT=prod
|
||||
NEXT_ENV=PROD
|
||||
EMULATOR=false ;;
|
||||
localdb)
|
||||
FIREBASE_PROJECT=dev
|
||||
NEXT_ENV=DEV
|
||||
EMULATOR=true ;;
|
||||
*)
|
||||
echo "Invalid environment; must be dev, prod, or localdb."
|
||||
exit 1
|
||||
esac
|
||||
|
||||
firebase use $FIREBASE_PROJECT
|
||||
|
||||
if [ ! -z $EMULATOR ]
|
||||
then
|
||||
npx concurrently \
|
||||
-n FIRESTORE,FUNCTIONS,NEXT,TS \
|
||||
-c green,white,magenta,cyan \
|
||||
"yarn --cwd=functions firestore" \
|
||||
"cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \
|
||||
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
|
||||
NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \
|
||||
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
|
||||
yarn --cwd=web serve" \
|
||||
"cross-env yarn --cwd=web ts-watch"
|
||||
else
|
||||
npx concurrently \
|
||||
-n FUNCTIONS,NEXT,TS \
|
||||
-c white,magenta,cyan \
|
||||
"yarn --cwd=functions dev" \
|
||||
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
|
||||
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
|
||||
yarn --cwd=web serve" \
|
||||
"cross-env yarn --cwd=web ts-watch"
|
||||
fi
|
|
@ -15,6 +15,7 @@ A list of community-created projects built on, or related to, Manifold Markets.
|
|||
|
||||
- [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API
|
||||
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
|
||||
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
|
||||
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
|
||||
|
||||
## Bots
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
"start": "yarn shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log",
|
||||
"dev": "nodemon src/serve.ts",
|
||||
"firestore": "firebase emulators:start --only firestore --import=./firestore_export",
|
||||
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
|
||||
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
||||
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
||||
|
@ -27,7 +29,10 @@
|
|||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.18.1",
|
||||
"firebase-admin": "10.0.0",
|
||||
"firebase-functions": "3.21.2",
|
||||
"lodash": "4.17.21",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { logger } from 'firebase-functions/v2'
|
||||
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https'
|
||||
import { Request, RequestHandler, Response } from 'express'
|
||||
import { error } from 'firebase-functions/logger'
|
||||
import { HttpsOptions } from 'firebase-functions/v2/https'
|
||||
import { log } from './utils'
|
||||
import { z } from 'zod'
|
||||
import { APIError } from '../../common/api'
|
||||
|
@ -45,7 +46,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
|||
return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
|
||||
} catch (err) {
|
||||
// This is somewhat suspicious, so get it into the firebase console
|
||||
logger.error('Error verifying Firebase JWT: ', err)
|
||||
error('Error verifying Firebase JWT: ', err)
|
||||
throw new APIError(403, 'Error validating token.')
|
||||
}
|
||||
case 'Key':
|
||||
|
@ -83,6 +84,11 @@ export const zTimestamp = () => {
|
|||
}, z.date())
|
||||
}
|
||||
|
||||
export type EndpointDefinition = {
|
||||
opts: EndpointOptions & { method: string }
|
||||
handler: RequestHandler
|
||||
}
|
||||
|
||||
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
|
||||
const result = schema.safeParse(val)
|
||||
if (!result.success) {
|
||||
|
@ -99,12 +105,12 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
|
|||
}
|
||||
}
|
||||
|
||||
interface EndpointOptions extends HttpsOptions {
|
||||
methods?: string[]
|
||||
export interface EndpointOptions extends HttpsOptions {
|
||||
method?: string
|
||||
}
|
||||
|
||||
const DEFAULT_OPTS = {
|
||||
methods: ['POST'],
|
||||
method: 'POST',
|
||||
minInstances: 1,
|
||||
concurrency: 100,
|
||||
memory: '2GiB',
|
||||
|
@ -113,28 +119,29 @@ const DEFAULT_OPTS = {
|
|||
}
|
||||
|
||||
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||
const opts = Object.assign(endpointOpts, DEFAULT_OPTS)
|
||||
return onRequest(opts, async (req, res) => {
|
||||
log('Request processing started.')
|
||||
try {
|
||||
if (!opts.methods.includes(req.method)) {
|
||||
const allowed = opts.methods.join(', ')
|
||||
throw new APIError(405, `This endpoint supports only ${allowed}.`)
|
||||
}
|
||||
const authedUser = await lookupUser(await parseCredentials(req))
|
||||
log('User credentials processed.')
|
||||
res.status(200).json(await fn(req, authedUser))
|
||||
} catch (e) {
|
||||
if (e instanceof APIError) {
|
||||
const output: { [k: string]: unknown } = { message: e.message }
|
||||
if (e.details != null) {
|
||||
output.details = e.details
|
||||
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
|
||||
return {
|
||||
opts,
|
||||
handler: async (req: Request, res: Response) => {
|
||||
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
|
||||
try {
|
||||
if (opts.method !== req.method) {
|
||||
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
|
||||
}
|
||||
const authedUser = await lookupUser(await parseCredentials(req))
|
||||
res.status(200).json(await fn(req, authedUser))
|
||||
} catch (e) {
|
||||
if (e instanceof APIError) {
|
||||
const output: { [k: string]: unknown } = { message: e.message }
|
||||
if (e.details != null) {
|
||||
output.details = e.details
|
||||
}
|
||||
res.status(e.code).json(output)
|
||||
} else {
|
||||
error(e)
|
||||
res.status(500).json({ message: 'An unknown error occurred.' })
|
||||
}
|
||||
res.status(e.code).json(output)
|
||||
} else {
|
||||
logger.error(e)
|
||||
res.status(500).json({ message: 'An unknown error occurred.' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
} as EndpointDefinition
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const bodySchema = z.object({
|
|||
export const cancelbet = newEndpoint({}, async (req, auth) => {
|
||||
const { betId } = validate(bodySchema, req.body)
|
||||
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
return await firestore.runTransaction(async (trans) => {
|
||||
const snap = await trans.get(
|
||||
firestore.collectionGroup('bets').where('id', '==', betId)
|
||||
)
|
||||
|
@ -28,8 +28,6 @@ export const cancelbet = newEndpoint({}, async (req, auth) => {
|
|||
|
||||
return { ...bet, isCancelled: true }
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
|
@ -120,7 +120,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
|
||||
let group = null
|
||||
if (groupId) {
|
||||
const groupDocRef = await firestore.collection('groups').doc(groupId)
|
||||
const groupDocRef = firestore.collection('groups').doc(groupId)
|
||||
const groupDoc = await groupDocRef.get()
|
||||
if (!groupDoc.exists) {
|
||||
throw new APIError(400, 'No group exists with the given group ID.')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { newEndpoint } from './api'
|
||||
|
||||
export const health = newEndpoint({ methods: ['GET'] }, async (_req, auth) => {
|
||||
export const health = newEndpoint({ method: 'GET' }, async (_req, auth) => {
|
||||
return {
|
||||
message: 'Server is working.',
|
||||
uid: auth.uid,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import { EndpointDefinition } from './api'
|
||||
|
||||
admin.initializeApp()
|
||||
|
||||
|
@ -25,20 +27,63 @@ export * from './on-delete-group'
|
|||
export * from './score-contracts'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
export * from './transact'
|
||||
export * from './change-user-info'
|
||||
export * from './create-user'
|
||||
export * from './create-answer'
|
||||
export * from './place-bet'
|
||||
export * from './cancel-bet'
|
||||
export * from './sell-bet'
|
||||
export * from './sell-shares'
|
||||
export * from './claim-manalink'
|
||||
export * from './create-contract'
|
||||
export * from './add-liquidity'
|
||||
export * from './withdraw-liquidity'
|
||||
export * from './create-group'
|
||||
export * from './resolve-market'
|
||||
export * from './unsubscribe'
|
||||
export * from './stripe'
|
||||
import { health } from './health'
|
||||
import { transact } from './transact'
|
||||
import { changeuserinfo } from './change-user-info'
|
||||
import { createuser } from './create-user'
|
||||
import { createanswer } from './create-answer'
|
||||
import { placebet } from './place-bet'
|
||||
import { cancelbet } from './cancel-bet'
|
||||
import { sellbet } from './sell-bet'
|
||||
import { sellshares } from './sell-shares'
|
||||
import { claimmanalink } from './claim-manalink'
|
||||
import { createmarket } from './create-contract'
|
||||
import { addliquidity } from './add-liquidity'
|
||||
import { withdrawliquidity } from './withdraw-liquidity'
|
||||
import { creategroup } from './create-group'
|
||||
import { resolvemarket } from './resolve-market'
|
||||
import { unsubscribe } from './unsubscribe'
|
||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
|
||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||
return onRequest(opts, handler as any)
|
||||
}
|
||||
const healthFunction = toCloudFunction(health)
|
||||
const transactFunction = toCloudFunction(transact)
|
||||
const changeUserInfoFunction = toCloudFunction(changeuserinfo)
|
||||
const createUserFunction = toCloudFunction(createuser)
|
||||
const createAnswerFunction = toCloudFunction(createanswer)
|
||||
const placeBetFunction = toCloudFunction(placebet)
|
||||
const cancelBetFunction = toCloudFunction(cancelbet)
|
||||
const sellBetFunction = toCloudFunction(sellbet)
|
||||
const sellSharesFunction = toCloudFunction(sellshares)
|
||||
const claimManalinkFunction = toCloudFunction(claimmanalink)
|
||||
const createMarketFunction = toCloudFunction(createmarket)
|
||||
const addLiquidityFunction = toCloudFunction(addliquidity)
|
||||
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
|
||||
const createGroupFunction = toCloudFunction(creategroup)
|
||||
const resolveMarketFunction = toCloudFunction(resolvemarket)
|
||||
const unsubscribeFunction = toCloudFunction(unsubscribe)
|
||||
const stripeWebhookFunction = toCloudFunction(stripewebhook)
|
||||
const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||
|
||||
export {
|
||||
healthFunction as health,
|
||||
transactFunction as transact,
|
||||
changeUserInfoFunction as changeuserinfo,
|
||||
createUserFunction as createuser,
|
||||
createAnswerFunction as createanswer,
|
||||
placeBetFunction as placebet,
|
||||
cancelBetFunction as cancelbet,
|
||||
sellBetFunction as sellbet,
|
||||
sellSharesFunction as sellshares,
|
||||
claimManalinkFunction as claimmanalink,
|
||||
createMarketFunction as createmarket,
|
||||
addLiquidityFunction as addliquidity,
|
||||
withdrawLiquidityFunction as withdrawliquidity,
|
||||
createGroupFunction as creategroup,
|
||||
resolveMarketFunction as resolvemarket,
|
||||
unsubscribeFunction as unsubscribe,
|
||||
stripeWebhookFunction as stripewebhook,
|
||||
createCheckoutSessionFunction as createcheckoutsession,
|
||||
}
|
||||
|
|
|
@ -10,14 +10,14 @@ export const onCreateAnswer = functions.firestore
|
|||
contractId: string
|
||||
}
|
||||
const { eventId } = context
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract)
|
||||
throw new Error('Could not find contract corresponding with answer')
|
||||
|
||||
const answer = change.data() as Answer
|
||||
// Ignore ante answer.
|
||||
if (answer.number === 0) return
|
||||
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract)
|
||||
throw new Error('Could not find contract corresponding with answer')
|
||||
|
||||
const answerCreator = await getUser(answer.userId)
|
||||
if (!answerCreator) throw new Error('Could not find answer creator')
|
||||
|
||||
|
|
|
@ -8,14 +8,14 @@ export const onCreateLiquidityProvision = functions.firestore
|
|||
.onCreate(async (change, context) => {
|
||||
const liquidity = change.data() as LiquidityProvision
|
||||
const { eventId } = context
|
||||
const contract = await getContract(liquidity.contractId)
|
||||
|
||||
if (!contract)
|
||||
throw new Error('Could not find contract corresponding with liquidity')
|
||||
|
||||
// Ignore Manifold Markets liquidity for now - users see a notification for free market liquidity provision
|
||||
if (liquidity.userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2') return
|
||||
|
||||
const contract = await getContract(liquidity.contractId)
|
||||
if (!contract)
|
||||
throw new Error('Could not find contract corresponding with liquidity')
|
||||
|
||||
const liquidityProvider = await getUser(liquidity.userId)
|
||||
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||
|
||||
|
|
|
@ -103,8 +103,8 @@ async function handleUserUpdatedReferral(user: User, eventId: string) {
|
|||
description: `Referred new user id: ${user.id} for ${REFERRAL_AMOUNT}`,
|
||||
}
|
||||
|
||||
const txnDoc = await firestore.collection(`txns/`).doc(txn.id)
|
||||
await transaction.set(txnDoc, txn)
|
||||
const txnDoc = firestore.collection(`txns/`).doc(txn.id)
|
||||
transaction.set(txnDoc, txn)
|
||||
console.log('created referral with txn id:', txn.id)
|
||||
// We're currently not subtracting M$ from the house, not sure if we want to for accounting purposes.
|
||||
transaction.update(referredByUserDoc, {
|
||||
|
|
55
functions/src/scripts/backfill-comment-ids.ts
Normal file
55
functions/src/scripts/backfill-comment-ids.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
// We have some old comments without IDs and user IDs. Let's fill them in.
|
||||
// Luckily, this was back when all comments had associated bets, so it's possible
|
||||
// to retrieve the user IDs through the bets.
|
||||
|
||||
import * as admin from 'firebase-admin'
|
||||
import { QueryDocumentSnapshot } from 'firebase-admin/firestore'
|
||||
import { initAdmin } from './script-init'
|
||||
import { log, writeAsync } from '../utils'
|
||||
import { Bet } from '../../../common/bet'
|
||||
|
||||
initAdmin()
|
||||
const firestore = admin.firestore()
|
||||
|
||||
const getUserIdsByCommentId = async (comments: QueryDocumentSnapshot[]) => {
|
||||
const bets = await firestore.collectionGroup('bets').get()
|
||||
log(`Loaded ${bets.size} bets.`)
|
||||
const betsById = Object.fromEntries(
|
||||
bets.docs.map((b) => [b.id, b.data() as Bet])
|
||||
)
|
||||
return Object.fromEntries(
|
||||
comments.map((c) => [c.id, betsById[c.data().betId].userId])
|
||||
)
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const commentsQuery = firestore.collectionGroup('comments')
|
||||
commentsQuery.get().then(async (commentSnaps) => {
|
||||
log(`Loaded ${commentSnaps.size} comments.`)
|
||||
const needsFilling = commentSnaps.docs.filter((ct) => {
|
||||
return !('id' in ct.data()) || !('userId' in ct.data())
|
||||
})
|
||||
log(`${needsFilling.length} comments need IDs.`)
|
||||
const userIdNeedsFilling = needsFilling.filter((ct) => {
|
||||
return !('userId' in ct.data())
|
||||
})
|
||||
log(`${userIdNeedsFilling.length} comments need user IDs.`)
|
||||
const userIdsByCommentId =
|
||||
userIdNeedsFilling.length > 0
|
||||
? await getUserIdsByCommentId(userIdNeedsFilling)
|
||||
: {}
|
||||
const updates = needsFilling.map((ct) => {
|
||||
const fields: { [k: string]: unknown } = {}
|
||||
if (!ct.data().id) {
|
||||
fields.id = ct.id
|
||||
}
|
||||
if (!ct.data().userId && userIdsByCommentId[ct.id]) {
|
||||
fields.userId = userIdsByCommentId[ct.id]
|
||||
}
|
||||
return { doc: ct.ref, fields }
|
||||
})
|
||||
log(`Updating ${updates.length} comments.`)
|
||||
await writeAsync(firestore, updates)
|
||||
log(`Updated all comments.`)
|
||||
})
|
||||
}
|
|
@ -66,10 +66,18 @@ export const getServiceAccountCredentials = (env?: string) => {
|
|||
}
|
||||
|
||||
export const initAdmin = (env?: string) => {
|
||||
const serviceAccount = getServiceAccountCredentials(env)
|
||||
console.log(`Initializing connection to ${serviceAccount.project_id}...`)
|
||||
return admin.initializeApp({
|
||||
projectId: serviceAccount.project_id,
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
try {
|
||||
const serviceAccount = getServiceAccountCredentials(env)
|
||||
console.log(
|
||||
`Initializing connection to ${serviceAccount.project_id} Firebase...`
|
||||
)
|
||||
return admin.initializeApp({
|
||||
projectId: serviceAccount.project_id,
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.log(`Initializing connection to default Firebase...`)
|
||||
return admin.initializeApp()
|
||||
}
|
||||
}
|
||||
|
|
68
functions/src/serve.ts
Normal file
68
functions/src/serve.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import * as cors from 'cors'
|
||||
import * as express from 'express'
|
||||
import { Express, Request, Response, NextFunction } from 'express'
|
||||
import { EndpointDefinition } from './api'
|
||||
|
||||
const PORT = 8088
|
||||
|
||||
import { initAdmin } from './scripts/script-init'
|
||||
initAdmin()
|
||||
|
||||
import { health } from './health'
|
||||
import { transact } from './transact'
|
||||
import { changeuserinfo } from './change-user-info'
|
||||
import { createuser } from './create-user'
|
||||
import { createanswer } from './create-answer'
|
||||
import { placebet } from './place-bet'
|
||||
import { cancelbet } from './cancel-bet'
|
||||
import { sellbet } from './sell-bet'
|
||||
import { sellshares } from './sell-shares'
|
||||
import { claimmanalink } from './claim-manalink'
|
||||
import { createmarket } from './create-contract'
|
||||
import { addliquidity } from './add-liquidity'
|
||||
import { withdrawliquidity } from './withdraw-liquidity'
|
||||
import { creategroup } from './create-group'
|
||||
import { resolvemarket } from './resolve-market'
|
||||
import { unsubscribe } from './unsubscribe'
|
||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
|
||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||
const app = express()
|
||||
|
||||
const addEndpointRoute = (
|
||||
path: string,
|
||||
endpoint: EndpointDefinition,
|
||||
...middlewares: Middleware[]
|
||||
) => {
|
||||
const method = endpoint.opts.method.toLowerCase() as keyof Express
|
||||
const corsMiddleware = cors({ origin: endpoint.opts.cors })
|
||||
const allMiddleware = [...middlewares, corsMiddleware]
|
||||
app.options(path, corsMiddleware) // preflight requests
|
||||
app[method](path, ...allMiddleware, endpoint.handler)
|
||||
}
|
||||
|
||||
const addJsonEndpointRoute = (name: string, endpoint: EndpointDefinition) => {
|
||||
addEndpointRoute(name, endpoint, express.json())
|
||||
}
|
||||
|
||||
addEndpointRoute('/health', health)
|
||||
addJsonEndpointRoute('/transact', transact)
|
||||
addJsonEndpointRoute('/changeuserinfo', changeuserinfo)
|
||||
addJsonEndpointRoute('/createuser', createuser)
|
||||
addJsonEndpointRoute('/createanswer', createanswer)
|
||||
addJsonEndpointRoute('/placebet', placebet)
|
||||
addJsonEndpointRoute('/cancelbet', cancelbet)
|
||||
addJsonEndpointRoute('/sellbet', sellbet)
|
||||
addJsonEndpointRoute('/sellshares', sellshares)
|
||||
addJsonEndpointRoute('/claimmanalink', claimmanalink)
|
||||
addJsonEndpointRoute('/createmarket', createmarket)
|
||||
addJsonEndpointRoute('/addliquidity', addliquidity)
|
||||
addJsonEndpointRoute('/withdrawliquidity', withdrawliquidity)
|
||||
addJsonEndpointRoute('/creategroup', creategroup)
|
||||
addJsonEndpointRoute('/resolvemarket', resolvemarket)
|
||||
addJsonEndpointRoute('/unsubscribe', unsubscribe)
|
||||
addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||
|
||||
app.listen(PORT)
|
||||
console.log(`Serving functions on port ${PORT}.`)
|
|
@ -1,7 +1,7 @@
|
|||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import * as admin from 'firebase-admin'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import { EndpointDefinition } from './api'
|
||||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
||||
import { sendThankYouEmail } from './emails'
|
||||
import { track } from './analytics'
|
||||
|
@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd()
|
|||
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
|
||||
}
|
||||
|
||||
export const createcheckoutsession = onRequest(
|
||||
{ minInstances: 1, secrets: ['STRIPE_APIKEY'] },
|
||||
async (req, res) => {
|
||||
export const createcheckoutsession: EndpointDefinition = {
|
||||
opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] },
|
||||
handler: async (req, res) => {
|
||||
const userId = req.query.userId?.toString()
|
||||
|
||||
const manticDollarQuantity = req.query.manticDollarQuantity?.toString()
|
||||
|
@ -86,21 +86,24 @@ export const createcheckoutsession = onRequest(
|
|||
})
|
||||
|
||||
res.redirect(303, session.url || '')
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const stripewebhook = onRequest(
|
||||
{
|
||||
export const stripewebhook: EndpointDefinition = {
|
||||
opts: {
|
||||
method: 'POST',
|
||||
minInstances: 1,
|
||||
secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'],
|
||||
},
|
||||
async (req, res) => {
|
||||
handler: async (req, res) => {
|
||||
const stripe = initStripe()
|
||||
let event
|
||||
|
||||
try {
|
||||
// Cloud Functions jam the raw body into a special `rawBody` property
|
||||
const rawBody = (req as any).rawBody ?? req.body
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.rawBody,
|
||||
rawBody,
|
||||
req.headers['stripe-signature'] as string,
|
||||
process.env.STRIPE_WEBHOOKSECRET as string
|
||||
)
|
||||
|
@ -116,8 +119,8 @@ export const stripewebhook = onRequest(
|
|||
}
|
||||
|
||||
res.status(200).send('success')
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const issueMoneys = async (session: StripeSession) => {
|
||||
const { id: sessionId } = session
|
||||
|
|
|
@ -1,66 +1,72 @@
|
|||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { EndpointDefinition } from './api'
|
||||
import { getUser } from './utils'
|
||||
import { PrivateUser } from '../../common/user'
|
||||
|
||||
export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => {
|
||||
const id = req.query.id as string
|
||||
let type = req.query.type as string
|
||||
if (!id || !type) {
|
||||
res.status(400).send('Empty id or type parameter.')
|
||||
return
|
||||
}
|
||||
export const unsubscribe: EndpointDefinition = {
|
||||
opts: { method: 'GET', minInstances: 1 },
|
||||
handler: async (req, res) => {
|
||||
const id = req.query.id as string
|
||||
let type = req.query.type as string
|
||||
if (!id || !type) {
|
||||
res.status(400).send('Empty id or type parameter.')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'market-resolved') type = 'market-resolve'
|
||||
if (type === 'market-resolved') type = 'market-resolve'
|
||||
|
||||
if (
|
||||
!['market-resolve', 'market-comment', 'market-answer', 'generic'].includes(
|
||||
type
|
||||
)
|
||||
) {
|
||||
res.status(400).send('Invalid type parameter.')
|
||||
return
|
||||
}
|
||||
if (
|
||||
![
|
||||
'market-resolve',
|
||||
'market-comment',
|
||||
'market-answer',
|
||||
'generic',
|
||||
].includes(type)
|
||||
) {
|
||||
res.status(400).send('Invalid type parameter.')
|
||||
return
|
||||
}
|
||||
|
||||
const user = await getUser(id)
|
||||
const user = await getUser(id)
|
||||
|
||||
if (!user) {
|
||||
res.send('This user is not currently subscribed or does not exist.')
|
||||
return
|
||||
}
|
||||
if (!user) {
|
||||
res.send('This user is not currently subscribed or does not exist.')
|
||||
return
|
||||
}
|
||||
|
||||
const { name } = user
|
||||
const { name } = user
|
||||
|
||||
const update: Partial<PrivateUser> = {
|
||||
...(type === 'market-resolve' && {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
}),
|
||||
...(type === 'market-comment' && {
|
||||
unsubscribedFromCommentEmails: true,
|
||||
}),
|
||||
...(type === 'market-answer' && {
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
...(type === 'generic' && {
|
||||
unsubscribedFromGenericEmails: true,
|
||||
}),
|
||||
}
|
||||
const update: Partial<PrivateUser> = {
|
||||
...(type === 'market-resolve' && {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
}),
|
||||
...(type === 'market-comment' && {
|
||||
unsubscribedFromCommentEmails: true,
|
||||
}),
|
||||
...(type === 'market-answer' && {
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
...(type === 'generic' && {
|
||||
unsubscribedFromGenericEmails: true,
|
||||
}),
|
||||
}
|
||||
|
||||
await firestore.collection('private-users').doc(id).update(update)
|
||||
await firestore.collection('private-users').doc(id).update(update)
|
||||
|
||||
if (type === 'market-resolve')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-comment')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-answer')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
||||
)
|
||||
else res.send(`${name}, you have been unsubscribed.`)
|
||||
})
|
||||
if (type === 'market-resolve')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-comment')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-answer')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
||||
)
|
||||
else res.send(`${name}, you have been unsubscribed.`)
|
||||
},
|
||||
}
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
|
@ -11,8 +11,6 @@ import { last } from 'lodash'
|
|||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
|
||||
const computeInvestmentValue = (
|
||||
bets: Bet[],
|
||||
contractsDict: { [k: string]: Contract }
|
||||
|
@ -59,8 +57,8 @@ export const updateMetricsCore = async () => {
|
|||
return {
|
||||
doc: firestore.collection('contracts').doc(contract.id),
|
||||
fields: {
|
||||
volume24Hours: computeVolume(contractBets, now - oneDay),
|
||||
volume7Days: computeVolume(contractBets, now - oneDay * 7),
|
||||
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
||||
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -14,10 +14,13 @@
|
|||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "5.25.0",
|
||||
"@typescript-eslint/parser": "5.25.0",
|
||||
"concurrently": "6.5.1",
|
||||
"eslint": "8.15.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"prettier": "2.5.0",
|
||||
"typescript": "4.6.4"
|
||||
"typescript": "4.6.4",
|
||||
"ts-node": "10.9.1",
|
||||
"nodemon": "2.0.19"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.43"
|
||||
|
|
|
@ -130,15 +130,15 @@ export function ContractSearch(props: {
|
|||
: '',
|
||||
additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '',
|
||||
additionalFilter?.groupSlug
|
||||
? `groupSlugs:${additionalFilter.groupSlug}`
|
||||
? `groupLinks.slug:${additionalFilter.groupSlug}`
|
||||
: '',
|
||||
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
||||
? `groupSlugs:${pillFilter}`
|
||||
? `groupLinks.slug:${pillFilter}`
|
||||
: '',
|
||||
pillFilter === 'personal'
|
||||
? // Show contracts in groups that the user is a member of
|
||||
memberGroupSlugs
|
||||
.map((slug) => `groupSlugs:${slug}`)
|
||||
.map((slug) => `groupLinks.slug:${slug}`)
|
||||
// Show contracts created by users the user follows
|
||||
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
||||
// Show contracts bet on by users the user follows
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
Contract,
|
||||
contractMetrics,
|
||||
contractPath,
|
||||
contractPool,
|
||||
updateContract,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import dayjs from 'dayjs'
|
||||
|
@ -55,10 +54,6 @@ export function MiscDetails(props: {
|
|||
groupLinks,
|
||||
} = contract
|
||||
|
||||
// Show at most one category that this contract is tagged by
|
||||
// const categories = CATEGORY_LIST.filter((category) =>
|
||||
// tags.map((t) => t.toLowerCase()).includes(category)
|
||||
// ).slice(0, 1)
|
||||
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
||||
|
||||
return (
|
||||
|
@ -80,14 +75,11 @@ export function MiscDetails(props: {
|
|||
{fromNow(resolutionTime || 0)}
|
||||
</Row>
|
||||
) : volume > 0 || !isNew ? (
|
||||
<Row className={'shrink-0'}>{contractPool(contract)} pool</Row>
|
||||
<Row className={'shrink-0'}>{formatMoney(contract.volume)} bet</Row>
|
||||
) : (
|
||||
<NewContractBadge />
|
||||
)}
|
||||
|
||||
{/*{categories.length > 0 && (*/}
|
||||
{/* <TagsList className="text-gray-400" tags={categories} noLabel />*/}
|
||||
{/*)}*/}
|
||||
{groupLinks && groupLinks.length > 0 && (
|
||||
<SiteLink
|
||||
href={groupPath(groupLinks[0].slug)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Tabs } from '../layout/tabs'
|
|||
import { Col } from '../layout/col'
|
||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||
import { useComments } from 'web/hooks/use-comments'
|
||||
|
||||
export function ContractTabs(props: {
|
||||
contract: Contract
|
||||
|
@ -18,11 +19,15 @@ export function ContractTabs(props: {
|
|||
comments: Comment[]
|
||||
tips: CommentTipMap
|
||||
}) {
|
||||
const { contract, user, bets, comments, tips, liquidityProvisions } = props
|
||||
const { contract, user, bets, tips, liquidityProvisions } = props
|
||||
const { outcomeType } = contract
|
||||
|
||||
const userBets = user && bets.filter((bet) => bet.userId === user.id)
|
||||
|
||||
// Load comments here, so the badge count will be correct
|
||||
const updatedComments = useComments(contract.id)
|
||||
const comments = updatedComments ?? props.comments
|
||||
|
||||
const betActivity = (
|
||||
<ContractActivity
|
||||
contract={contract}
|
||||
|
@ -89,8 +94,12 @@ export function ContractTabs(props: {
|
|||
<Tabs
|
||||
currentPageForAnalytics={'contract'}
|
||||
tabs={[
|
||||
{ title: 'Comments', content: commentActivity },
|
||||
{ title: 'Bets', content: betActivity },
|
||||
{
|
||||
title: 'Comments',
|
||||
content: commentActivity,
|
||||
badge: `${comments.length}`,
|
||||
},
|
||||
{ title: 'Bets', content: betActivity, badge: `${bets.length}` },
|
||||
...(!user || !userBets?.length
|
||||
? []
|
||||
: [{ title: 'Your bets', content: yourTrades }]),
|
||||
|
|
|
@ -11,15 +11,25 @@ import {
|
|||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Mention } from '@tiptap/extension-mention'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Linkify } from './linkify'
|
||||
import { uploadImage } from 'web/lib/firebase/storage'
|
||||
import { useMutation } from 'react-query'
|
||||
import { exhibitExts } from 'common/util/parse'
|
||||
import { FileUploadButton } from './file-upload-button'
|
||||
import { linkClass } from './site-link'
|
||||
import { useUsers } from 'web/hooks/use-users'
|
||||
import { mentionSuggestion } from './editor/mention-suggestion'
|
||||
import { DisplayMention } from './editor/mention'
|
||||
import Iframe from 'common/util/tiptap-iframe'
|
||||
import { CodeIcon, PhotographIcon } from '@heroicons/react/solid'
|
||||
import { Modal } from './layout/modal'
|
||||
import { Col } from './layout/col'
|
||||
import { Button } from './button'
|
||||
import { Row } from './layout/row'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
||||
const proseClass = clsx(
|
||||
'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
|
||||
|
@ -34,33 +44,41 @@ export function useTextEditor(props: {
|
|||
}) {
|
||||
const { placeholder, max, defaultValue = '', disabled } = props
|
||||
|
||||
const users = useUsers()
|
||||
|
||||
const editorClass = clsx(
|
||||
proseClass,
|
||||
'box-content min-h-[6em] textarea textarea-bordered text-base'
|
||||
'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0'
|
||||
)
|
||||
|
||||
const editor = useEditor({
|
||||
editorProps: { attributes: { class: editorClass } },
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3] },
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass:
|
||||
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0',
|
||||
}),
|
||||
CharacterCount.configure({ limit: max }),
|
||||
Image,
|
||||
Link.configure({
|
||||
HTMLAttributes: {
|
||||
class: clsx('no-underline !text-indigo-700', linkClass),
|
||||
},
|
||||
}),
|
||||
Iframe,
|
||||
],
|
||||
content: defaultValue,
|
||||
})
|
||||
const editor = useEditor(
|
||||
{
|
||||
editorProps: { attributes: { class: editorClass } },
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3] },
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass:
|
||||
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0',
|
||||
}),
|
||||
CharacterCount.configure({ limit: max }),
|
||||
Image,
|
||||
Link.configure({
|
||||
HTMLAttributes: {
|
||||
class: clsx('no-underline !text-indigo-700', linkClass),
|
||||
},
|
||||
}),
|
||||
DisplayMention.configure({
|
||||
suggestion: mentionSuggestion(users),
|
||||
}),
|
||||
Iframe,
|
||||
],
|
||||
content: defaultValue,
|
||||
},
|
||||
[!users.length] // passed as useEffect dependency. (re-render editor when users load, to update mention menu)
|
||||
)
|
||||
|
||||
const upload = useUploadMutation(editor)
|
||||
|
||||
|
@ -78,8 +96,7 @@ export function useTextEditor(props: {
|
|||
|
||||
// If the pasted content is iframe code, directly inject it
|
||||
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
||||
const isValidIframe = /^<iframe.*<\/iframe>$/.test(text)
|
||||
if (isValidIframe) {
|
||||
if (isValidIframe(text)) {
|
||||
editor.chain().insertContent(text).run()
|
||||
return true // Prevent the code from getting pasted as text
|
||||
}
|
||||
|
@ -96,16 +113,21 @@ export function useTextEditor(props: {
|
|||
return { editor, upload }
|
||||
}
|
||||
|
||||
function isValidIframe(text: string) {
|
||||
return /^<iframe.*<\/iframe>$/.test(text)
|
||||
}
|
||||
|
||||
export function TextEditor(props: {
|
||||
editor: Editor | null
|
||||
upload: ReturnType<typeof useUploadMutation>
|
||||
}) {
|
||||
const { editor, upload } = props
|
||||
const [iframeOpen, setIframeOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* hide placeholder when focused */}
|
||||
<div className="w-full [&:focus-within_p.is-empty]:before:content-none">
|
||||
<div className="relative w-full [&:focus-within_p.is-empty]:before:content-none">
|
||||
{editor && (
|
||||
<FloatingMenu
|
||||
editor={editor}
|
||||
|
@ -121,7 +143,46 @@ export function TextEditor(props: {
|
|||
images!
|
||||
</FloatingMenu>
|
||||
)}
|
||||
<EditorContent editor={editor} />
|
||||
<div className="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500">
|
||||
<EditorContent editor={editor} />
|
||||
{/* Spacer element to match the height of the toolbar */}
|
||||
<div className="py-2" aria-hidden="true">
|
||||
{/* Matches height of button in toolbar (1px border + 36px content height) */}
|
||||
<div className="py-px">
|
||||
<div className="h-9" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar, with buttons for image and embeds */}
|
||||
<div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2">
|
||||
<div className="flex items-center space-x-5">
|
||||
<div className="flex items-center">
|
||||
<FileUploadButton
|
||||
onFiles={upload.mutate}
|
||||
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Upload an image</span>
|
||||
</FileUploadButton>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIframeOpen(true)}
|
||||
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<IframeModal
|
||||
editor={editor}
|
||||
open={iframeOpen}
|
||||
setOpen={setIframeOpen}
|
||||
/>
|
||||
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Embed an iframe</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{upload.isLoading && <span className="text-xs">Uploading image...</span>}
|
||||
{upload.isError && (
|
||||
|
@ -131,6 +192,65 @@ export function TextEditor(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function IframeModal(props: {
|
||||
editor: Editor | null
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) {
|
||||
const { editor, open, setOpen } = props
|
||||
const [embedCode, setEmbedCode] = useState('')
|
||||
const valid = isValidIframe(embedCode)
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<Col className="gap-2 rounded bg-white p-6">
|
||||
<label
|
||||
htmlFor="embed"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Embed a market, Youtube video, etc.
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="embed"
|
||||
id="embed"
|
||||
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
placeholder='e.g. <iframe src="..."></iframe>'
|
||||
value={embedCode}
|
||||
onChange={(e) => setEmbedCode(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Preview the embed if it's valid */}
|
||||
{valid ? <RichContent content={embedCode} /> : <Spacer h={2} />}
|
||||
|
||||
<Row className="gap-2">
|
||||
<Button
|
||||
disabled={!valid}
|
||||
onClick={() => {
|
||||
if (editor && valid) {
|
||||
editor.chain().insertContent(embedCode).run()
|
||||
setEmbedCode('')
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Embed
|
||||
</Button>
|
||||
<Button
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
setEmbedCode('')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const useUploadMutation = (editor: Editor | null) =>
|
||||
useMutation(
|
||||
(files: File[]) =>
|
||||
|
@ -149,11 +269,15 @@ const useUploadMutation = (editor: Editor | null) =>
|
|||
}
|
||||
)
|
||||
|
||||
function RichContent(props: { content: JSONContent }) {
|
||||
function RichContent(props: { content: JSONContent | string }) {
|
||||
const { content } = props
|
||||
const editor = useEditor({
|
||||
editorProps: { attributes: { class: proseClass } },
|
||||
extensions: exhibitExts,
|
||||
extensions: [
|
||||
// replace tiptap's Mention with ours, to add style and link
|
||||
...exhibitExts.filter((ex) => ex.name !== Mention.name),
|
||||
DisplayMention,
|
||||
],
|
||||
content,
|
||||
editable: false,
|
||||
})
|
||||
|
|
62
web/components/editor/mention-list.tsx
Normal file
62
web/components/editor/mention-list.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { SuggestionProps } from '@tiptap/suggestion'
|
||||
import clsx from 'clsx'
|
||||
import { User } from 'common/user'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { Avatar } from '../avatar'
|
||||
|
||||
// copied from https://tiptap.dev/api/nodes/mention#usage
|
||||
export const MentionList = forwardRef((props: SuggestionProps<User>, ref) => {
|
||||
const { items: users, command } = props
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
useEffect(() => setSelectedIndex(0), [users])
|
||||
|
||||
const submitUser = (index: number) => {
|
||||
const user = users[index]
|
||||
if (user) command({ id: user.id, label: user.username } as any)
|
||||
}
|
||||
|
||||
const onUp = () =>
|
||||
setSelectedIndex((i) => (i + users.length - 1) % users.length)
|
||||
const onDown = () => setSelectedIndex((i) => (i + 1) % users.length)
|
||||
const onEnter = () => submitUser(selectedIndex)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }: any) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
onUp()
|
||||
return true
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
onDown()
|
||||
return true
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
onEnter()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="w-42 absolute z-10 overflow-x-hidden rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{!users.length ? (
|
||||
<span className="m-1 whitespace-nowrap">No results...</span>
|
||||
) : (
|
||||
users.map((user, i) => (
|
||||
<button
|
||||
className={clsx(
|
||||
'flex h-8 w-full cursor-pointer select-none items-center gap-2 truncate px-4',
|
||||
selectedIndex === i ? 'bg-indigo-500 text-white' : 'text-gray-900'
|
||||
)}
|
||||
onClick={() => submitUser(i)}
|
||||
>
|
||||
<Avatar avatarUrl={user.avatarUrl} size="xs" />
|
||||
{user.username}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
72
web/components/editor/mention-suggestion.ts
Normal file
72
web/components/editor/mention-suggestion.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import type { MentionOptions } from '@tiptap/extension-mention'
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import { User } from 'common/user'
|
||||
import { searchInAny } from 'common/util/parse'
|
||||
import { orderBy } from 'lodash'
|
||||
import tippy from 'tippy.js'
|
||||
import { MentionList } from './mention-list'
|
||||
|
||||
type Suggestion = MentionOptions['suggestion']
|
||||
|
||||
const beginsWith = (text: string, query: string) =>
|
||||
text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase())
|
||||
|
||||
// copied from https://tiptap.dev/api/nodes/mention#usage
|
||||
export const mentionSuggestion = (users: User[]): Suggestion => ({
|
||||
items: ({ query }) =>
|
||||
orderBy(
|
||||
users.filter((u) => searchInAny(query, u.username, u.name)),
|
||||
[
|
||||
(u) => [u.name, u.username].some((s) => beginsWith(s, query)),
|
||||
'followerCountCached',
|
||||
],
|
||||
['desc', 'desc']
|
||||
).slice(0, 5),
|
||||
render: () => {
|
||||
let component: ReactRenderer
|
||||
let popup: ReturnType<typeof tippy>
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
return true
|
||||
}
|
||||
return (component.ref as any)?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
29
web/components/editor/mention.tsx
Normal file
29
web/components/editor/mention.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Mention from '@tiptap/extension-mention'
|
||||
import {
|
||||
mergeAttributes,
|
||||
NodeViewWrapper,
|
||||
ReactNodeViewRenderer,
|
||||
} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import { Linkify } from '../linkify'
|
||||
|
||||
const name = 'mention-component'
|
||||
|
||||
const MentionComponent = (props: any) => {
|
||||
return (
|
||||
<NodeViewWrapper className={clsx(name, 'not-prose inline text-indigo-700')}>
|
||||
<Linkify text={'@' + props.node.attrs.label} />
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mention extension that renders React. See:
|
||||
* https://tiptap.dev/guide/custom-extensions#extend-existing-extensions
|
||||
* https://tiptap.dev/guide/node-views/react#render-a-react-component
|
||||
*/
|
||||
export const DisplayMention = Mention.extend({
|
||||
parseHTML: () => [{ tag: name }],
|
||||
renderHTML: ({ HTMLAttributes }) => [name, mergeAttributes(HTMLAttributes)],
|
||||
addNodeView: () => ReactNodeViewRenderer(MentionComponent),
|
||||
})
|
|
@ -2,7 +2,6 @@ import { Contract } from 'web/lib/firebase/contracts'
|
|||
import { Comment } from 'web/lib/firebase/comments'
|
||||
import { Bet } from 'common/bet'
|
||||
import { useBets } from 'web/hooks/use-bets'
|
||||
import { useComments } from 'web/hooks/use-comments'
|
||||
import { getSpecificContractActivityItems } from './activity-items'
|
||||
import { FeedItems } from './feed-items'
|
||||
import { User } from 'common/user'
|
||||
|
@ -26,10 +25,7 @@ export function ContractActivity(props: {
|
|||
props
|
||||
|
||||
const contract = useContractWithPreload(props.contract) ?? props.contract
|
||||
|
||||
const updatedComments = useComments(contract.id)
|
||||
const comments = updatedComments ?? props.comments
|
||||
|
||||
const comments = props.comments
|
||||
const updatedBets = useBets(contract.id)
|
||||
const bets = (updatedBets ?? props.bets).filter(
|
||||
(bet) => !bet.isRedemption && bet.amount !== 0
|
||||
|
|
|
@ -10,6 +10,8 @@ type Tab = {
|
|||
content: ReactNode
|
||||
// If set, change the url to this href when the tab is selected
|
||||
href?: string
|
||||
// If set, show a badge with this content
|
||||
badge?: string
|
||||
}
|
||||
|
||||
export function Tabs(props: {
|
||||
|
@ -63,6 +65,9 @@ export function Tabs(props: {
|
|||
>
|
||||
<Row className={'items-center justify-center gap-1'}>
|
||||
{tab.tabIcon && <span> {tab.tabIcon}</span>}
|
||||
{tab.badge ? (
|
||||
<div className="px-0.5 font-bold">{tab.badge}</div>
|
||||
) : null}
|
||||
{tab.title}
|
||||
</Row>
|
||||
</a>
|
||||
|
|
|
@ -62,4 +62,6 @@ const visuallyHiddenStyle = {
|
|||
position: 'absolute',
|
||||
width: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
visibility: 'hidden',
|
||||
} as const
|
||||
|
|
|
@ -187,6 +187,7 @@ export async function setContractGroupLinks(
|
|||
userId: string
|
||||
) {
|
||||
await updateContract(contractId, {
|
||||
groupSlugs: [group.slug],
|
||||
groupLinks: [
|
||||
{
|
||||
groupId: group.id,
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"",
|
||||
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=DEV next dev -p 3000\" \"cross-env FIREBASE_ENV=DEV yarn ts --watch\"",
|
||||
"serve": "next dev -p 3000",
|
||||
"ts-watch": "tsc --watch --noEmit --incremental --preserveWatchOutput --pretty",
|
||||
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"",
|
||||
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev",
|
||||
"dev:dev": "yarn devdev",
|
||||
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"",
|
||||
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE yarn dev",
|
||||
"dev:local": "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8080 yarn devdev",
|
||||
"dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev",
|
||||
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
@ -27,6 +29,7 @@
|
|||
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/extension-placeholder": "2.0.0-beta.53",
|
||||
"@tiptap/react": "2.0.0-beta.114",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
|
@ -50,7 +53,8 @@
|
|||
"react-hot-toast": "2.2.0",
|
||||
"react-instantsearch-hooks-web": "6.24.1",
|
||||
"react-query": "3.39.0",
|
||||
"string-similarity": "^4.0.4"
|
||||
"string-similarity": "^4.0.4",
|
||||
"tippy.js": "6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "0.4.0",
|
||||
|
@ -61,7 +65,6 @@
|
|||
"@types/react": "17.0.43",
|
||||
"@types/string-similarity": "^4.0.0",
|
||||
"autoprefixer": "10.2.6",
|
||||
"concurrently": "6.5.1",
|
||||
"critters": "0.0.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-next": "12.1.6",
|
||||
|
|
163
yarn.lock
163
yarn.lock
|
@ -1353,6 +1353,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-2.6.148.tgz#8fa825d53ffd1cbcafce1b6a830eefd3dcc09dd5"
|
||||
integrity sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA==
|
||||
|
||||
"@cspotcode/source-map-support@^0.8.0":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@docsearch/css@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.1.0.tgz#6781cad43fc2e034d012ee44beddf8f93ba21f19"
|
||||
|
@ -2337,6 +2344,14 @@
|
|||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c"
|
||||
integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==
|
||||
|
||||
"@jridgewell/trace-mapping@0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.9":
|
||||
version "0.3.13"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea"
|
||||
|
@ -3010,6 +3025,15 @@
|
|||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843"
|
||||
integrity sha512-AkzvdELz3ZnrlZM0r9+ritBDOnAjXHR/8zCZhW0ZlWx4zyKPMsNG5ygivY+xr4QT65NEGRT8P8b2zOhXrMjjMQ==
|
||||
|
||||
"@tiptap/extension-mention@2.0.0-beta.102":
|
||||
version "2.0.0-beta.102"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.102.tgz#a80036b0a4481efc4f69b788af3f5c76428624cc"
|
||||
integrity sha512-QTBBpWnRnoV7/ZW31HwhPvZL3HiwnlehlHSLeMioVxAQPF5WrRtlOpxK/SRu7+KuwdCb7ZA1eWW/yjbXI3oktg==
|
||||
dependencies:
|
||||
"@tiptap/suggestion" "^2.0.0-beta.97"
|
||||
prosemirror-model "1.18.1"
|
||||
prosemirror-state "1.4.1"
|
||||
|
||||
"@tiptap/extension-ordered-list@^2.0.0-beta.30":
|
||||
version "2.0.0-beta.30"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.30.tgz#1f656b664302d90272c244b2e478d7056203f2a8"
|
||||
|
@ -3073,6 +3097,15 @@
|
|||
"@tiptap/extension-strike" "^2.0.0-beta.29"
|
||||
"@tiptap/extension-text" "^2.0.0-beta.17"
|
||||
|
||||
"@tiptap/suggestion@^2.0.0-beta.97":
|
||||
version "2.0.0-beta.97"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.97.tgz#2e3dc20deebc2c37c5d39c848e61e9c837e7188a"
|
||||
integrity sha512-3NWG+HE7v2w97Ek6z1tUosoZKpCDH+oAtIG9XoNkK1PmlaVV/H4d6HT9uPX+Y6SeN7fSAqlcXFUGLXcDi9d+Zw==
|
||||
dependencies:
|
||||
prosemirror-model "1.18.1"
|
||||
prosemirror-state "1.4.1"
|
||||
prosemirror-view "1.26.2"
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
|
@ -3088,6 +3121,26 @@
|
|||
resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.5.tgz#5298c5b0333c6263f06c3149b38ebccc9f169a4e"
|
||||
integrity sha512-KM/TuJa9fugo67dTGx+ktIqf3fVc077J6jwHu845Hex4EQf7LABlNonP/mohDKT0cmncdtlYVHHF74xR/YpThg==
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
||||
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
|
||||
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
|
||||
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||
|
@ -3652,7 +3705,7 @@ acorn-walk@^7.0.0:
|
|||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
|
||||
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
|
||||
|
||||
acorn-walk@^8.0.0:
|
||||
acorn-walk@^8.0.0, acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
|
@ -3836,6 +3889,11 @@ anymatch@~3.1.2:
|
|||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
arg@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb"
|
||||
|
@ -4399,7 +4457,7 @@ cheerio@^1.0.0-rc.10:
|
|||
parse5-htmlparser2-tree-adapter "^7.0.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
chokidar@^3.4.2, chokidar@^3.5.3:
|
||||
chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||
|
@ -4761,6 +4819,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
critters@0.0.16:
|
||||
version "0.0.16"
|
||||
resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93"
|
||||
|
@ -5263,6 +5326,11 @@ didyoumean@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
|
||||
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
|
@ -5916,7 +5984,7 @@ execa@^5.0.0:
|
|||
signal-exit "^3.0.3"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
express@^4.16.4, express@^4.17.1, express@^4.17.3:
|
||||
express@4.18.1, express@^4.16.4, express@^4.17.1, express@^4.17.3:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
||||
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
|
||||
|
@ -7048,6 +7116,11 @@ idb@3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384"
|
||||
integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==
|
||||
|
||||
ignore-by-default@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||
|
||||
ignore@^5.1.9, ignore@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
|
@ -8072,6 +8145,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
|
|||
dependencies:
|
||||
semver "^6.0.0"
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
markdown-escapes@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
|
||||
|
@ -8423,7 +8501,23 @@ node-releases@^2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476"
|
||||
integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==
|
||||
|
||||
nopt@1.0.10:
|
||||
nodemon@2.0.19:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.19.tgz#cac175f74b9cb8b57e770d47841995eebe4488bd"
|
||||
integrity sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A==
|
||||
dependencies:
|
||||
chokidar "^3.5.2"
|
||||
debug "^3.2.7"
|
||||
ignore-by-default "^1.0.1"
|
||||
minimatch "^3.0.4"
|
||||
pstree.remy "^1.1.8"
|
||||
semver "^5.7.1"
|
||||
simple-update-notifier "^1.0.7"
|
||||
supports-color "^5.5.0"
|
||||
touch "^3.1.0"
|
||||
undefsafe "^2.0.5"
|
||||
|
||||
nopt@1.0.10, nopt@~1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
|
||||
integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=
|
||||
|
@ -9541,6 +9635,11 @@ pseudomap@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||
|
||||
pstree.remy@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
||||
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
|
@ -10411,12 +10510,12 @@ semver-diff@^3.1.1:
|
|||
dependencies:
|
||||
semver "^6.3.0"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0, semver@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@7.0.0:
|
||||
semver@7.0.0, semver@~7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||
|
@ -10563,6 +10662,13 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
|
|||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
simple-update-notifier@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc"
|
||||
integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==
|
||||
dependencies:
|
||||
semver "~7.0.0"
|
||||
|
||||
sirv@^1.0.7:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
|
||||
|
@ -10926,7 +11032,7 @@ stylehacks@^5.1.0:
|
|||
browserslist "^4.16.6"
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
supports-color@^5.3.0:
|
||||
supports-color@^5.3.0, supports-color@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
|
@ -11065,7 +11171,7 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tippy.js@^6.3.7:
|
||||
tippy.js@6.3.7, tippy.js@^6.3.7:
|
||||
version "6.3.7"
|
||||
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
|
||||
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
|
||||
|
@ -11106,6 +11212,13 @@ totalist@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
|
||||
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
|
||||
|
||||
touch@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
|
||||
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
|
||||
dependencies:
|
||||
nopt "~1.0.10"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
|
@ -11131,6 +11244,25 @@ trough@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
||||
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
|
||||
|
||||
ts-node@10.9.1:
|
||||
version "10.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
||||
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "^0.8.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
tsc-files@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tsc-files/-/tsc-files-1.1.3.tgz#ef4cfcb7affc9b90577d707a879dc53bb105be83"
|
||||
|
@ -11242,6 +11374,11 @@ unbox-primitive@^1.0.2:
|
|||
has-symbols "^1.0.3"
|
||||
which-boxed-primitive "^1.0.2"
|
||||
|
||||
undefsafe@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
||||
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
|
||||
|
||||
unherit@^1.0.4:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
|
||||
|
@ -11505,6 +11642,11 @@ uzip@0.20201231.0:
|
|||
resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14"
|
||||
integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
|
@ -11924,6 +12066,11 @@ yargs@^16.2.0:
|
|||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
|
Loading…
Reference in New Issue
Block a user