diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index b27ac977..7c2153c1 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -21,6 +21,25 @@ const computeInvestmentValue = ( }) } +export const computeInvestmentValueCustomProb = ( + bets: Bet[], + contract: Contract, + p: number +) => { + return sumBy(bets, (bet) => { + if (!contract || contract.isResolved) return 0 + if (bet.sale || bet.isSold) return 0 + const { outcome, shares } = bet + + const betP = outcome === 'YES' ? p : 1 - p + + const payout = betP * shares + const value = payout - (bet.loanAmount ?? 0) + if (isNaN(value)) return 0 + return value + }) +} + const computeTotalPool = (userContracts: Contract[], startTime = 0) => { const periodFilteredContracts = userContracts.filter( (contract) => contract.createdTime >= startTime diff --git a/common/group.ts b/common/group.ts index 871bc821..5220a1e8 100644 --- a/common/group.ts +++ b/common/group.ts @@ -10,6 +10,7 @@ export type Group = { totalContracts: number totalMembers: number aboutPostId?: string + postIds: string[] chatDisabled?: boolean mostRecentContractAddedTime?: number cachedLeaderboard?: { diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts index bf6f5ebc..48850dca 100644 --- a/common/payouts-dpm.ts +++ b/common/payouts-dpm.ts @@ -168,7 +168,7 @@ export const getPayoutsMultiOutcome = ( const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal const profit = winnings - amount - const payout = amount + (1 - DPM_FEES) * Math.max(0, profit) + const payout = amount + (1 - DPM_FEES) * profit return { userId, profit, payout } }) diff --git a/common/user.ts b/common/user.ts index 0372d99b..b1365929 100644 --- a/common/user.ts +++ b/common/user.ts @@ -57,6 +57,7 @@ export type PrivateUser = { email?: string weeklyTrendingEmailSent?: boolean + weeklyPortfolioUpdateEmailSent?: boolean manaBonusEmailSent?: boolean initialDeviceToken?: string initialIpAddress?: string diff --git a/common/util/time.ts b/common/util/time.ts index 9afb8db4..81dc3600 100644 --- a/common/util/time.ts +++ b/common/util/time.ts @@ -1,3 +1,6 @@ export const MINUTE_MS = 60 * 1000 export const HOUR_MS = 60 * MINUTE_MS export const DAY_MS = 24 * HOUR_MS + +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/functions/package.json b/functions/package.json index ba59f090..0397c5db 100644 --- a/functions/package.json +++ b/functions/package.json @@ -40,7 +40,6 @@ "mailgun-js": "0.22.0", "module-alias": "2.2.2", "node-fetch": "2", - "react-masonry-css": "1.0.16", "stripe": "8.194.0", "zod": "3.17.2" }, @@ -48,7 +47,8 @@ "@types/mailgun-js": "0.22.12", "@types/module-alias": "2.0.1", "@types/node-fetch": "2.6.2", - "firebase-functions-test": "0.3.3" + "firebase-functions-test": "0.3.3", + "puppeteer": "18.0.5" }, "private": true } diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index cc05d817..911f3b8c 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -7,6 +7,7 @@ import { getNewMultiBetInfo } from '../../common/new-bet' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { getValues } from './utils' import { APIError, newEndpoint, validate } from './api' +import { addUserToContractFollowers } from './follow-market' const bodySchema = z.object({ contractId: z.string().max(MAX_ANSWER_LENGTH), @@ -96,6 +97,8 @@ export const createanswer = newEndpoint(opts, async (req, auth) => { return answer }) + await addUserToContractFollowers(contractId, auth.uid) + return answer }) diff --git a/functions/src/create-group.ts b/functions/src/create-group.ts index 9d00bb0b..76dc1298 100644 --- a/functions/src/create-group.ts +++ b/functions/src/create-group.ts @@ -61,6 +61,7 @@ export const creategroup = newEndpoint({}, async (req, auth) => { anyoneCanJoin, totalContracts: 0, totalMembers: memberIds.length, + postIds: [], } await groupRef.create(group) diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index 40d39bba..113a34bd 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -34,11 +34,12 @@ const contentSchema: z.ZodType = z.lazy(() => const postSchema = z.object({ title: z.string().min(1).max(MAX_POST_TITLE_LENGTH), content: contentSchema, + groupId: z.string().optional(), }) export const createpost = newEndpoint({}, async (req, auth) => { const firestore = admin.firestore() - const { title, content } = validate(postSchema, req.body) + const { title, content, groupId } = validate(postSchema, req.body) const creator = await getUser(auth.uid) if (!creator) @@ -60,6 +61,18 @@ export const createpost = newEndpoint({}, async (req, auth) => { } await postRef.create(post) + if (groupId) { + const groupRef = firestore.collection('groups').doc(groupId) + const group = await groupRef.get() + if (group.exists) { + const groupData = group.data() + if (groupData) { + const postIds = groupData.postIds ?? [] + postIds.push(postRef.id) + await groupRef.update({ postIds }) + } + } + } return { status: 'success', post } }) diff --git a/functions/src/email-templates/weekly-portfolio-update-no-movers.html b/functions/src/email-templates/weekly-portfolio-update-no-movers.html new file mode 100644 index 00000000..15303992 --- /dev/null +++ b/functions/src/email-templates/weekly-portfolio-update-no-movers.html @@ -0,0 +1,411 @@ + + + + + Weekly Portfolio Update on Manifold + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + + + + + + +
+ + + + banner logo + + + +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+
+

+ Hi {{name}},

+
+
+
+

+ + We ran the numbers and here's how you did this past week! + +

+
+
+ Profit +
+

+ {{profit}} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 🔥 Prediction streak + + {{prediction_streak}} +
+ 💸 Tips received + + {{tips_received}} +
+ 📈 Markets traded + + {{markets_traded}} +
+ ❓ Markets created + + {{markets_created}} +
+ 🥳 Traders attracted + + {{unique_bettors}} +
+ +
+
+
+
+ + +
+ + + + + + +
+ + +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

+ This e-mail has been sent to + {{name}}, + click here to unsubscribe from this type of notification. +

+
+
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/functions/src/email-templates/weekly-portfolio-update.html b/functions/src/email-templates/weekly-portfolio-update.html new file mode 100644 index 00000000..fd99837f --- /dev/null +++ b/functions/src/email-templates/weekly-portfolio-update.html @@ -0,0 +1,510 @@ + + + + + Weekly Portfolio Update on Manifold + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + + + + + + +
+ + + + banner logo + + + +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

+ Hi {{name}},

+
+
+
+

+ + We ran the numbers and here's how you did this past week! + +

+
+
+ Profit +
+

+ {{profit}} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 🔥 Prediction streak + + {{prediction_streak}} +
+ 💸 Tips received + + {{tips_received}} +
+ 📈 Markets traded + + {{markets_traded}} +
+ ❓ Markets created + + {{markets_created}} +
+ 🥳 Traders attracted + + {{unique_bettors}} +
+ +
+
+

+ + And here's some of the biggest changes in your portfolio: + +

+
+
+ + + + + + + + + + + + + + + + + + +
+ + {{question1Title}} + + + +

+ {{question1Prob}} + +

+ {{question1Change}} + +

+

+
+ + {{question2Title}} + + + +

+ {{question2Prob}} + +

+ {{question2Change}} + +

+

+
+ + {{question3Title}} + + + +

+ {{question3Prob}} + +

+ {{question3Change}} + +

+

+
+ + {{question4Title}} + + + +

+ {{question4Prob}} + +

+ {{question4Change}} + +

+

+
+ +
+
+
+
+ + +
+ + + + + + +
+ + +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
+

+ This e-mail has been sent to + {{name}}, + click here to unsubscribe from this type of notification. +

+
+
+
+
+ +
+
+ +
+
diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 98309ebe..dd91789a 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -12,14 +12,15 @@ import { getValueFromBucket } from '../../common/calculate-dpm' import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail, sendTextEmail } from './send-email' -import { getUser } from './utils' +import { contractUrl, getUser } from './utils' import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details' import { notification_reason_types } from '../../common/notification' import { Dictionary } from 'lodash' +import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences' import { - getNotificationDestinationsForUser, - notification_preference, -} from '../../common/user-notification-preferences' + PerContractInvestmentsData, + OverallPerformanceData, +} from 'functions/src/weekly-portfolio-emails' export const sendMarketResolutionEmail = async ( reason: notification_reason_types, @@ -152,9 +153,10 @@ export const sendWelcomeEmail = async ( const { name } = user const firstName = name.split(' ')[0] - const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${ - 'onboarding_flow' as notification_preference - }` + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'onboarding_flow' + ) return await sendTemplateEmail( privateUser.email, @@ -220,9 +222,11 @@ export const sendOneWeekBonusEmail = async ( const { name } = user const firstName = name.split(' ')[0] - const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${ - 'onboarding_flow' as notification_preference - }` + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'onboarding_flow' + ) + return await sendTemplateEmail( privateUser.email, 'Manifold Markets one week anniversary gift', @@ -252,10 +256,10 @@ export const sendCreatorGuideEmail = async ( const { name } = user const firstName = name.split(' ')[0] - - const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${ - 'onboarding_flow' as notification_preference - }` + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'onboarding_flow' + ) return await sendTemplateEmail( privateUser.email, 'Create your own prediction market', @@ -286,10 +290,10 @@ export const sendThankYouEmail = async ( const { name } = user const firstName = name.split(' ')[0] - - const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${ - 'thank_you_for_purchases' as notification_preference - }` + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'thank_you_for_purchases' + ) return await sendTemplateEmail( privateUser.email, @@ -469,9 +473,10 @@ export const sendInterestingMarketsEmail = async ( ) return - const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${ - 'trending_markets' as notification_preference - }` + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'trending_markets' + ) const { name } = user const firstName = name.split(' ')[0] @@ -507,10 +512,6 @@ export const sendInterestingMarketsEmail = async ( ) } -function contractUrl(contract: Contract) { - return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}` -} - function imageSourceUrl(contract: Contract) { return buildCardUrl(getOpenGraphProps(contract)) } @@ -612,3 +613,47 @@ export const sendNewUniqueBettorsEmail = async ( } ) } + +export const sendWeeklyPortfolioUpdateEmail = async ( + user: User, + privateUser: PrivateUser, + investments: PerContractInvestmentsData[], + overallPerformance: OverallPerformanceData +) => { + if ( + !privateUser || + !privateUser.email || + !privateUser.notificationPreferences.profit_loss_updates.includes('email') + ) + return + + const { unsubscribeUrl } = getNotificationDestinationsForUser( + privateUser, + 'profit_loss_updates' + ) + + const { name } = user + const firstName = name.split(' ')[0] + const templateData: Record = { + name: firstName, + unsubscribeUrl, + ...overallPerformance, + } + investments.forEach((investment, i) => { + templateData[`question${i + 1}Title`] = investment.questionTitle + templateData[`question${i + 1}Url`] = investment.questionUrl + templateData[`question${i + 1}Prob`] = investment.questionProb + templateData[`question${i + 1}Change`] = investment.questionChange + templateData[`question${i + 1}ChangeStyle`] = investment.questionChangeStyle + }) + + await sendTemplateEmail( + privateUser.email, + // 'iansphilips@gmail.com', + `Here's your weekly portfolio update!`, + investments.length === 0 + ? 'portfolio-update-no-movers' + : 'portfolio-update', + templateData + ) +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 2cb6f515..9a8ec232 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -27,9 +27,10 @@ export * from './on-delete-group' export * from './score-contracts' export * from './weekly-markets-emails' export * from './reset-betting-streaks' -export * from './reset-weekly-emails-flag' +export * from './reset-weekly-emails-flags' export * from './on-update-contract-follow' export * from './on-update-like' +export * from './weekly-portfolio-emails' // v2 export * from './health' diff --git a/functions/src/market-close-notifications.ts b/functions/src/market-close-notifications.ts index 7878e410..21b52fbc 100644 --- a/functions/src/market-close-notifications.ts +++ b/functions/src/market-close-notifications.ts @@ -60,7 +60,7 @@ async function sendMarketCloseEmails() { 'contract', 'closed', user, - 'closed' + contract.id.slice(6, contract.id.length), + contract.id + '-closed-at-' + contract.closeTime, contract.closeTime?.toString() ?? new Date().toString(), { contract } ) diff --git a/functions/src/reset-weekly-emails-flag.ts b/functions/src/reset-weekly-emails-flags.ts similarity index 87% rename from functions/src/reset-weekly-emails-flag.ts rename to functions/src/reset-weekly-emails-flags.ts index 947e3e88..1b2109a1 100644 --- a/functions/src/reset-weekly-emails-flag.ts +++ b/functions/src/reset-weekly-emails-flags.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { getAllPrivateUsers } from './utils' -export const resetWeeklyEmailsFlag = functions +export const resetWeeklyEmailsFlags = functions .runWith({ timeoutSeconds: 300, memory: '4GB', @@ -17,6 +17,7 @@ export const resetWeeklyEmailsFlag = functions privateUsers.map(async (user) => { return firestore.collection('private-users').doc(user.id).update({ weeklyTrendingEmailSent: false, + weeklyPortfolioUpdateEmailSent: false, }) }) ) diff --git a/functions/src/score-contracts.ts b/functions/src/score-contracts.ts index 52ef39d4..497a4ba0 100644 --- a/functions/src/score-contracts.ts +++ b/functions/src/score-contracts.ts @@ -5,6 +5,7 @@ import { Bet } from '../../common/bet' import { Contract } from '../../common/contract' import { log } from './utils' import { removeUndefinedProps } from '../../common/util/object' +import { DAY_MS, HOUR_MS } from '../../common/util/time' export const scoreContracts = functions .runWith({ memory: '4GB', timeoutSeconds: 540 }) @@ -16,11 +17,12 @@ const firestore = admin.firestore() async function scoreContractsInternal() { const now = Date.now() - const lastHour = now - 60 * 60 * 1000 - const last3Days = now - 1000 * 60 * 60 * 24 * 3 + const hourAgo = now - HOUR_MS + const dayAgo = now - DAY_MS + const threeDaysAgo = now - DAY_MS * 3 const activeContractsSnap = await firestore .collection('contracts') - .where('lastUpdatedTime', '>', lastHour) + .where('lastUpdatedTime', '>', hourAgo) .get() const activeContracts = activeContractsSnap.docs.map( (doc) => doc.data() as Contract @@ -41,15 +43,21 @@ async function scoreContractsInternal() { for (const contract of contracts) { const bets = await firestore .collection(`contracts/${contract.id}/bets`) - .where('createdTime', '>', last3Days) + .where('createdTime', '>', threeDaysAgo) .get() const bettors = bets.docs .map((doc) => doc.data() as Bet) .map((bet) => bet.userId) const popularityScore = uniq(bettors).length + const wasCreatedToday = contract.createdTime > dayAgo + let dailyScore: number | undefined - if (contract.outcomeType === 'BINARY' && contract.mechanism === 'cpmm-1') { + if ( + contract.outcomeType === 'BINARY' && + contract.mechanism === 'cpmm-1' && + !wasCreatedToday + ) { const percentChange = Math.abs(contract.probChanges.day) dailyScore = popularityScore * percentChange } diff --git a/functions/src/scripts/contest/create-markets.ts b/functions/src/scripts/contest/create-markets.ts new file mode 100644 index 00000000..ba7245fe --- /dev/null +++ b/functions/src/scripts/contest/create-markets.ts @@ -0,0 +1,115 @@ +// Run with `npx ts-node src/scripts/contest/create-markets.ts` + +import { data } from './criticism-and-red-teaming' + +// Dev API key for Cause Exploration Prizes (@CEP) +// const API_KEY = '188f014c-0ba2-4c35-9e6d-88252e281dbf' +// DEV API key for Criticism and Red Teaming (@CARTBot) +const API_KEY = '6ff1f78a-32fe-43b2-b31b-9e3c78c5f18c' + +type CEPSubmission = { + title: string + author?: string + link: string +} + +// Use the API to create a new market for this Cause Exploration Prize submission +async function postMarket(submission: CEPSubmission) { + const { title, author } = submission + const response = await fetch('https://dev.manifold.markets/api/v0/market', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Key ${API_KEY}`, + }, + body: JSON.stringify({ + outcomeType: 'BINARY', + question: `"${title}" by ${author ?? 'anonymous'}`, + description: makeDescription(submission), + closeTime: Date.parse('2022-09-30').valueOf(), + initialProb: 10, + // Super secret options: + // groupId: 'y2hcaGybXT1UfobK3XTx', // [DEV] CEP Tournament + // groupId: 'cMcpBQ2p452jEcJD2SFw', // [PROD] Predict CEP + groupId: 'h3MhjYbSSG6HbxY8ZTwE', // [DEV] CART + // groupId: 'K86LmEmidMKdyCHdHNv4', // [PROD] CART + visibility: 'unlisted', + // TODO: Increase liquidity? + }), + }) + const data = await response.json() + console.log('Created market:', data.slug) +} + +async function postAll() { + for (const submission of data.slice(0, 3)) { + await postMarket(submission) + } +} +postAll() + +/* Example curl request: +$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' + --data-raw '{"outcomeType":"BINARY", \ + "question":"Is there life on Mars?", \ + "description":"I'm not going to type some long ass example description.", \ + "closeTime":1700000000000, \ + "initialProb":25}' +*/ + +function makeDescription(submission: CEPSubmission) { + const { title, author, link } = submission + return { + content: [ + { + content: [ + { text: `Will ${author ?? 'anonymous'}'s post "`, type: 'text' }, + { + marks: [ + { + attrs: { + target: '_blank', + href: link, + class: + 'no-underline !text-indigo-700 z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2', + }, + type: 'link', + }, + ], + type: 'text', + text: title, + }, + { text: '" win any prize in the ', type: 'text' }, + { + text: 'EA Criticism and Red Teaming Contest', + type: 'text', + marks: [ + { + attrs: { + target: '_blank', + class: + 'no-underline !text-indigo-700 z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2', + href: 'https://forum.effectivealtruism.org/posts/8hvmvrgcxJJ2pYR4X/announcing-a-contest-ea-criticism-and-red-teaming', + }, + type: 'link', + }, + ], + }, + { text: '?', type: 'text' }, + ], + type: 'paragraph', + }, + { type: 'paragraph' }, + { + type: 'iframe', + attrs: { + allowfullscreen: true, + src: link, + frameborder: 0, + }, + }, + ], + type: 'doc', + } +} diff --git a/functions/src/scripts/contest/criticism-and-red-teaming.ts b/functions/src/scripts/contest/criticism-and-red-teaming.ts new file mode 100644 index 00000000..0f19705a --- /dev/null +++ b/functions/src/scripts/contest/criticism-and-red-teaming.ts @@ -0,0 +1,1219 @@ +export const data = [ + // { + // "title": "Announcing a contest: EA Criticism and Red Teaming", + // "author": "Lizka", + // "link": "https://forum.effectivealtruism.org/posts/8hvmvrgcxJJ2pYR4X/announcing-a-contest-ea-criticism-and-red-teaming" + // }, + // { + // "title": "Pre-announcing a contest for critiques and red teaming", + // "author": "Lizka", + // "link": "https://forum.effectivealtruism.org/posts/Fx8pWSLKGwuqsfuRQ/pre-announcing-a-contest-for-critiques-and-red-teaming" + // }, + // { + // "title": "Resource for criticisms and red teaming", + // "author": "Lizka", + // "link": "https://forum.effectivealtruism.org/posts/uuQDgiJJaswEyyzan/resource-for-criticisms-and-red-teaming" + // }, + { + title: + 'Deworming and decay: replicating GiveWell’s cost-effectiveness analysis ', + author: 'JoelMcGuire', + link: 'https://forum.effectivealtruism.org/posts/MKiqGvijAXfcBHCYJ/deworming-and-decay-replicating-givewell-s-cost', + }, + { + title: 'Critiques of EA that I want to read', + author: 'abrahamrowe', + link: 'https://forum.effectivealtruism.org/posts/n3WwTz4dbktYwNQ2j/critiques-of-ea-that-i-want-to-read', + }, + { + title: 'My take on What We Owe the Future', + author: 'elifland', + link: 'https://forum.effectivealtruism.org/posts/9Y6Y6qoAigRC7A8eX/my-take-on-what-we-owe-the-future', + }, + { + title: + 'A Critical Review of Open Philanthropy’s Bet On Criminal Justice Reform', + author: 'NunoSempere', + link: 'https://forum.effectivealtruism.org/posts/h2N9qEbvQ6RHABcae/a-critical-review-of-open-philanthropy-s-bet-on-criminal', + }, + { + title: 'Leaning into EA Disillusionment', + author: 'Helen', + link: 'https://forum.effectivealtruism.org/posts/MjTB4MvtedbLjgyja/leaning-into-ea-disillusionment', + }, + { + title: 'Red Teaming CEA’s Community Building Work', + author: 'AnonymousEAForumAccount', + link: 'https://forum.effectivealtruism.org/posts/hbejbRBpd6quqnTAB/red-teaming-cea-s-community-building-work-2', + }, + { + title: + 'A philosophical review of Open Philanthropy’s Cause Prioritisation Framework', + author: 'MichaelPlant', + link: 'https://forum.effectivealtruism.org/posts/bdiDW83SFAsoA4EeB/a-philosophical-review-of-open-philanthropy-s-cause', + }, + { + title: 'The Future Might Not Be So Great', + author: 'Jacy', + link: 'https://forum.effectivealtruism.org/posts/WebLP36BYDbMAKoa5/the-future-might-not-be-so-great', + }, + { + title: 'Potatoes: A Critical Review', + author: 'Pablo Villalobos', + link: 'https://forum.effectivealtruism.org/posts/iZrrWGvx2s2uPtica/potatoes-a-critical-review', + }, + { + title: 'Effective Altruism as Coordination & Field Incubation', + author: 'DavidNash', + link: 'https://forum.effectivealtruism.org/posts/Zm6iaaJhoZsoZ2uMD/effective-altruism-as-coordination-and-field-incubation', + }, + { + title: 'Enlightenment Values in a Vulnerable World', + author: 'Maxwell Tabarrok', + link: 'https://forum.effectivealtruism.org/posts/A4fMkKhBxio83NtBL/enlightenment-values-in-a-vulnerable-world', + }, + { + title: "21 criticisms of EA I'm thinking about", + author: 'Peter Wildeford', + link: 'https://forum.effectivealtruism.org/posts/X47rn28Xy5TRfGgSj/21-criticisms-of-ea-i-m-thinking-about', + }, + { + title: 'Longtermism and Computational Complexity', + author: 'David Kinney', + link: 'https://forum.effectivealtruism.org/posts/RRyHcupuDafFNXt6p/longtermism-and-computational-complexity', + }, + { + title: 'The Community Manifesto', + author: 'dianaqianmorgan', + link: 'https://forum.effectivealtruism.org/posts/cY3wBXoJoeHXJ7XYt/the-community-manifesto', + }, + { + title: 'Existential risk pessimism and the time of perils', + author: 'David Thorstad', + link: 'https://forum.effectivealtruism.org/posts/N6hcw8CxK7D3FCD5v/existential-risk-pessimism-and-the-time-of-perils-4', + }, + { + title: "A critical review of GiveWell's 2022 cost-effectiveness model", + author: 'Froolow', + link: 'https://forum.effectivealtruism.org/posts/6dtwkwBrHBGtc3xes/a-critical-review-of-givewell-s-2022-cost-effectiveness', + }, + { + title: 'How EA is perceived is crucial to its future trajectory', + author: 'GidonKadosh', + link: 'https://forum.effectivealtruism.org/posts/82ig8odF9ooccfJfa/how-ea-is-perceived-is-crucial-to-its-future-trajectory', + }, + { + title: + 'Before There Was Effective Altruism, There Was Effective Philanthropy', + author: 'ColdButtonIssues', + link: 'https://forum.effectivealtruism.org/posts/CdrKtaAX69iJuJD2r/before-there-was-effective-altruism-there-was-effective', + }, + { + title: + 'A concern about the “evolutionary anchor” of Ajeya Cotra’s report on AI timelines.', + author: 'NunoSempere', + link: 'https://forum.effectivealtruism.org/posts/FHTyixYNnGaQfEexH/a-concern-about-the-evolutionary-anchor-of-ajeya-cotra-s', + }, + { + title: + 'EA is becoming increasingly inaccessible, at the worst possible time', + author: 'Ann Garth', + link: 'https://forum.effectivealtruism.org/posts/duPDKhtXTJNAJBaSf/ea-is-becoming-increasingly-inaccessible-at-the-worst', + }, + { + title: 'Red-teaming contest: demographics and power structures in EA', + author: 'TheOtherHannah', + link: 'https://forum.effectivealtruism.org/posts/oD3zus6LhbhBj6z2F/red-teaming-contest-demographics-and-power-structures-in-ea', + }, + { + title: 'The Nietzschean Challenge to Effective Altruism', + author: 'Richard Y Chappell', + link: 'https://forum.effectivealtruism.org/posts/bedstSbqaP8aDBfDr/the-nietzschean-challenge-to-effective-altruism', + }, + { + title: + 'The Case for Funding New Long-Term Randomized Controlled Trials of Deworming', + author: 'MHR', + link: 'https://forum.effectivealtruism.org/posts/CyxZmwQ7gADwjBHG6/the-case-for-funding-new-long-term-randomized-controlled', + }, + { + title: 'Population Ethics Without Axiology: A Framework', + author: 'Lukas_Gloor', + link: 'https://forum.effectivealtruism.org/posts/dQvDxDMyueLyydHw4/population-ethics-without-axiology-a-framework', + }, + { + title: 'Questioning the Value of Extinction Risk Reduction', + author: 'Red Team 8', + link: 'https://forum.effectivealtruism.org/posts/eeDsHDoM9De4iGGLw/questioning-the-value-of-extinction-risk-reduction-1', + }, + { + title: 'Red teaming introductory EA courses', + author: 'Philip Hall Andersen', + link: 'https://forum.effectivealtruism.org/posts/JDEDsCaQd2CYm7QEi/red-teaming-introductory-ea-courses', + }, + { + title: 'Systemic Cascading Risks: Relevance in Longtermism & Value Lock-In', + author: 'Richard Ren', + link: 'https://forum.effectivealtruism.org/posts/mWGodAi9Mv2a2EbNj/systemic-cascading-risks-relevance-in-longtermism-and-value', + }, + { + title: + 'community building solely as a tool for impact creates toxic communities', + author: 'ruthgrace', + link: 'https://forum.effectivealtruism.org/posts/EpMQBmQv7e4yaDYYN/community-building-solely-as-a-tool-for-impact-creates-toxic', + }, + { + title: + 'Are you really in a race? The Cautionary Tales of Szilárd and Ellsberg', + author: 'HaydnBelfield', + link: 'https://forum.effectivealtruism.org/posts/cXBznkfoPJAjacFoT/are-you-really-in-a-race-the-cautionary-tales-of-szilard-and', + }, + { + title: + "Quantifying Uncertainty in GiveWell's GiveDirectly Cost-Effectiveness Analysis", + author: 'Hazelfire', + link: 'https://forum.effectivealtruism.org/posts/ycLhq4Bmep8ssr4wR/quantifying-uncertainty-in-givewell-s-givedirectly-cost', + }, + { + title: 'Changing the world through slack & hobbies', + author: 'Steven Byrnes', + link: 'https://forum.effectivealtruism.org/posts/ZkhABk4rRMqsNmwvf/changing-the-world-through-slack-and-hobbies', + }, + { + title: + 'Some concerns about policy work funding and the Long Term Future Fund', + author: 'weeatquince', + link: 'https://forum.effectivealtruism.org/posts/Xfon9oxyMFv47kFnc/some-concerns-about-policy-work-funding-and-the-long-term', + }, + { + title: + 'Why Effective Altruists Should Put a Higher Priority on Funding Academic Research', + author: 'Stuart Buck', + link: 'https://forum.effectivealtruism.org/posts/uTQKFNXrMXuwGe4vw/why-effective-altruists-should-put-a-higher-priority-on', + }, + { + title: 'Remuneration In Effective Altruism', + author: 'Stefan_Schubert', + link: 'https://forum.effectivealtruism.org/posts/wWnRtjDiyjRRgaFDb/remuneration-in-effective-altruism', + }, + { + title: "You Don't Need To Justify Everything", + author: 'ThomasW', + link: 'https://forum.effectivealtruism.org/posts/HX9ZDGwwSxAab46N9/you-don-t-need-to-justify-everything', + }, + { + title: 'EAs underestimate uncertainty in cause prioritisation', + author: 'freedomandutility', + link: 'https://forum.effectivealtruism.org/posts/Ekd3oATEZkBbJ95uD/eas-underestimate-uncertainty-in-cause-prioritisation', + }, + { + title: '"Doing Good Best" isn\'t the EA ideal', + author: 'Davidmanheim', + link: 'https://forum.effectivealtruism.org/posts/f9NpDx65zY6Qk9ofe/doing-good-best-isn-t-the-ea-ideal', + }, + { + title: 'The discount rate is not zero', + author: 'Thomaaas', + link: 'https://forum.effectivealtruism.org/posts/zLZMsthcqfmv5J6Ev/the-discount-rate-is-not-zero', + }, + { + title: 'Questioning the Foundations of EA', + author: 'Wei_Dai', + link: 'https://forum.effectivealtruism.org/posts/zvNwSG2Xvy8x5Rtba/questioning-the-foundations-of-ea', + }, + { + title: + 'Notes on how prizes may fail and how to reduce the risk of them failing', + author: 'Peter Wildeford', + link: 'https://forum.effectivealtruism.org/posts/h2WcJf7pg5Qdfhsm3/notes-on-how-prizes-may-fail-and-how-to-reduce-the-risk-of', + }, + { + title: 'EA Culture and Causes: Less is More', + author: 'Allen Bell', + link: 'https://forum.effectivealtruism.org/posts/FWHDX32ecr9aF4xKw/ea-culture-and-causes-less-is-more', + }, + { + title: 'Things usually end slowly', + author: 'OllieBase', + link: 'https://forum.effectivealtruism.org/posts/qLwtCuh6nDCsrsrMK/things-usually-end-slowly', + }, + { + title: + 'Doing good is a privilege. This needs to change if we want to do good long-term. ', + author: 'SofiaBalderson', + link: 'https://forum.effectivealtruism.org/posts/gicYG5ymk4pPzrKAd/doing-good-is-a-privilege-this-needs-to-change-if-we-want-to', + }, + { + title: 'Animal Zoo', + author: 'bericlair', + link: 'https://forum.effectivealtruism.org/posts/YfmdnkmnoWBhmBaQL/animal-zoo', + }, + { + title: 'Summaries are underrated', + author: 'Nathan Young', + link: 'https://forum.effectivealtruism.org/posts/nDawZHxDR3j53zdbf/summaries-are-underrated', + }, + { + title: 'Longtermism, risk, and extinction', + author: 'Richard Pettigrew', + link: 'https://forum.effectivealtruism.org/posts/xAoZotkzcY5mvmXFY/longtermism-risk-and-extinction', + }, + { + title: + 'Prioritisation should consider potential for ongoing evaluation alongside expected value and evidence quality', + author: 'freedomandutility', + link: 'https://forum.effectivealtruism.org/posts/orfgdYRZXNKQtqzmh/prioritisation-should-consider-potential-for-ongoing', + }, + { + title: + '“Existential Risk” is badly named and leads to narrow focus on astronomical waste', + author: 'freedomandutility', + link: 'https://forum.effectivealtruism.org/posts/qFdifovCmckujxEsq/existential-risk-is-badly-named-and-leads-to-narrow-focus-on', + }, + { + title: + 'The great energy descent (short version) - An important thing EA might have missed', + author: 'Corentin Biteau', + link: 'https://forum.effectivealtruism.org/posts/wXzc75txE5hbHqYug/the-great-energy-descent-short-version-an-important-thing-ea', + }, + { + title: 'The Long Reflection as the Great Stagnation ', + author: 'Larks', + link: 'https://forum.effectivealtruism.org/posts/o5Q8dXfnHTozW9jkY/the-long-reflection-as-the-great-stagnation', + }, + { + title: 'Community posts: The Forum needs a way to work in public', + author: 'Nathan Young', + link: 'https://forum.effectivealtruism.org/posts/NxWssGagWoQWErRer/community-posts-the-forum-needs-a-way-to-work-in-public', + }, + { + title: 'Improving Karma: $8mn of possible value (my estimate)', + author: 'Nathan Young', + link: 'https://forum.effectivealtruism.org/posts/YajssmjwKndBTahQx/improving-karma-usd8mn-of-possible-value-my-estimate', + }, + { + title: 'Leveraging labor shortages as a pathway to career impact', + author: 'IanDavidMoss', + link: 'https://forum.effectivealtruism.org/posts/xdMn6FeQGjrXDPnQj/leveraging-labor-shortages-as-a-pathway-to-career-impact', + }, + { + title: 'How to dissolve moral cluelessness about donating mosquito nets', + author: 'ben.smith', + link: 'https://forum.effectivealtruism.org/posts/9XgLq4eQHMWybDsrv/how-to-dissolve-moral-cluelessness-about-donating-mosquito-1', + }, + { + title: '[TikTok] Comparability between suffering and happiness', + author: 'Ben_West', + link: 'https://forum.effectivealtruism.org/posts/ASmtarf3ADYb2Xmrt/tiktok-comparability-between-suffering-and-happiness', + }, + { + title: + 'Red teaming a model for estimating the value of longtermist interventions - A critique of Tarsney\'s "The Epistemic Challenge to Longtermism"', + author: 'Anjay F', + link: 'https://forum.effectivealtruism.org/posts/u9CvMCCmQRgjBD828/red-teaming-a-model-for-estimating-the-value-of-longtermist', + }, + { + title: + 'Criticism of EA Criticisms: Is the real disagreement about cause prio?', + author: 'Akash', + link: 'https://forum.effectivealtruism.org/posts/qgQaWub8iR2EERq7i/criticism-of-ea-criticisms-is-the-real-disagreement-about', + }, + { + title: 'Effective Altruism Should Seek Less Criticism', + author: 'The Chaostician', + link: 'https://forum.effectivealtruism.org/posts/oQA7Z6JKHAwvWz9wk/effective-altruism-should-seek-less-criticism', + }, + { + title: + 'The great energy descent - Part 1: Can renewables replace fossil fuels?', + author: 'Corentin Biteau', + link: 'https://forum.effectivealtruism.org/posts/qG8k5pzhaDk6FhcYv/the-great-energy-descent-part-1-can-renewables-replace', + }, + { + title: 'We’re searching for meaning, not happiness (et al.)', + author: 'Joshua Clingo', + link: 'https://forum.effectivealtruism.org/posts/gmTYoTmggojK5bywA/we-re-searching-for-meaning-not-happiness-et-al', + }, + { + title: + 'Earning to give should have focused more on “entrepreneurship to give”', + author: 'freedomandutility', + link: 'https://forum.effectivealtruism.org/posts/JXDi8tL6uoKPhg4uw/earning-to-give-should-have-focused-more-on-entrepreneurship', + }, + { + title: 'Longtermism neglects anti-ageing research', + author: 'freedomandutility', + link: 'https://forum.effectivealtruism.org/posts/pbPmhWhGxsGSzwpNE/longtermism-neglects-anti-ageing-research', + }, + { + title: + 'Popular EA Authors Should Give Libraries More Copies of Their EBooks', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/AAL2zPtg7T6bjWijT/popular-ea-authors-should-give-libraries-more-copies-of', + }, + { + title: 'this one weird trick creates infinite utility', + author: 'Hmash', + link: 'https://forum.effectivealtruism.org/posts/BwuLaA97BAtLcbuuF/this-one-weird-trick-creates-infinite-utility', + }, + { + title: 'Think again: Should EA be a social movement?', + author: 'An A', + link: 'https://forum.effectivealtruism.org/posts/giznESHxt45SvuhZw/think-again-should-ea-be-a-social-movement', + }, + { + title: 'Rethinking longtermism and global development', + author: 'BrownHairedEevee', + link: 'https://forum.effectivealtruism.org/posts/GAEjbu2eHRXPHwTxF/rethinking-longtermism-and-global-development', + }, + { + title: 'Hiring: The Ignored Resource of Rejected EA Job Candidates', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/ekLyLdiCCcD6BqbJR/hiring-the-ignored-resource-of-rejected-ea-job-candidates-1', + }, + { + title: 'Suggestions for 80,000K ', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/MdvGwL4hTM2B96x4d/suggestions-for-80-000k', + }, + { + title: "We're funding task-adjusted survival (DALYs)", + author: 'brb243', + link: 'https://forum.effectivealtruism.org/posts/Gs8HS8QFBhtgAa8qo/we-re-funding-task-adjusted-survival-dalys', + }, + { + title: + 'A Need for Final Chapter Revision in Intro EA Fellowship Curricula & Other Ways to Fix Holes in The Funnel', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/AaXDbHZhJYgxLCjYZ/a-need-for-final-chapter-revision-in-intro-ea-fellowship', + }, + { + title: 'Moral Injury, Mental Health, and Obsession in EA', + author: 'ECJ', + link: 'https://forum.effectivealtruism.org/posts/jiiyBcoZXXXT7eFHm/moral-injury-mental-health-and-obsession-in-ea', + }, + { + title: 'Are we already past the precipice?', + author: 'Dem0sthenes', + link: 'https://forum.effectivealtruism.org/posts/e6prpQSojPW3jC7YD/are-we-already-past-the-precipice', + }, + { + title: 'EA has a lying problem [Link Post]', + author: 'Nathan Young', + link: 'https://forum.effectivealtruism.org/posts/8dWms5YxYwZW9xneL/ea-has-a-lying-problem-link-post', + }, + { + title: + "Senior EA 'ops' roles: if you want to undo the bottleneck, hire differently", + author: 'AnonymousThrowAway', + link: 'https://forum.effectivealtruism.org/posts/X8YMxbWNsF5FNaCFz/senior-ea-ops-roles-if-you-want-to-undo-the-bottleneck-hire', + }, + { + title: "On Deference and Yudkowsky's AI Risk Estimates", + author: 'Ben Garfinkel', + link: 'https://forum.effectivealtruism.org/posts/NBgpPaz5vYe3tH4ga/on-deference-and-yudkowsky-s-ai-risk-estimates', + }, + { + title: 'EA is too reliant on personal connections', + author: 'sawyer', + link: 'https://forum.effectivealtruism.org/posts/dvcpKuajunxdaZ6se/ea-is-too-reliant-on-personal-connections', + }, + { + title: 'Michael Nielsen\'s "Notes on effective altruism"', + author: 'Pablo', + link: 'https://forum.effectivealtruism.org/posts/JBAPssaYMMRfNqYt7/michael-nielsen-s-notes-on-effective-altruism', + }, + { + title: 'Effective altruism in the garden of ends', + author: 'tyleralterman', + link: 'https://forum.effectivealtruism.org/posts/AjxqsDmhGiW9g8ju6/effective-altruism-in-the-garden-of-ends', + }, + { + title: 'Critique of MacAskill’s “Is It Good to Make Happy People?”', + author: 'Magnus Vinding', + link: 'https://forum.effectivealtruism.org/posts/vZ4kB8gpvkfHLfz8d/critique-of-macaskill-s-is-it-good-to-make-happy-people', + }, + { + title: 'Effective altruism is no longer the right name for the movement', + author: 'ParthThaya', + link: 'https://forum.effectivealtruism.org/posts/2FB8tK9da89qksZ9E/effective-altruism-is-no-longer-the-right-name-for-the-1', + }, + { + title: 'Prioritizing x-risks may require caring about future people', + author: 'elifland', + link: 'https://forum.effectivealtruism.org/posts/rvvwCcixmEep4RSjg/prioritizing-x-risks-may-require-caring-about-future-people', + }, + { + title: 'Ways money can make things worse', + author: 'Jan_Kulveit', + link: 'https://forum.effectivealtruism.org/posts/YKEPXLQhYjm3nP7Td/ways-money-can-make-things-worse', + }, + { + title: "EA Shouldn't Try to Exercise Direct Political Power", + author: 'iamasockpuppet', + link: 'https://forum.effectivealtruism.org/posts/BgNnctp6deoGdKtbr/ea-shouldn-t-try-to-exercise-direct-political-power', + }, + { + title: 'EA on nuclear war and expertise', + author: 'bean', + link: 'https://forum.effectivealtruism.org/posts/bCB88GKeXTaxozr6y/ea-on-nuclear-war-and-expertise', + }, + { + title: 'The most important climate change uncertainty', + author: 'cwa', + link: 'https://forum.effectivealtruism.org/posts/nBN6NENeudd2uJBCQ/the-most-important-climate-change-uncertainty', + }, + { + title: "Critique of OpenPhil's macroeconomic policy advocacy", + author: 'Hauke Hillebrandt', + link: 'https://forum.effectivealtruism.org/posts/cDdcNzyizzdZD4hbR/critique-of-openphil-s-macroeconomic-policy-advocacy', + }, + { + title: + 'Methods for improving uncertainty analysis in EA cost-effectiveness models', + author: 'Froolow', + link: 'https://forum.effectivealtruism.org/posts/CuuCGzuzwD6cdu9mo/methods-for-improving-uncertainty-analysis-in-ea-cost', + }, + { + title: + 'Did OpenPhil ever publish their in-depth review of their three-year OpenAI grant?', + author: 'Markus Amalthea Magnuson', + link: 'https://forum.effectivealtruism.org/posts/sZhhW2AECqT5JikdE/did-openphil-ever-publish-their-in-depth-review-of-their', + }, + { + title: 'Go Republican, Young EA!', + author: 'ColdButtonIssues', + link: 'https://forum.effectivealtruism.org/posts/myympkZ6SuT59vuEQ/go-republican-young-ea', + }, + { + title: + 'Are too many young, highly-engaged longtermist EAs doing movement-building?', + author: 'Anonymous_EA', + link: 'https://forum.effectivealtruism.org/posts/Lfy89vKqHatQdJgDZ/are-too-many-young-highly-engaged-longtermist-eas-doing', + }, + { + title: "EA's Culture and Thinking are Severely Limiting its Impact", + author: 'Peter Elam', + link: 'https://forum.effectivealtruism.org/posts/jhCGX8Gwq44TmyPJv/ea-s-culture-and-thinking-are-severely-limiting-its-impact', + }, + { + title: 'Criticism of EA Criticism Contest', + author: 'Zvi ', + link: 'https://forum.effectivealtruism.org/posts/qjMPATBLM5p4ABcEB/criticism-of-ea-criticism-contest', + }, + { + title: + 'The EA community might be neglecting the value of influencing people', + author: 'JulianHazell', + link: 'https://forum.effectivealtruism.org/posts/3szWd8HwWccJb9z5L/the-ea-community-might-be-neglecting-the-value-of', + }, + { + title: 'Slowing down AI progress is an underexplored alignment strategy', + author: 'Michael Huang', + link: 'https://forum.effectivealtruism.org/posts/6LNvQYyNQpDQmnnux/slowing-down-ai-progress-is-an-underexplored-alignment', + }, + { + title: 'Some core assumptions of effective altruism, according to me', + author: 'peterhartree', + link: 'https://forum.effectivealtruism.org/posts/av7MiEhi983SjoXTe/some-core-assumptions-of-effective-altruism-according-to-me', + }, + { + title: 'Transcript of Twitter Discussion on EA from June 2022', + author: 'Zvi ', + link: 'https://forum.effectivealtruism.org/posts/MpJcvzHfQyFLxLZNh/transcript-of-twitter-discussion-on-ea-from-june-2022', + }, + { + title: 'EA culture is special; we should proceed with intentionality', + author: 'James Lin', + link: 'https://forum.effectivealtruism.org/posts/KuKzqhxLzaREL7KKi/ea-culture-is-special-we-should-proceed-with-intentionality', + }, + { + title: 'Four Concerns Regarding Longtermism', + author: 'Pat Andriola', + link: 'https://forum.effectivealtruism.org/posts/ESzGcWfkMtJgF2CCA/four-concerns-regarding-longtermism', + }, + { + title: 'Chesterton Fences and EA’s X-risks', + author: 'jehan', + link: 'https://forum.effectivealtruism.org/posts/j4RnXAQgyMCSLzBkW/chesterton-fences-and-ea-s-x-risks', + }, + { + title: 'Introduction to Pragmatic AI Safety [Pragmatic AI Safety #1]', + author: 'ThomasW', + link: 'https://forum.effectivealtruism.org/posts/MskKEsj8nWREoMjQK/introduction-to-pragmatic-ai-safety-pragmatic-ai-safety-1', + }, + { + title: 'EA needs to understand its “failures” better', + author: 'mariushobbhahn', + link: 'https://forum.effectivealtruism.org/posts/Nwut6L6eAGmrFSaT4/ea-needs-to-understand-its-failures-better', + }, + { + title: 'An Evaluation of Animal Charity Evaluators ', + author: 'eaanonymous1234', + link: 'https://forum.effectivealtruism.org/posts/pfSiMpkmskRB4WxYW/an-evaluation-of-animal-charity-evaluators', + }, + { + title: 'What is the overhead of grantmaking?', + author: 'MathiasKB', + link: 'https://forum.effectivealtruism.org/posts/RXm2mxvq3ReXmsHm4/what-is-the-overhead-of-grantmaking', + }, + { + title: + 'A Critique of The Precipice: Chapter 6 - The Risk Landscape [Red Team Challenge]', + author: 'Sarah Weiler', + link: 'https://forum.effectivealtruism.org/posts/faW24r7ocbcPisgCH/a-critique-of-the-precipice-chapter-6-the-risk-landscape-red', + }, + { + title: + 'Wheeling and dealing: An internal bargaining approach to moral uncertainty', + author: 'MichaelPlant', + link: 'https://forum.effectivealtruism.org/posts/kxEAkcEvyiwmjirjN/wheeling-and-dealing-an-internal-bargaining-approach-to', + }, + { + title: 'Let’s not glorify people for how they look.', + author: 'Florence', + link: 'https://forum.effectivealtruism.org/posts/8ii5SD7HBL4EdYw5K/let-s-not-glorify-people-for-how-they-look-2', + }, + { + title: 'The first AGI will be a buggy mess', + author: 'titotal', + link: 'https://forum.effectivealtruism.org/posts/pXjpZep49M6GGxFQF/the-first-agi-will-be-a-buggy-mess', + }, + { + title: '[Cause Exploration Prizes] The importance of Intercausal Impacts', + author: 'Sebastian Joy 樂百善', + link: 'https://forum.effectivealtruism.org/posts/MayveXrHbvXMBRo78/cause-exploration-prizes-the-importance-of-intercausal', + }, + { + title: 'The Windfall Clause has a remedies problem', + author: 'John Bridge', + link: 'https://forum.effectivealtruism.org/posts/wBzfLyfJFfocmdrwL/the-windfall-clause-has-a-remedies-problem', + }, + { + title: 'Future Paths for Effective Altruism', + author: 'James Broughel', + link: 'https://forum.effectivealtruism.org/posts/yzAoHcTzf3AjeGYsP/future-paths-for-effective-altruism', + }, + { + title: 'The Effective Altruism culture', + author: 'PabloAMC', + link: 'https://forum.effectivealtruism.org/posts/NkF9rAjZpkDajqDDt/the-effective-altruism-culture', + }, + { + title: + 'The Role of Individual Consumption Decisions in Animal Welfare and Climate are Analogous', + author: 'Gabriel Weil', + link: 'https://forum.effectivealtruism.org/posts/HWpwfTF5M84jo4iyo/the-role-of-individual-consumption-decisions-in-animal', + }, + { + title: 'Criticism of the main framework in AI alignment', + author: 'Michele Campolo', + link: 'https://forum.effectivealtruism.org/posts/Cs8qhNakLuLXY4GvE/criticism-of-the-main-framework-in-ai-alignment', + }, + { + title: 'Crowdsourced Criticisms: What does EA think about EA?', + author: 'Hmash', + link: 'https://forum.effectivealtruism.org/posts/jK2Qends7GnyaRhm2/crowdsourced-criticisms-what-does-ea-think-about-ea', + }, + { + title: + 'EAs should recommend cost-effective interventions in more cause areas (not just the most pressing ones) \n\n', + author: 'Amber Dawn', + link: 'https://forum.effectivealtruism.org/posts/JiEyCNoGD3WwTgDkG/eas-should-recommend-cost-effective-interventions-in-more', + }, + { + title: + 'AGI Battle Royale: Why “slow takeover” scenarios devolve into a chaotic multi-AGI fight to the death', + author: 'titotal', + link: 'https://forum.effectivealtruism.org/posts/TxrzhfRr6EXiZHv4G/agi-battle-royale-why-slow-takeover-scenarios-devolve-into-a', + }, + { + title: 'Effective means to combat autocracies', + author: 'Junius Brutus', + link: 'https://forum.effectivealtruism.org/posts/kawE7rFmp3SkzLxpx/effective-means-to-combat-autocracies', + }, + { + title: 'Editing wild animals is underexplored in What We Owe the Future', + author: 'Michael Huang', + link: 'https://forum.effectivealtruism.org/posts/cWnQMagKFqJoaGA5M/editing-wild-animals-is-underexplored-in-what-we-owe-the', + }, + { + title: 'Reasons for my negative feelings towards the AI risk discussion', + author: 'fergusq', + link: 'https://forum.effectivealtruism.org/posts/hLbWWuDr3EbeQqrmg/reasons-for-my-negative-feelings-towards-the-ai-risk', + }, + { + title: + 'We need more discussion and clarity on how university groups create value', + author: 'Oscar Galvin', + link: 'https://forum.effectivealtruism.org/posts/HNHHNCDLEsDNjNwvm/we-need-more-discussion-and-clarity-on-how-university-groups', + }, + { + title: 'What 80000 Hours gets wrong about solar geoengineering', + author: 'Gideon Futerman', + link: 'https://forum.effectivealtruism.org/posts/6dbET4f9LbJZZTuDW/what-80000-hours-gets-wrong-about-solar-geoengineering', + }, + { + title: + 'Concerns/Thoughts over international aid, longtermism and philosophical notes on speaking with Larry Temkin.', + author: 'Ben Yeoh', + link: 'https://forum.effectivealtruism.org/posts/uhaKXdkAcuXJZHSci/concerns-thoughts-over-international-aid-longtermism-and', + }, + { + title: 'On longtermism, Bayesianism, and the doomsday argument', + author: 'iporphyry', + link: 'https://forum.effectivealtruism.org/posts/f2RzSd2ukFZyNB86L/on-longtermism-bayesianism-and-the-doomsday-argument', + }, + { + title: 'Friendship is Optimal: EAGs should be online', + author: 'Emrik', + link: 'https://forum.effectivealtruism.org/posts/35nRwEyzCKDfh3dCr/friendship-is-optimal-eags-should-be-online', + }, + { + title: 'A Critique of AI Takeover Scenarios', + author: 'Fods12', + link: 'https://forum.effectivealtruism.org/posts/j7X8nQ7YvvA7Pi4BX/a-critique-of-ai-takeover-scenarios', + }, + { + title: 'The dangers of high salaries within EA organisations', + author: 'James Ozden', + link: 'https://forum.effectivealtruism.org/posts/WXD3bRDBkcBhJ5Wcr/the-dangers-of-high-salaries-within-ea-organisations', + }, + { + title: 'Low-key Longtermism', + author: 'Jonathan Rystrom', + link: 'https://forum.effectivealtruism.org/posts/BaynHfmkjrfL8DXcK/low-key-longtermism', + }, + { + title: + 'The Credibility of Apocalyptic Claims: A Critique of Techno-Futurism within Existential Risk', + author: 'Ember', + link: 'https://forum.effectivealtruism.org/posts/a2XaDeadFe6eHfDwG/the-credibility-of-apocalyptic-claims-a-critique-of-techno', + }, + { + title: + 'The Role of "Economism" in the Belief-Formation Systems of Effective Altruism', + author: 'Thomas Aitken', + link: 'https://forum.effectivealtruism.org/posts/cR4pCrATD5SSN35Sm/the-role-of-economism-in-the-belief-formation-systems-of', + }, + { + title: + 'A slightly (I think?) different slant on why EA elitism bias/top-university focus/lack of diversity is a problem', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/LCfQCvtFyAEnxCnMf/a-slightly-i-think-different-slant-on-why-ea-elitism-bias', + }, + { + title: 'Chaining the evil genie: why "outer" AI safety is probably easy', + author: 'titotal', + link: 'https://forum.effectivealtruism.org/posts/AoPR8BFrAFgGGN9iZ/chaining-the-evil-genie-why-outer-ai-safety-is-probably-easy', + }, + { + title: 'Should EA shift away (a bit) from elite universities?', + author: 'Joseph Lemien', + link: 'https://forum.effectivealtruism.org/posts/Rts8vKvbxkngPbFh7/should-ea-shift-away-a-bit-from-elite-universities', + }, + { + title: 'Aesthetics as Epistemic Humility', + author: 'Étienne Fortier-Dubois', + link: 'https://forum.effectivealtruism.org/posts/bo6Jsvmq9oiykbDrM/aesthetics-as-epistemic-humility', + }, + { + title: + "What if states don't listen? A fundamental gap in x-risk reduction strategies ", + author: 'HTC', + link: 'https://forum.effectivealtruism.org/posts/sFxtu6ZKAScDSqLrK/what-if-states-don-t-listen-a-fundamental-gap-in-x-risk', + }, + { + title: 'Eliminate or Adjust Strong Upvotes to Improve the Forum', + author: 'Afternoon Coffee', + link: 'https://forum.effectivealtruism.org/posts/2XGFdBkxa5Hm5LWZq/eliminate-or-adjust-strong-upvotes-to-improve-the-forum', + }, + { + title: 'On the Philosophical Foundations of EA', + author: 'mm6', + link: 'https://forum.effectivealtruism.org/posts/gLWmeKTe68ZHnomwy/on-the-philosophical-foundations-of-ea', + }, + { + title: + "Why I Hope (Certain) Hedonic Utilitarians Don't Control the Long-term Future", + author: 'Jared_Riggs', + link: 'https://forum.effectivealtruism.org/posts/PJKecg5ugYnyWhezC/why-i-hope-certain-hedonic-utilitarians-don-t-control-the', + }, + { + title: 'Be More Succinct', + author: 'RedTeamPseudonym', + link: 'https://forum.effectivealtruism.org/posts/eNa8GpEi5HX94CZ2n/be-more-succinct', + }, + { + title: 'Against Anthropic Shadow', + author: 'tobycrisford', + link: 'https://forum.effectivealtruism.org/posts/A47EWTS6oBKLqxBpw/against-anthropic-shadow', + }, + { + title: + 'Ideological tensions between Effective Altruism and The UK Civil Service', + author: 'KZ X', + link: 'https://forum.effectivealtruism.org/posts/J7cAFqq9g9LzSe5E3/ideological-tensions-between-effective-altruism-and-the-uk', + }, + { + title: 'We’re really bad at guessing the future', + author: 'Benj Azose', + link: 'https://forum.effectivealtruism.org/posts/DkmNPpqTJKmudBHnp/we-re-really-bad-at-guessing-the-future', + }, + { + title: + 'Effective Altruism, the Principle of Explosion and Epistemic Fragility', + author: 'Eigengender', + link: 'https://forum.effectivealtruism.org/posts/zG4pnJBCMi5t49Eya/effective-altruism-the-principle-of-explosion-and-epistemic', + }, + { + title: + 'Should EA influence governments to enact more effective interventions?', + author: 'Markus Amalthea Magnuson', + link: 'https://forum.effectivealtruism.org/posts/pGPcfjxazPGFJyYHW/should-ea-influence-governments-to-enact-more-effective', + }, + { + title: 'Should large EA nonprofits consider splitting?', + author: 'Arepo', + link: 'https://forum.effectivealtruism.org/posts/J3pZ7fY6yvypvJrJE/should-large-ea-nonprofits-consider-splitting', + }, + { + title: + "Evaluating large-scale movement building: A better way to critique Open Philanthropy's criminal justice reform", + author: 'ruthgrace', + link: 'https://forum.effectivealtruism.org/posts/7ajePuRKiCo7fA92B/evaluating-large-scale-movement-building-a-better-way-to', + }, + { + title: 'Evaluation of Longtermist Institutional Reform', + author: 'Dwarkesh Patel', + link: 'https://forum.effectivealtruism.org/posts/v4Z6phNcDsdXtzj2K/evaluation-of-longtermist-institutional-reform', + }, + { + title: 'A Quick List of Some Problems in AI Alignment As A Field', + author: 'NicholasKross', + link: 'https://forum.effectivealtruism.org/posts/JFmhYuso5s9PgrQET/a-quick-list-of-some-problems-in-ai-alignment-as-a-field', + }, + { + title: 'Fixing bad incentives in EA', + author: 'IncentivesAccount', + link: 'https://forum.effectivealtruism.org/posts/3PrTiXhhNBdGtR9qf/fixing-bad-incentives-in-ea', + }, + { + title: 'The danger of good stories', + author: 'DuncanS', + link: 'https://forum.effectivealtruism.org/posts/eZK95zxwpzNySRebC/the-danger-of-good-stories', + }, + { + title: 'A dilemma for Maximize Expected Choiceworthiness (MEC)', + author: 'Calvin_Baker', + link: 'https://forum.effectivealtruism.org/posts/Gk7NhzFy2hHFdFTYr/a-dilemma-for-maximize-expected-choiceworthiness-mec', + }, + { + title: 'Should you still use the ITN framework? [Red Teaming Contest]', + author: 'frib', + link: 'https://forum.effectivealtruism.org/posts/hjH94Ji4CrpKadoCi/should-you-still-use-the-itn-framework-red-teaming-contest', + }, + { + title: 'Proposed tweak to the longtermism pitch', + author: 'TheOtherHannah', + link: 'https://forum.effectivealtruism.org/posts/nAvpSXELT2FZMD9aA/proposed-tweak-to-the-longtermism-pitch', + }, + { + title: 'EA vs. FIRE – reconciling these two movements', + author: 'Stewed_Walrus', + link: 'https://forum.effectivealtruism.org/posts/j2ccaxmHcjiwGDs9T/ea-vs-fire-reconciling-these-two-movements', + }, + { + title: 'Should young EAs really focus on career capital?', + author: 'Michael B.', + link: 'https://forum.effectivealtruism.org/posts/RYBFyDWAYZL4YCkW2/should-young-eas-really-focus-on-career-capital', + }, + { + title: 'Prediction Markets are Somewhat Overrated Within EA', + author: 'Francis', + link: 'https://forum.effectivealtruism.org/posts/4LsrNczpF6mfrHP4M/prediction-markets-are-somewhat-overrated-within-ea', + }, + { + title: 'Capitalism, power and epistemology: a critique of EA', + author: 'Matthew_Doran', + link: 'https://forum.effectivealtruism.org/posts/xWFhD6uQuZehrDKeY/capitalism-power-and-epistemology-a-critique-of-ea', + }, + { + title: 'EA Worries and Criticism ', + author: 'Connor Tabarrok', + link: 'https://forum.effectivealtruism.org/posts/A5tRZC2mduJfpMhud/ea-worries-and-criticism', + }, + { + title: 'EA criticism contest: Why I am not an effective altruist', + author: 'ErikHoel', + link: 'https://forum.effectivealtruism.org/posts/PZ6pEaNkzAg62ze69/ea-criticism-contest-why-i-am-not-an-effective-altruist', + }, + { + title: 'Nuclear Fine-Tuning: How Many Worlds Have Been Destroyed?', + author: 'Ember', + link: 'https://forum.effectivealtruism.org/posts/Gg2YsjGe3oahw2kxE/nuclear-fine-tuning-how-many-worlds-have-been-destroyed', + }, + { + title: 'An epistemic critique of longtermism', + author: 'Nathan_Barnard', + link: 'https://forum.effectivealtruism.org/posts/2455tgtiBsm5KXBfv/an-epistemic-critique-of-longtermism', + }, + { + title: 'Red Team: Write More.', + author: 'Weaver', + link: 'https://forum.effectivealtruism.org/posts/5A5cMh223b9s4uHwE/red-team-write-more', + }, + { + title: 'End-To-End Encryption For EA', + author: 'Talking Tree', + link: 'https://forum.effectivealtruism.org/posts/tekdQKdfFe3YJTwML/end-to-end-encryption-for-ea', + }, + { + title: 'Effective Altruism is Unkind', + author: 'Oliver Scott Curry', + link: 'https://forum.effectivealtruism.org/posts/cC6tGHctzrMmEAH8j/effective-altruism-is-unkind', + }, + { + title: 'Towards a more ecumenical EA movement ', + author: 'Locke', + link: 'https://forum.effectivealtruism.org/posts/NR2Y2B8Y4Wxn8pAS8/towards-a-more-ecumenical-ea-movement', + }, + { + title: + 'Effective altruism is similar to the AI alignment problem and suffers from the same difficulties [Criticism and Red Teaming Contest entry]', + author: 'turchin', + link: 'https://forum.effectivealtruism.org/posts/g8fn7oyvki4psJeYR/effective-altruism-is-similar-to-the-ai-alignment-problem', + }, + { + title: + 'The great energy descent - Part 2: Limits to growth and why we probably won’t reach the stars', + author: 'Corentin Biteau', + link: 'https://forum.effectivealtruism.org/posts/8sW4h368DsoooHBNP/the-great-energy-descent-part-2-limits-to-growth-and-why-we', + }, + { + title: 'What a Large and Welcoming EA Could Accomplish', + author: 'Peter Elam', + link: 'https://forum.effectivealtruism.org/posts/K24widt85ZbGqzZKN/what-a-large-and-welcoming-ea-could-accomplish', + }, + { + title: 'Should we call ourselves effective altruists?', + author: 'Sam_Coggins', + link: 'https://forum.effectivealtruism.org/posts/YyDRSARnXz8r5dgca/should-we-call-ourselves-effective-altruists', + }, + { + title: 'Compounding assumptions and what it mean to be altruistic', + author: 'Badger', + link: 'https://forum.effectivealtruism.org/posts/4RGuqDxui2xWkXnda/compounding-assumptions-and-what-it-mean-to-be-altruistic', + }, + { + title: + 'The great energy descent - Post 3: What we can do, what we can’t do', + author: 'Corentin Biteau', + link: 'https://forum.effectivealtruism.org/posts/9zTLPy3zqJ7YfS7kn/the-great-energy-descent-post-3-what-we-can-do-what-we-can-t', + }, + { + title: 'Enantiodromia', + author: 'ChristianKleineidam', + link: 'https://forum.effectivealtruism.org/posts/b4ASDM434qh3rxLki/enantiodromia', + }, + { + title: 'Deontology, the Paralysis Argument and altruistic longtermism', + author: "William D'Alessandro", + link: 'https://forum.effectivealtruism.org/posts/DKe5eQhJoLNMWgaQv/deontology-the-paralysis-argument-and-altruistic-longtermism', + }, + { + title: 'Path dependence and its impact on long-term outcomes', + author: 'Archanaa', + link: 'https://forum.effectivealtruism.org/posts/jadS8deYknecGSebp/path-dependence-and-its-impact-on-long-term-outcomes', + }, + { + title: 'Histories of Value Lock-in and Ideology Critique', + author: 'clem', + link: 'https://forum.effectivealtruism.org/posts/poWd3CcGeQPas3Zbo/histories-of-value-lock-in-and-ideology-critique', + }, + { + title: 'A Case Against Strong Longtermism', + author: 'A. Wolff', + link: 'https://forum.effectivealtruism.org/posts/LADQ6dTGsQ2BBMrBv/a-case-against-strong-longtermism-1', + }, + { + title: 'The totalitarian implications of Effective Altruism', + author: 'Ed_Talks', + link: 'https://forum.effectivealtruism.org/posts/guyuidDdxNNxFegbJ/the-totalitarian-implications-of-effective-altruism-1', + }, + { + title: 'Forecasting Through Fiction', + author: 'Yitz', + link: 'https://forum.effectivealtruism.org/posts/DhJhtxMX6SdYAsWiY/forecasting-through-fiction', + }, + { + title: 'EA Undervalues Unseen Data', + author: 'tcelferact', + link: 'https://forum.effectivealtruism.org/posts/MpYPCq9dW8wovYpRY/ea-undervalues-unseen-data', + }, + { + title: 'The Happiness Maximizer:\nWhy EA is an x-risk', + author: 'Obasi Shaw', + link: 'https://forum.effectivealtruism.org/posts/ByHc6jdXF9skwevYf/the-happiness-maximizer-why-ea-is-an-x-risk', + }, + { + title: 'EA is a fight against Knightian uncertainty', + author: 'Rohit (Strange Loop)', + link: 'https://forum.effectivealtruism.org/posts/vic7EdWCGKd4fYtYd/ea-is-a-fight-against-knightian-uncertainty', + }, + { + title: + 'The Malthusian Gradient: Why some third-world interventions may be doing more harm than good', + author: 'JoePater', + link: 'https://forum.effectivealtruism.org/posts/juFzy7CWhu6ApQMAA/the-malthusian-gradient-why-some-third-world-interventions', + }, + { + title: 'The Hidden Impossibilities Of Being An Effective Altruist.', + author: 'Refined Insights ', + link: 'https://forum.effectivealtruism.org/posts/fsxEDLM2oPzSREM4G/the-hidden-impossibilities-of-being-an-effective-altruist', + }, + { + title: 'A critique of strong longtermism', + author: 'Pablo Rosado', + link: 'https://forum.effectivealtruism.org/posts/ryJys2fAz7J4vAwFC/a-critique-of-strong-longtermism', + }, + { + title: 'Making EA More Effective', + author: 'Peter Kelly', + link: 'https://forum.effectivealtruism.org/posts/Ag6bsmqxwqWTSjcHX/making-ea-more-effective', + }, + { + title: "A part of the system's apology", + author: 'Niv Cohen', + link: 'https://forum.effectivealtruism.org/posts/vNTD4mBAzfyZFJkfW/a-part-of-the-system-s-apology', + }, + { + title: 'The Wages of North-Atlantic Bias', + author: 'Sach Wry', + link: 'https://forum.effectivealtruism.org/posts/FA4tC72qAB5k37uFC/the-wages-of-north-atlantic-bias', + }, + { + title: 'Keeping it Real', + author: 'calumdavey', + link: 'https://forum.effectivealtruism.org/posts/ewEiyspZeqjZC7Yh7/keeping-it-real', + }, + { + title: 'Hobbit Manifesto', + author: 'Clay Cube', + link: 'https://forum.effectivealtruism.org/posts/3caZ7LhMsvsS7kRrz/hobbit-manifesto', + }, + { + title: + "How avoiding drastic career changes could support EA's epistemic health and long-term efficacy.", + author: 'nat goldthwaite', + link: 'https://forum.effectivealtruism.org/posts/3txJdk6ZcNmcRBjWP/how-avoiding-drastic-career-changes-could-support-ea-s', + }, + { + title: + "Present-day good intentions aren't sufficient to make the longterm future good in expectation", + author: 'trurl', + link: 'https://forum.effectivealtruism.org/posts/FBNk5ibcWwYcavkh4/present-day-good-intentions-aren-t-sufficient-to-make-the', + }, + { + title: + 'A podcast episode exploring critiques of effective altruism (with Michael Nielsen and Ajeya Cotra)', + author: 'spencerg', + link: 'https://forum.effectivealtruism.org/posts/2dHk3zBmmnNTefjWB/a-podcast-episode-exploring-critiques-of-effective-altruism', + }, + { + title: 'Follow-up: Crowdsourced Criticisms', + author: 'Hmash', + link: 'https://forum.effectivealtruism.org/posts/kXCsTDB5s7QRnWS8f/follow-up-crowdsourced-criticisms', + }, + { + title: 'Why the EA aversion to local altruistic action?', + author: 'Locke', + link: 'https://forum.effectivealtruism.org/posts/LnuuN7zuBSZvEo845/why-the-ea-aversion-to-local-altruistic-action', + }, + { + title: 'Effective Altruists and Religion: A Proposal for Experimentation', + author: 'Kbrown', + link: 'https://forum.effectivealtruism.org/posts/YA6fCNwB2c5cydrtG/effective-altruists-and-religion-a-proposal-for', + }, + { + title: + 'On the institutional critique of effective altruism: a response (mainly) to Brian Berkey ', + author: 'zzz1407', + link: 'https://forum.effectivealtruism.org/posts/GgNgnzjqceDghhozf/on-the-institutional-critique-of-effective-altruism-a-1', + }, + { + title: + 'We Can’t Do Long Term Utilitarian Calculations Until We Know if AIs Can Be Conscious or Not', + author: 'Mike20731', + link: 'https://forum.effectivealtruism.org/posts/Zsz3BYQTJjJdZd4DR/we-can-t-do-long-term-utilitarian-calculations-until-we-know', + }, + { + title: 'The ordinal utility argument against effective altruism ', + author: 'Barracuda', + link: 'https://forum.effectivealtruism.org/posts/rNYCcRLzkQtQEBnLa/the-ordinal-utility-argument-against-effective-altruism', + }, + { + title: + 'Reciprocity & the causes of diminishing returns: cause exploration submission', + link: 'https://forum.effectivealtruism.org/posts/x9towRLtvYidkXugk/reciprocity-and-the-causes-of-diminishing-returns-cause', + }, + { + title: + 'Altruism is systems change, so why isn’t EA? Constructive criticism.', + link: 'https://forum.effectivealtruism.org/posts/xZrvbwhSLmsGmHHSD/altruism-is-systems-change-so-why-isn-t-ea-constructive', + }, + { + title: 'The reasonableness of special concerns', + author: 'jwt', + link: 'https://forum.effectivealtruism.org/posts/CFGYLDgvsYQhsyZ42/the-reasonableness-of-special-concerns', + }, + { + title: 'The EA community should utilize the concept of beliefs more often', + author: 'Noah Scales', + link: 'https://forum.effectivealtruism.org/posts/9SKqeNSvAKozeMvGq/the-ea-community-should-utilize-the-concept-of-beliefs-more', + }, + { + title: 'Why bother doing the most good?', + author: 'Dov', + link: 'https://forum.effectivealtruism.org/posts/ZPcKeZbcC5SgLGLwg/why-bother-doing-the-most-good', + }, + { + title: + 'Framing EA as tending towards longtermism might be diminishing its potential impact', + author: 'Mm', + link: 'https://forum.effectivealtruism.org/posts/guGteQYvwcuDAECPA/framing-ea-as-tending-towards-longtermism-might-be', + }, + { + title: 'Bernard Williams: Ethics and the limits of impartiality', + author: 'peterhartree', + link: 'https://forum.effectivealtruism.org/posts/G6EWTrArPDf74sr3S/bernard-williams-ethics-and-the-limits-of-impartiality', + }, + { + title: 'Love and AI: Relational Brain/Mind Dynamics in AI Development', + author: 'JeffreyK', + link: 'https://forum.effectivealtruism.org/posts/MdfLn33GpNWGN7CSE/love-and-ai-relational-brain-mind-dynamics-in-ai-development', + }, + { + title: 'When 2/3rds of the world goes against you', + author: 'JeffreyK', + link: 'https://forum.effectivealtruism.org/posts/6va2EfHkQ3bTmdDyn/when-2-3rds-of-the-world-goes-against-you', + }, + { + title: 'My views on EA --> attempt to a constructive criticism', + author: 'Jin Jo', + link: 'https://forum.effectivealtruism.org/posts/LcDcqX6KWGHm3tSgr/my-views-on-ea-greater-than-attempt-to-a-constructive', + }, + { + title: 'Empirical critique of EA from another direction', + author: 'tonz', + link: 'https://forum.effectivealtruism.org/posts/nsqhmwmwZmWvFA2wb/empirical-critique-of-ea-from-another-direction', + }, + { + title: 'Critique: Cost-Benefit of Weirdness', + author: 'Mike Elias', + link: 'https://forum.effectivealtruism.org/posts/kw8ZmziAwcqPW2jt6/critique-cost-benefit-of-weirdness', + }, + { + title: 'Hits- or misses-based giving', + author: 'brb243', + link: 'https://forum.effectivealtruism.org/posts/XzawnaT4jyqpkEihz/hits-or-misses-based-giving', + }, + { + title: 'Mind your step', + author: 'Talsome', + link: 'https://forum.effectivealtruism.org/posts/rGNaz4GtWCzPbCWCB/mind-your-step', + }, + { + title: 'Against Impartial Altruism', + author: 'Sam K', + link: 'https://forum.effectivealtruism.org/posts/f5ZxK2k9gyZthHGND/against-impartial-altruism', + }, + { + title: 'Criticism of EA and longtermism', + author: 'St. Ignorant', + link: 'https://forum.effectivealtruism.org/posts/DuG8rBSAErSmSN7uE/criticism-of-ea-and-longtermism', + }, + { + title: + 'Against Longtermism: \nI welcome our robot overlords, and you should too!', + author: 'MattBall', + link: 'https://forum.effectivealtruism.org/posts/Cuu4Jjmp7QqL4a5Ls/against-longtermism-i-welcome-our-robot-overlords-and-you', + }, + { + title: 'Effective Altruism Criticisms', + author: 'Gavin Palmer (heroLFG.com)', + link: 'https://forum.effectivealtruism.org/posts/mMaAcvNLQPC3aTqB6/effective-altruism-criticisms', + }, + { + title: '"Of Human Bondage" and Morality', + author: 'Casaubon', + link: 'https://forum.effectivealtruism.org/posts/ZJnKCToBojYqqQphb/of-human-bondage-and-morality', + }, + { + title: + 'Portfolios, Locality, and Career - Three Critiques of Effective Altruism', + author: 'Philip Apps', + link: 'https://forum.effectivealtruism.org/posts/AoL2h2ZqTSNevdtRM/portfolios-locality-and-career-three-critiques-of-effective', + }, + { + title: 'Effective altruism in a non-ideal world', + author: 'Eric Kramer', + link: 'https://forum.effectivealtruism.org/posts/2nApcLJsZeABu38uW/effective-altruism-in-a-non-ideal-world', + }, + { + title: 'The future of humanity', + author: 'Dem0sthenes', + link: 'https://forum.effectivealtruism.org/posts/nLyG65eQepKKeGbrg/the-future-of-humanity', + }, + { + title: + 'Investigating Ideology: want to earn money, help EA and/or me? Then check this out; it may be a mighty neglected cause ', + author: 'Dov', + link: 'https://forum.effectivealtruism.org/posts/twaKWNjAc4KEz3kMq/investigating-ideology-want-to-earn-money-help-ea-and-or-me', + }, + { + title: 'Accepting the Inevitability of Ambitious Egoism (”AE”)', + author: 'Dem0sthenes', + link: 'https://forum.effectivealtruism.org/posts/bQsxsaEcvxzEML9ZW/accepting-the-inevitability-of-ambitious-egoism-ae', + }, + { + title: 'Book Review: What We Owe The Future (Erik Hoel)', + author: 'ErikHoel', + link: 'https://forum.effectivealtruism.org/posts/AyPTZLTwm5hN2Kfcb/book-review-what-we-owe-the-future-erik-hoel', + }, + { + title: + 'Effective Altruism Goes Political: Normative Conflicts and Practical Judgment', + author: 'Michael Haiden', + link: 'https://forum.effectivealtruism.org/posts/aisE9yhZHuiM9Cdn7/effective-altruism-goes-political-normative-conflicts-and-1', + }, + { + title: 'Values lock-in is already happening (without AGI)', + link: 'https://forum.effectivealtruism.org/posts/ogwD28mzJy8dkwtmc/values-lock-in-is-already-happening-without-agi', + }, + { + title: 'EA Should Rename Itself', + author: 'Name Rectifier', + link: 'https://forum.effectivealtruism.org/posts/swkxLtjG9z7RY7i9x/ea-should-rename-itself', + }, + { + title: 'What we are for? Community, Correction and Scale [wip]', + author: 'Nathan Young', + link: 'https://forum.effectivealtruism.org/posts/QCv5GNcQFeH34iN2w/what-we-are-for-community-correction-and-scale-wip', + }, + { + title: '“One should love one’s neighbor more than oneself.”', + author: 'Barracuda', + link: 'https://forum.effectivealtruism.org/posts/bxbu8v83gw3MDzCBX/one-should-love-one-s-neighbor-more-than-oneself', + }, + { + title: 'Run For President', + author: 'Brian Moore', + link: 'https://forum.effectivealtruism.org/posts/ZniCnE8XhCMLeGHj8/run-for-president', + }, + { + title: 'Effective Altruism Risks Perpetuating a Harmful Worldview', + author: 'Theo Cox', + link: 'https://forum.effectivealtruism.org/posts/QRaf9iWvGbfKgWBvY/effective-altruism-risks-perpetuating-a-harmful-worldview', + }, +] diff --git a/functions/src/scripts/contest/scrape-ea.ts b/functions/src/scripts/contest/scrape-ea.ts new file mode 100644 index 00000000..c22f4ac7 --- /dev/null +++ b/functions/src/scripts/contest/scrape-ea.ts @@ -0,0 +1,55 @@ +// Run with `npx ts-node src/scripts/contest/scrape-ea.ts` +import * as fs from 'fs' +import * as puppeteer from 'puppeteer' + +export function scrapeEA(contestLink: string, fileName: string) { + ;(async () => { + const browser = await puppeteer.launch({ headless: true }) + const page = await browser.newPage() + await page.goto(contestLink) + + let loadMoreButton = await page.$('.LoadMore-root') + + while (loadMoreButton) { + await loadMoreButton.click() + await page.waitForNetworkIdle() + loadMoreButton = await page.$('.LoadMore-root') + } + + /* Run javascript inside the page */ + const data = await page.evaluate(() => { + const list = [] + const items = document.querySelectorAll('.PostsItem2-root') + + for (const item of items) { + const link = + 'https://forum.effectivealtruism.org' + + item?.querySelector('a')?.getAttribute('href') + + // Replace '&' with '&' + const clean = (str: string | undefined) => str?.replace(/&/g, '&') + + list.push({ + title: clean(item?.querySelector('a>span>span')?.innerHTML), + author: item?.querySelector('a.UsersNameDisplay-userName')?.innerHTML, + link: link, + }) + } + + return list + }) + + fs.writeFileSync( + `./src/scripts/contest/${fileName}.ts`, + `export const data = ${JSON.stringify(data, null, 2)}` + ) + + console.log(data) + await browser.close() + })() +} + +scrapeEA( + 'https://forum.effectivealtruism.org/topics/criticism-and-red-teaming-contest', + 'criticism-and-red-teaming' +) diff --git a/functions/src/scripts/convert-tag-to-group.ts b/functions/src/scripts/convert-tag-to-group.ts index 3240357e..b2e4c4d8 100644 --- a/functions/src/scripts/convert-tag-to-group.ts +++ b/functions/src/scripts/convert-tag-to-group.ts @@ -41,6 +41,7 @@ const createGroup = async ( anyoneCanJoin: true, totalContracts: contracts.length, totalMembers: 1, + postIds: [], } await groupRef.create(group) // create a GroupMemberDoc for the creator diff --git a/functions/src/serve.ts b/functions/src/serve.ts index 6d062d40..99ac6281 100644 --- a/functions/src/serve.ts +++ b/functions/src/serve.ts @@ -28,6 +28,7 @@ import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' import { createpost } from './create-post' import { savetwitchcredentials } from './save-twitch-credentials' +import { testscheduledfunction } from './test-scheduled-function' type Middleware = (req: Request, res: Response, next: NextFunction) => void const app = express() @@ -69,6 +70,7 @@ addJsonEndpointRoute('/getcurrentuser', getcurrentuser) addJsonEndpointRoute('/savetwitchcredentials', savetwitchcredentials) addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) addEndpointRoute('/createpost', createpost) +addEndpointRoute('/testscheduledfunction', testscheduledfunction) app.listen(PORT) console.log(`Serving functions on port ${PORT}.`) diff --git a/functions/src/test-scheduled-function.ts b/functions/src/test-scheduled-function.ts new file mode 100644 index 00000000..41aa9fe9 --- /dev/null +++ b/functions/src/test-scheduled-function.ts @@ -0,0 +1,17 @@ +import { APIError, newEndpoint } from './api' +import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails' +import { isProd } from './utils' + +// Function for testing scheduled functions locally +export const testscheduledfunction = newEndpoint( + { method: 'GET', memory: '4GiB' }, + async (_req) => { + if (isProd()) + throw new APIError(400, 'This function is only available in dev mode') + + // Replace your function here + await sendPortfolioUpdateEmailsToAllUsers() + + return { success: true } + } +) diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 6bb8349a..efc22e53 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -170,3 +170,7 @@ export const chargeUser = ( export const getContractPath = (contract: Contract) => { return `/${contract.creatorUsername}/${contract.slug}` } + +export function contractUrl(contract: Contract) { + return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}` +} diff --git a/functions/src/weekly-markets-emails.ts b/functions/src/weekly-markets-emails.ts index bec5949c..7c6f21a4 100644 --- a/functions/src/weekly-markets-emails.ts +++ b/functions/src/weekly-markets-emails.ts @@ -46,12 +46,14 @@ async function sendTrendingMarketsEmailsToAllUsers() { ? await getAllPrivateUsers() : filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')]) // get all users that haven't unsubscribed from weekly emails - const privateUsersToSendEmailsTo = privateUsers.filter((user) => { - return ( - user.notificationPreferences.trending_markets.includes('email') && - !user.weeklyTrendingEmailSent - ) - }) + const privateUsersToSendEmailsTo = privateUsers + .filter((user) => { + return ( + user.notificationPreferences.trending_markets.includes('email') && + !user.weeklyTrendingEmailSent + ) + }) + .slice(150) // Send the emails out in batches log( 'Sending weekly trending emails to', privateUsersToSendEmailsTo.length, @@ -74,6 +76,7 @@ async function sendTrendingMarketsEmailsToAllUsers() { trendingContracts.map((c) => c.question).join('\n ') ) + // TODO: convert to Promise.all for (const privateUser of privateUsersToSendEmailsTo) { if (!privateUser.email) { log(`No email for ${privateUser.username}`) @@ -84,6 +87,9 @@ async function sendTrendingMarketsEmailsToAllUsers() { }) if (contractsAvailableToSend.length < numContractsToSend) { log('not enough new, unbet-on contracts to send to user', privateUser.id) + await firestore.collection('private-users').doc(privateUser.id).update({ + weeklyTrendingEmailSent: true, + }) continue } // choose random subset of contracts to send to user diff --git a/functions/src/weekly-portfolio-emails.ts b/functions/src/weekly-portfolio-emails.ts new file mode 100644 index 00000000..dcbb68dd --- /dev/null +++ b/functions/src/weekly-portfolio-emails.ts @@ -0,0 +1,280 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { Contract, CPMMContract } from '../../common/contract' +import { + getAllPrivateUsers, + getPrivateUser, + getUser, + getValue, + getValues, + isProd, + log, +} from './utils' +import { filterDefined } from '../../common/util/array' +import { DAY_MS } from '../../common/util/time' +import { partition, sortBy, sum, uniq } from 'lodash' +import { Bet } from '../../common/bet' +import { computeInvestmentValueCustomProb } from '../../common/calculate-metrics' +import { sendWeeklyPortfolioUpdateEmail } from './emails' +import { contractUrl } from './utils' +import { Txn } from '../../common/txn' +import { formatMoney } from '../../common/util/format' + +// TODO: reset weeklyPortfolioUpdateEmailSent to false for all users at the start of each week +export const weeklyPortfolioUpdateEmails = functions + .runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' }) + // every minute on Friday for an hour at 12pm PT (UTC -07:00) + .pubsub.schedule('* 19 * * 5') + .timeZone('Etc/UTC') + .onRun(async () => { + await sendPortfolioUpdateEmailsToAllUsers() + }) + +const firestore = admin.firestore() + +export async function sendPortfolioUpdateEmailsToAllUsers() { + const privateUsers = isProd() + ? // ian & stephen's ids + // ? filterDefined([ + // await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'), + // await getPrivateUser('tlmGNz9kjXc2EteizMORes4qvWl2'), + // ]) + await getAllPrivateUsers() + : filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')]) + // get all users that haven't unsubscribed from weekly emails + const privateUsersToSendEmailsTo = privateUsers + .filter((user) => { + return isProd() + ? user.notificationPreferences.profit_loss_updates.includes('email') && + !user.weeklyPortfolioUpdateEmailSent + : true + }) + // Send emails in batches + .slice(0, 200) + log( + 'Sending weekly portfolio emails to', + privateUsersToSendEmailsTo.length, + 'users' + ) + + const usersBets: { [userId: string]: Bet[] } = {} + // get all bets made by each user + await Promise.all( + privateUsersToSendEmailsTo.map(async (user) => { + return getValues( + firestore.collectionGroup('bets').where('userId', '==', user.id) + ).then((bets) => { + usersBets[user.id] = bets + }) + }) + ) + + const usersToContractsCreated: { [userId: string]: Contract[] } = {} + // Get all contracts created by each user + await Promise.all( + privateUsersToSendEmailsTo.map(async (user) => { + return getValues( + firestore + .collection('contracts') + .where('creatorId', '==', user.id) + .where('createdTime', '>', Date.now() - 7 * DAY_MS) + ).then((contracts) => { + usersToContractsCreated[user.id] = contracts + }) + }) + ) + + // Get all txns the users received over the past week + const usersToTxnsReceived: { [userId: string]: Txn[] } = {} + await Promise.all( + privateUsersToSendEmailsTo.map(async (user) => { + return getValues( + firestore + .collection(`txns`) + .where('toId', '==', user.id) + .where('createdTime', '>', Date.now() - 7 * DAY_MS) + ).then((txn) => { + usersToTxnsReceived[user.id] = txn + }) + }) + ) + + // Get a flat map of all the bets that users made to get the contracts they bet on + const contractsUsersBetOn = filterDefined( + await Promise.all( + uniq( + Object.values(usersBets).flatMap((bets) => + bets.map((bet) => bet.contractId) + ) + ).map((contractId) => + getValue(firestore.collection('contracts').doc(contractId)) + ) + ) + ) + log('Found', contractsUsersBetOn.length, 'contracts') + let count = 0 + await Promise.all( + privateUsersToSendEmailsTo.map(async (privateUser) => { + const user = await getUser(privateUser.id) + if (!user) return + const userBets = usersBets[privateUser.id] as Bet[] + const contractsUserBetOn = contractsUsersBetOn.filter((contract) => + userBets.some((bet) => bet.contractId === contract.id) + ) + const contractsBetOnInLastWeek = uniq( + userBets + .filter((bet) => bet.createdTime > Date.now() - 7 * DAY_MS) + .map((bet) => bet.contractId) + ) + const totalTips = sum( + usersToTxnsReceived[privateUser.id] + .filter((txn) => txn.category === 'TIP') + .map((txn) => txn.amount) + ) + const greenBg = 'rgba(0,160,0,0.2)' + const redBg = 'rgba(160,0,0,0.2)' + const clearBg = 'rgba(255,255,255,0)' + const roundedProfit = + Math.round(user.profitCached.weekly) === 0 + ? 0 + : Math.floor(user.profitCached.weekly) + const performanceData = { + profit: formatMoney(user.profitCached.weekly), + profit_style: `background-color: ${ + roundedProfit > 0 ? greenBg : roundedProfit === 0 ? clearBg : redBg + }`, + markets_created: + usersToContractsCreated[privateUser.id].length.toString(), + tips_received: formatMoney(totalTips), + unique_bettors: usersToTxnsReceived[privateUser.id] + .filter((txn) => txn.category === 'UNIQUE_BETTOR_BONUS') + .length.toString(), + markets_traded: contractsBetOnInLastWeek.length.toString(), + prediction_streak: + (user.currentBettingStreak?.toString() ?? '0') + ' days', + // More options: bonuses, tips given, + } as OverallPerformanceData + + const investmentValueDifferences = sortBy( + filterDefined( + contractsUserBetOn.map((contract) => { + const cpmmContract = contract as CPMMContract + if (cpmmContract === undefined || cpmmContract.prob === undefined) + return + const bets = userBets.filter( + (bet) => bet.contractId === contract.id + ) + + const marketProbabilityAWeekAgo = + cpmmContract.prob - cpmmContract.probChanges.week + const currentMarketProbability = cpmmContract.resolutionProbability + ? cpmmContract.resolutionProbability + : cpmmContract.prob + const betsValueAWeekAgo = computeInvestmentValueCustomProb( + bets.filter((b) => b.createdTime < Date.now() - 7 * DAY_MS), + contract, + marketProbabilityAWeekAgo + ) + const currentBetsValue = computeInvestmentValueCustomProb( + bets, + contract, + currentMarketProbability + ) + const marketChange = + currentMarketProbability - marketProbabilityAWeekAgo + return { + currentValue: currentBetsValue, + pastValue: betsValueAWeekAgo, + difference: currentBetsValue - betsValueAWeekAgo, + contractSlug: contract.slug, + marketProbAWeekAgo: marketProbabilityAWeekAgo, + questionTitle: contract.question, + questionUrl: contractUrl(contract), + questionProb: cpmmContract.resolution + ? cpmmContract.resolution + : Math.round(cpmmContract.prob * 100) + '%', + questionChange: + (marketChange > 0 ? '+' : '') + + Math.round(marketChange * 100) + + '%', + questionChangeStyle: `color: ${ + currentMarketProbability > marketProbabilityAWeekAgo + ? 'rgba(0,160,0,1)' + : '#a80000' + };`, + } as PerContractInvestmentsData + }) + ), + (differences) => Math.abs(differences.difference) + ).reverse() + + log( + 'Found', + investmentValueDifferences.length, + 'investment differences for user', + privateUser.id + ) + + const [winningInvestments, losingInvestments] = partition( + investmentValueDifferences.filter( + (diff) => + diff.pastValue > 0.01 && + Math.abs(diff.difference / diff.pastValue) > 0.01 // difference is greater than 1% + ), + (investmentsData: PerContractInvestmentsData) => { + return investmentsData.difference > 0 + } + ) + // pick 3 winning investments and 3 losing investments + const topInvestments = winningInvestments.slice(0, 2) + const worstInvestments = losingInvestments.slice(0, 2) + // if no bets in the last week ANd no market movers AND no markets created, don't send email + if ( + contractsBetOnInLastWeek.length === 0 && + topInvestments.length === 0 && + worstInvestments.length === 0 && + usersToContractsCreated[privateUser.id].length === 0 + ) { + log('No bets in last week, no market movers, no markets created') + await firestore.collection('private-users').doc(privateUser.id).update({ + weeklyPortfolioUpdateEmailSent: true, + }) + return + } + await sendWeeklyPortfolioUpdateEmail( + user, + privateUser, + topInvestments.concat(worstInvestments) as PerContractInvestmentsData[], + performanceData + ) + await firestore.collection('private-users').doc(privateUser.id).update({ + weeklyPortfolioUpdateEmailSent: true, + }) + log('Sent weekly portfolio update email to', privateUser.email) + count++ + log('sent out emails to user count:', count) + }) + ) +} + +export type PerContractInvestmentsData = { + questionTitle: string + questionUrl: string + questionProb: string + questionChange: string + questionChangeStyle: string + currentValue: number + pastValue: number + difference: number +} + +export type OverallPerformanceData = { + profit: string + prediction_streak: string + markets_traded: string + profit_style: string + tips_received: string + markets_created: string + unique_bettors: string +} diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 2ad745a8..fbb49677 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -6,6 +6,7 @@ import { Col } from './layout/col' import { SiteLink } from './site-link' import { ENV_CONFIG } from 'common/envs/constants' import { useWindowSize } from 'web/hooks/use-window-size' +import { Row } from './layout/row' export function AmountInput(props: { amount: number | undefined @@ -34,46 +35,53 @@ export function AmountInput(props: { const isInvalid = !str || isNaN(amount) onChange(isInvalid ? undefined : amount) } + const { width } = useWindowSize() const isMobile = (width ?? 0) < 768 - return ( - - - {error && ( -
- {error === 'Insufficient balance' ? ( - <> - Not enough funds. - - Buy more? - - - ) : ( - error - )} -
- )} - + return ( + <> + + + + {error && ( +
+ {error === 'Insufficient balance' ? ( + <> + Not enough funds. + + Buy more? + + + ) : ( + error + )} +
+ )} + + ) } @@ -136,27 +144,29 @@ export function BuyAmountInput(props: { return ( <> - - {showSlider && ( - onAmountChange(parseRaw(parseInt(e.target.value)))} - className="range range-lg only-thumb z-40 mb-2 xl:hidden" - step="5" + + - )} + {showSlider && ( + onAmountChange(parseRaw(parseInt(e.target.value)))} + className="range range-lg only-thumb z-40 my-auto align-middle xl:hidden" + step="5" + /> + )} + ) } diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 3339ded5..85f61034 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -182,17 +182,17 @@ export function AnswerBetPanel(props: { - {user ? ( ) : ( diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 51cf5799..a1cef4c3 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -38,13 +38,26 @@ export function AnswersPanel(props: { const answers = (useAnswers(contract.id) ?? contract.answers).filter( (a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE' ) - const [winningAnswers, notWinningAnswers] = partition( - answers, - (a) => a.id === resolution || (resolutions && resolutions[a.id]) + const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1) + + const [winningAnswers, losingAnswers] = partition( + answers.filter((a) => (showAllAnswers ? true : totalBets[a.id] > 0)), + (answer) => + answer.id === resolution || (resolutions && resolutions[answer.id]) ) - const [visibleAnswers, invisibleAnswers] = partition( - sortBy(notWinningAnswers, (a) => -getOutcomeProbability(contract, a.id)), - (a) => showAllAnswers || totalBets[a.id] > 0 + const sortedAnswers = [ + ...sortBy(winningAnswers, (answer) => + resolutions ? -1 * resolutions[answer.id] : 0 + ), + ...sortBy( + resolution ? [] : losingAnswers, + (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) + ), + ] + + const answerItems = sortBy( + losingAnswers.length > 0 ? losingAnswers : sortedAnswers, + (answer) => -getOutcomeProbability(contract, answer.id) ) const user = useUser() @@ -94,13 +107,13 @@ export function AnswersPanel(props: { return ( {(resolveOption || resolution) && - sortBy(winningAnswers, (a) => -(resolutions?.[a.id] ?? 0)).map((a) => ( + sortedAnswers.map((answer) => ( - {visibleAnswers.map((a) => ( - + {answerItems.map((item) => ( + ))} - {invisibleAnswers.length > 0 && !showAllAnswers && ( + {hasZeroBetAnswers && !showAllAnswers && ( + + + <LimitOrderPanel + hidden={!seeLimit} + contract={contract} + user={user} + unfilledBets={unfilledBets} + /> + </Modal> + </Col> </Col> ) } @@ -389,7 +452,6 @@ function LimitOrderPanel(props: { const betChoice = 'YES' const [error, setError] = useState<string | undefined>() const [isSubmitting, setIsSubmitting] = useState(false) - const [wasSubmitted, setWasSubmitted] = useState(false) const rangeError = lowLimitProb !== undefined && @@ -437,7 +499,6 @@ function LimitOrderPanel(props: { const noAmount = shares * (1 - (noLimitProb ?? 0)) function onBetChange(newAmount: number | undefined) { - setWasSubmitted(false) setBetAmount(newAmount) } @@ -482,7 +543,6 @@ function LimitOrderPanel(props: { .then((r) => { console.log('placed bet. Result:', r) setIsSubmitting(false) - setWasSubmitted(true) setBetAmount(undefined) setLowLimitProb(undefined) setHighLimitProb(undefined) @@ -718,8 +778,6 @@ function LimitOrderPanel(props: { : `Submit order${hasTwoBets ? 's' : ''}`} </button> )} - - {wasSubmitted && <div className="mt-4">Order submitted!</div>} </Col> ) } @@ -866,11 +924,7 @@ export function SellPanel(props: { <> <AmountInput amount={ - amount - ? Math.round(amount) === 0 - ? 0 - : Math.floor(amount) - : undefined + amount ? (Math.round(amount) === 0 ? 0 : Math.floor(amount)) : 0 } onChange={onAmountChange} label="Qty" diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 038a3910..5a95f22f 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -77,7 +77,7 @@ export function BetsList(props: { user: User }) { }, [contractList]) const [sort, setSort] = useState<BetSort>('newest') - const [filter, setFilter] = useState<BetFilter>('open') + const [filter, setFilter] = useState<BetFilter>('all') const [page, setPage] = useState(0) const start = page * CONTRACTS_PER_PAGE const end = start + CONTRACTS_PER_PAGE @@ -155,34 +155,25 @@ export function BetsList(props: { user: User }) { (c) => contractsMetrics[c.id].netPayout ) - const totalPnl = user.profitCached.allTime - const totalProfitPercent = (totalPnl / user.totalDeposits) * 100 const investedProfitPercent = ((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100 return ( <Col> - <Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0"> - <Row className="gap-8"> - <Col> - <div className="text-sm text-gray-500">Investment value</div> - <div className="text-lg"> - {formatMoney(currentNetInvestment)}{' '} - <ProfitBadge profitPercent={investedProfitPercent} /> - </div> - </Col> - <Col> - <div className="text-sm text-gray-500">Total profit</div> - <div className="text-lg"> - {formatMoney(totalPnl)}{' '} - <ProfitBadge profitPercent={totalProfitPercent} /> - </div> - </Col> - </Row> + <Row className="justify-between gap-4 sm:flex-row"> + <Col> + <div className="text-greyscale-6 text-xs sm:text-sm"> + Investment value + </div> + <div className="text-lg"> + {formatMoney(currentNetInvestment)}{' '} + <ProfitBadge profitPercent={investedProfitPercent} /> + </div> + </Col> - <Row className="gap-8"> + <Row className="gap-2"> <select - className="select select-bordered self-start" + className="border-greyscale-4 self-start overflow-hidden rounded border px-2 py-2 text-sm" value={filter} onChange={(e) => setFilter(e.target.value as BetFilter)} > @@ -195,7 +186,7 @@ export function BetsList(props: { user: User }) { </select> <select - className="select select-bordered self-start" + className="border-greyscale-4 self-start overflow-hidden rounded px-2 py-2 text-sm" value={sort} onChange={(e) => setSort(e.target.value as BetSort)} > @@ -205,7 +196,7 @@ export function BetsList(props: { user: User }) { <option value="closeTime">Close date</option> </select> </Row> - </Col> + </Row> <Col className="mt-6 divide-y"> {displayedContracts.length === 0 ? ( @@ -490,18 +481,24 @@ function BetRow(props: { const isNumeric = outcomeType === 'NUMERIC' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const saleAmount = saleBet?.sale?.amount + // calculateSaleAmount is very slow right now so that's why we memoized this + const payout = useMemo(() => { + const saleBetAmount = saleBet?.sale?.amount + if (saleBetAmount) { + return saleBetAmount + } else if (contract.isResolved) { + return resolvedPayout(contract, bet) + } else { + return calculateSaleAmount(contract, bet, unfilledBets) + } + }, [contract, bet, saleBet, unfilledBets]) const saleDisplay = isAnte ? ( 'ANTE' - ) : saleAmount !== undefined ? ( - <>{formatMoney(saleAmount)} (sold)</> + ) : saleBet ? ( + <>{formatMoney(payout)} (sold)</> ) : ( - formatMoney( - isResolved - ? resolvedPayout(contract, bet) - : calculateSaleAmount(contract, bet, unfilledBets) - ) + formatMoney(payout) ) const payoutIfChosenDisplay = diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 919cce86..ba589d0e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -48,6 +48,7 @@ import { Title } from './title' export const SORTS = [ { label: 'Newest', value: 'newest' }, { label: 'Trending', value: 'score' }, + { label: 'Daily trending', value: 'daily-score' }, { label: '24h volume', value: '24-hour-vol' }, { label: 'Last updated', value: 'last-updated' }, { label: 'Closing soon', value: 'close-date' }, @@ -88,6 +89,7 @@ export function ContractSearch(props: { hideGroupLink?: boolean hideQuickBet?: boolean noLinkAvatar?: boolean + showProbChange?: boolean } headerClassName?: string persistPrefix?: string @@ -101,6 +103,7 @@ export function ContractSearch(props: { loadMore: () => void ) => ReactNode autoFocus?: boolean + profile?: boolean | undefined }) { const { user, @@ -121,6 +124,7 @@ export function ContractSearch(props: { maxResults, renderContracts, autoFocus, + profile, } = props const [state, setState] = usePersistentState( @@ -128,6 +132,7 @@ export function ContractSearch(props: { numPages: 1, pages: [] as Contract[][], showTime: null as ShowTime | null, + showProbChange: false, }, !persistPrefix ? undefined @@ -181,8 +186,9 @@ export function ContractSearch(props: { const newPage = results.hits as any as Contract[] const showTime = sort === 'close-date' || sort === 'resolve-date' ? sort : null + const showProbChange = sort === 'daily-score' const pages = freshQuery ? [newPage] : [...state.pages, newPage] - setState({ numPages: results.nbPages, pages, showTime }) + setState({ numPages: results.nbPages, pages, showTime, showProbChange }) if (freshQuery && isWholePage) window.scrollTo(0, 0) } } @@ -200,6 +206,12 @@ export function ContractSearch(props: { }, 100) ).current + const updatedCardUIOptions = useMemo(() => { + if (cardUIOptions?.showProbChange === undefined && state.showProbChange) + return { ...cardUIOptions, showProbChange: true } + return cardUIOptions + }, [cardUIOptions, state.showProbChange]) + const contracts = state.pages .flat() .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) @@ -229,6 +241,10 @@ export function ContractSearch(props: { /> {renderContracts ? ( renderContracts(renderedContracts, performQuery) + ) : renderedContracts && renderedContracts.length === 0 && profile ? ( + <p className="mx-2 text-gray-500"> + This creator does not yet have any markets. + </p> ) : ( <ContractsGrid contracts={renderedContracts} @@ -236,7 +252,7 @@ export function ContractSearch(props: { showTime={state.showTime ?? undefined} onContractClick={onContractClick} highlightOptions={highlightOptions} - cardUIOptions={cardUIOptions} + cardUIOptions={updatedCardUIOptions} /> )} </Col> diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index bfb4829f..139b30fe 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -13,17 +13,16 @@ import { PseudoNumericResolutionOrExpectation, } from './contract-card' import { Bet } from 'common/bet' -import BetButton from '../bet-button' +import BetButton, { BinaryMobileBetting } from '../bet-button' import { AnswersGraph } from '../answers/answers-graph' import { Contract, - BinaryContract, CPMMContract, - CPMMBinaryContract, FreeResponseContract, MultipleChoiceContract, NumericContract, PseudoNumericContract, + BinaryContract, } from 'common/contract' import { ContractDetails } from './contract-details' import { NumericGraph } from './numeric-graph' @@ -78,19 +77,18 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => { <Row className="justify-between gap-4"> <OverviewQuestion text={contract.question} /> <BinaryResolutionOrChance - className="hidden items-end xl:flex" + className="flex items-end" contract={contract} large /> </Row> - <Row className="items-center justify-between gap-4 xl:hidden"> - <BinaryResolutionOrChance contract={contract} /> - {tradingAllowed(contract) && ( - <BetWidget contract={contract as CPMMBinaryContract} /> - )} - </Row> </Col> <ContractProbGraph contract={contract} bets={[...bets].reverse()} /> + <Row className="items-center justify-between gap-4 xl:hidden"> + {tradingAllowed(contract) && ( + <BinaryMobileBetting contract={contract} /> + )} + </Row> </Col> ) } diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 0b93148d..d6c9c5fa 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -128,6 +128,7 @@ export function CreatorContractsList(props: { creatorId: creator.id, }} persistPrefix={`user-${creator.id}`} + profile={true} /> ) } diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index 7b19306f..a71b6c7d 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -344,7 +344,7 @@ export function getColor(contract: Contract) { return ( OUTCOME_TO_COLOR[resolution as resolution] ?? // If resolved to a FR answer, use 'primary' - 'primary' + 'teal-500' ) } @@ -355,5 +355,5 @@ export function getColor(contract: Contract) { } // TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind - return 'primary' + return 'teal-500' } diff --git a/web/components/create-post.tsx b/web/components/create-post.tsx new file mode 100644 index 00000000..c176e61d --- /dev/null +++ b/web/components/create-post.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react' +import { Spacer } from 'web/components/layout/spacer' +import { Title } from 'web/components/title' +import Textarea from 'react-expanding-textarea' + +import { TextEditor, useTextEditor } from 'web/components/editor' +import { createPost } from 'web/lib/firebase/api' +import clsx from 'clsx' +import Router from 'next/router' +import { MAX_POST_TITLE_LENGTH } from 'common/post' +import { postPath } from 'web/lib/firebase/posts' +import { Group } from 'common/group' + +export function CreatePost(props: { group?: Group }) { + const [title, setTitle] = useState('') + const [error, setError] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + const { group } = props + + const { editor, upload } = useTextEditor({ + disabled: isSubmitting, + }) + + const isValid = editor && title.length > 0 && editor.isEmpty === false + + async function savePost(title: string) { + if (!editor) return + const newPost = { + title: title, + content: editor.getJSON(), + groupId: group?.id, + } + + const result = await createPost(newPost).catch((e) => { + console.log(e) + setError('There was an error creating the post, please try again') + return e + }) + if (result.post) { + await Router.push(postPath(result.post.slug)) + } + } + + return ( + <div className="mx-auto w-full max-w-3xl"> + <div className="rounded-lg px-6 py-4 sm:py-0"> + <Title className="!mt-0" text="Create a post" /> + <form> + <div className="form-control w-full"> + <label className="label"> + <span className="mb-1"> + Title<span className={'text-red-700'}> *</span> + </span> + </label> + <Textarea + placeholder="e.g. Elon Mania Post" + className="input input-bordered resize-none" + autoFocus + maxLength={MAX_POST_TITLE_LENGTH} + value={title} + onChange={(e) => setTitle(e.target.value || '')} + /> + <Spacer h={6} /> + <label className="label"> + <span className="mb-1"> + Content<span className={'text-red-700'}> *</span> + </span> + </label> + <TextEditor editor={editor} upload={upload} /> + <Spacer h={6} /> + + <button + type="submit" + className={clsx( + 'btn btn-primary normal-case', + isSubmitting && 'loading disabled' + )} + disabled={isSubmitting || !isValid || upload.isLoading} + onClick={async () => { + setIsSubmitting(true) + await savePost(title) + setIsSubmitting(false) + }} + > + {isSubmitting ? 'Creating...' : 'Create a post'} + </button> + {error !== '' && <div className="text-red-700">{error}</div>} + </div> + </form> + </div> + </div> + ) +} diff --git a/web/components/following-button.tsx b/web/components/following-button.tsx index 135f43a8..c897c89b 100644 --- a/web/components/following-button.tsx +++ b/web/components/following-button.tsx @@ -13,16 +13,18 @@ import { useDiscoverUsers } from 'web/hooks/use-users' import { TextButton } from './text-button' import { track } from 'web/lib/service/analytics' -export function FollowingButton(props: { user: User }) { - const { user } = props +export function FollowingButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const followingIds = useFollows(user.id) const followerIds = useFollowers(user.id) return ( <> - <TextButton onClick={() => setIsOpen(true)}> - <span className="font-semibold">{followingIds?.length ?? ''}</span>{' '} + <TextButton onClick={() => setIsOpen(true)} className={className}> + <span className={clsx('font-semibold')}> + {followingIds?.length ?? ''} + </span>{' '} Following </TextButton> @@ -69,15 +71,15 @@ export function EditFollowingButton(props: { user: User; className?: string }) { ) } -export function FollowersButton(props: { user: User }) { - const { user } = props +export function FollowersButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const followingIds = useFollows(user.id) const followerIds = useFollowers(user.id) return ( <> - <TextButton onClick={() => setIsOpen(true)}> + <TextButton onClick={() => setIsOpen(true)} className={className}> <span className="font-semibold">{followerIds?.length ?? ''}</span>{' '} Followers </TextButton> diff --git a/web/components/groups/group-about-post.tsx b/web/components/groups/group-about-post.tsx index b76d8037..4d3046e9 100644 --- a/web/components/groups/group-about-post.tsx +++ b/web/components/groups/group-about-post.tsx @@ -16,29 +16,26 @@ import { usePost } from 'web/hooks/use-post' export function GroupAboutPost(props: { group: Group isEditable: boolean - post: Post + post: Post | null }) { const { group, isEditable } = props const post = usePost(group.aboutPostId) ?? props.post return ( <div className="rounded-md bg-white p-4 "> - {isEditable ? ( - <RichEditGroupAboutPost group={group} post={post} /> - ) : ( - <Content content={post.content} /> - )} + {isEditable && <RichEditGroupAboutPost group={group} post={post} />} + {!isEditable && post && <Content content={post.content} />} </div> ) } -function RichEditGroupAboutPost(props: { group: Group; post: Post }) { +function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) { const { group, post } = props const [editing, setEditing] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const { editor, upload } = useTextEditor({ - defaultValue: post.content, + defaultValue: post?.content, disabled: isSubmitting, }) @@ -49,7 +46,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) { content: editor.getJSON(), } - if (group.aboutPostId == null) { + if (post == null) { const result = await createPost(newPost).catch((e) => { console.error(e) return e @@ -65,6 +62,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) { } async function deleteGroupAboutPost() { + if (post == null) return await deletePost(post) await deleteFieldFromGroup(group, 'aboutPostId') } @@ -91,7 +89,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) { </> ) : ( <> - {group.aboutPostId == null ? ( + {post == null ? ( <div className="text-center text-gray-500"> <p className="text-sm"> No post has been added yet. diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index e6271466..5c9d2edd 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -14,14 +14,14 @@ import { firebaseLogin } from 'web/lib/firebase/users' import { GroupLinkItem } from 'web/pages/groups' import toast from 'react-hot-toast' -export function GroupsButton(props: { user: User }) { - const { user } = props +export function GroupsButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const groups = useMemberGroups(user.id) return ( <> - <TextButton onClick={() => setIsOpen(true)}> + <TextButton onClick={() => setIsOpen(true)} className={className}> <span className="font-semibold">{groups?.length ?? ''}</span> Groups </TextButton> diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 980a3cfc..b82131ec 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import { useRouter, NextRouter } from 'next/router' import { ReactNode, useState } from 'react' import { track } from '@amplitude/analytics-browser' +import { Col } from './col' type Tab = { title: string @@ -55,11 +56,13 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) { )} aria-current={activeIndex === i ? 'page' : undefined} > - {tab.tabIcon && <span>{tab.tabIcon}</span>} {tab.badge ? ( <span className="px-0.5 font-bold">{tab.badge}</span> ) : null} - {tab.title} + <Col> + {tab.tabIcon && <div className="mx-auto">{tab.tabIcon}</div>} + {tab.title} + </Col> </a> ))} </nav> diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 94b2b878..60b4403b 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -106,7 +106,7 @@ export const OUTCOME_TO_COLOR = { } export function YesLabel() { - return <span className="text-primary">YES</span> + return <span className="text-teal-500">YES</span> } export function HigherLabel() { diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index d8489b47..6ed5d195 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -1,72 +1,155 @@ import { ResponsiveLine } from '@nivo/line' import { PortfolioMetrics } from 'common/user' +import { filterDefined } from 'common/util/array' import { formatMoney } from 'common/util/format' +import dayjs from 'dayjs' import { last } from 'lodash' import { memo } from 'react' import { useWindowSize } from 'web/hooks/use-window-size' -import { formatTime } from 'web/lib/util/time' +import { Col } from '../layout/col' export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { portfolioHistory: PortfolioMetrics[] mode: 'value' | 'profit' + handleGraphDisplayChange: (arg0: string | number | null) => void height?: number - includeTime?: boolean }) { - const { portfolioHistory, height, includeTime, mode } = props + const { portfolioHistory, height, mode, handleGraphDisplayChange } = props const { width } = useWindowSize() - const points = portfolioHistory.map((p) => { - const { timestamp, balance, investmentValue, totalDeposits } = p - const value = balance + investmentValue - const profit = value - totalDeposits + const valuePoints = getPoints('value', portfolioHistory) + const posProfitPoints = getPoints('posProfit', portfolioHistory) + const negProfitPoints = getPoints('negProfit', portfolioHistory) + + const valuePointsY = valuePoints.map((p) => p.y) + const posProfitPointsY = posProfitPoints.map((p) => p.y) + const negProfitPointsY = negProfitPoints.map((p) => p.y) + + let data + + if (mode === 'value') { + data = [{ id: 'value', data: valuePoints, color: '#4f46e5' }] + } else { + data = [ + { + id: 'negProfit', + data: negProfitPoints, + color: '#dc2626', + }, + { + id: 'posProfit', + data: posProfitPoints, + color: '#14b8a6', + }, + ] + } + const numYTickValues = 2 + const endDate = last(data[0].data)?.x + + const yMin = + mode === 'value' + ? Math.min(...filterDefined(valuePointsY)) + : Math.min( + ...filterDefined(negProfitPointsY), + ...filterDefined(posProfitPointsY) + ) + + const yMax = + mode === 'value' + ? Math.max(...filterDefined(valuePointsY)) + : Math.max( + ...filterDefined(negProfitPointsY), + ...filterDefined(posProfitPointsY) + ) - return { - x: new Date(timestamp), - y: mode === 'value' ? value : profit, - } - }) - const data = [{ id: 'Value', data: points, color: '#11b981' }] - const numXTickValues = !width || width < 800 ? 2 : 5 - const numYTickValues = 4 - const endDate = last(points)?.x return ( <div className="w-full overflow-hidden" - style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }} + style={{ height: height ?? (!width || width >= 800 ? 200 : 100) }} + onMouseLeave={() => handleGraphDisplayChange(null)} > <ResponsiveLine + margin={{ top: 10, right: 0, left: 40, bottom: 10 }} data={data} - margin={{ top: 20, right: 28, bottom: 22, left: 60 }} xScale={{ type: 'time', - min: points[0]?.x, + min: valuePoints[0]?.x, max: endDate, }} yScale={{ type: 'linear', stacked: false, - min: Math.min(...points.map((p) => p.y)), + min: yMin, + max: yMax, }} - gridYValues={numYTickValues} curve="stepAfter" enablePoints={false} colors={{ datum: 'color' }} axisBottom={{ - tickValues: numXTickValues, - format: (time) => formatTime(+time, !!includeTime), + tickValues: 0, }} pointBorderColor="#fff" - pointSize={points.length > 100 ? 0 : 6} + pointSize={valuePoints.length > 100 ? 0 : 6} axisLeft={{ tickValues: numYTickValues, - format: (value) => formatMoney(value), + format: '.3s', }} - enableGridX={!!width && width >= 800} + enableGridX={false} enableGridY={true} + gridYValues={numYTickValues} enableSlices="x" animate={false} yFormat={(value) => formatMoney(+value)} + enableArea={true} + areaOpacity={0.1} + sliceTooltip={({ slice }) => { + handleGraphDisplayChange(slice.points[0].data.yFormatted) + return ( + <div className="rounded bg-white px-4 py-2 opacity-80"> + <div + key={slice.points[0].id} + className="text-xs font-semibold sm:text-sm" + > + <Col> + <div> + {dayjs(slice.points[0].data.xFormatted).format('MMM/D/YY')} + </div> + <div className="text-greyscale-6 text-2xs font-normal sm:text-xs"> + {dayjs(slice.points[0].data.xFormatted).format('h:mm A')} + </div> + </Col> + </div> + {/* ))} */} + </div> + ) + }} ></ResponsiveLine> </div> ) }) + +export function getPoints( + line: 'value' | 'posProfit' | 'negProfit', + portfolioHistory: PortfolioMetrics[] +) { + const points = portfolioHistory.map((p) => { + const { timestamp, balance, investmentValue, totalDeposits } = p + const value = balance + investmentValue + + const profit = value - totalDeposits + let posProfit = null + let negProfit = null + if (profit < 0) { + negProfit = profit + } else { + posProfit = profit + } + + return { + x: new Date(timestamp), + y: + line === 'value' ? value : line === 'posProfit' ? posProfit : negProfit, + } + }) + return points +} diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index a0006c60..ec364c8d 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -1,11 +1,12 @@ +import clsx from 'clsx' import { formatMoney } from 'common/util/format' import { last } from 'lodash' import { memo, useRef, useState } from 'react' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { Period } from 'web/lib/firebase/users' +import { PillButton } from '../buttons/pill-button' import { Col } from '../layout/col' import { Row } from '../layout/row' -import { Spacer } from '../layout/spacer' import { PortfolioValueGraph } from './portfolio-value-graph' export const PortfolioValueSection = memo( @@ -14,6 +15,13 @@ export const PortfolioValueSection = memo( const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('weekly') const portfolioHistory = usePortfolioHistory(userId, portfolioPeriod) + const [graphMode, setGraphMode] = useState<'profit' | 'value'>('value') + const [graphDisplayNumber, setGraphDisplayNumber] = useState< + number | string | null + >(null) + const handleGraphDisplayChange = (num: string | number | null) => { + setGraphDisplayNumber(num) + } // Remember the last defined portfolio history. const portfolioRef = useRef(portfolioHistory) @@ -28,43 +36,144 @@ export const PortfolioValueSection = memo( const { balance, investmentValue, totalDeposits } = lastPortfolioMetrics const totalValue = balance + investmentValue const totalProfit = totalValue - totalDeposits - return ( <> - <Row className="gap-8"> - <Col className="flex-1 justify-center"> - <div className="text-sm text-gray-500">Profit</div> - <div className="text-lg">{formatMoney(totalProfit)}</div> - </Col> - <select - className="select select-bordered self-start" - value={portfolioPeriod} - onChange={(e) => { - setPortfolioPeriod(e.target.value as Period) - }} - > - <option value="allTime">All time</option> - <option value="monthly">Last Month</option> - <option value="weekly">Last 7d</option> - <option value="daily">Last 24h</option> - </select> + <Row className="mb-2 justify-between"> + <Row className="gap-4 sm:gap-8"> + <Col + className={clsx( + 'cursor-pointer', + graphMode != 'value' ? 'opacity-40 hover:opacity-80' : '' + )} + onClick={() => setGraphMode('value')} + > + <div className="text-greyscale-6 text-xs sm:text-sm"> + Portfolio value + </div> + <div className={clsx('text-lg text-indigo-600 sm:text-xl')}> + {graphMode === 'value' + ? graphDisplayNumber + ? graphDisplayNumber + : formatMoney(totalValue) + : formatMoney(totalValue)} + </div> + </Col> + <Col + className={clsx( + 'cursor-pointer', + graphMode != 'profit' + ? 'cursor-pointer opacity-40 hover:opacity-80' + : '' + )} + onClick={() => setGraphMode('profit')} + > + <div className="text-greyscale-6 text-xs sm:text-sm">Profit</div> + <div + className={clsx( + graphMode === 'profit' + ? graphDisplayNumber + ? graphDisplayNumber.toString().includes('-') + ? 'text-red-600' + : 'text-teal-500' + : totalProfit > 0 + ? 'text-teal-500' + : 'text-red-600' + : totalProfit > 0 + ? 'text-teal-500' + : 'text-red-600', + 'text-lg sm:text-xl' + )} + > + {graphMode === 'profit' + ? graphDisplayNumber + ? graphDisplayNumber + : formatMoney(totalProfit) + : formatMoney(totalProfit)} + </div> + </Col> + </Row> </Row> <PortfolioValueGraph portfolioHistory={currPortfolioHistory} - includeTime={portfolioPeriod == 'daily'} - mode="profit" + mode={graphMode} + handleGraphDisplayChange={handleGraphDisplayChange} /> - <Spacer h={8} /> - <Col className="flex-1 justify-center"> - <div className="text-sm text-gray-500">Portfolio value</div> - <div className="text-lg">{formatMoney(totalValue)}</div> - </Col> - <PortfolioValueGraph - portfolioHistory={currPortfolioHistory} - includeTime={portfolioPeriod == 'daily'} - mode="value" + <PortfolioPeriodSelection + portfolioPeriod={portfolioPeriod} + setPortfolioPeriod={setPortfolioPeriod} + className="border-greyscale-2 mt-2 gap-4 border-b" + selectClassName="text-indigo-600 text-bold border-b border-indigo-600" /> </> ) } ) + +export function PortfolioPeriodSelection(props: { + setPortfolioPeriod: (string: any) => void + portfolioPeriod: string + className?: string + selectClassName?: string +}) { + const { setPortfolioPeriod, portfolioPeriod, className, selectClassName } = + props + return ( + <Row className={clsx(className, 'text-greyscale-4')}> + <button + className={clsx(portfolioPeriod === 'daily' ? selectClassName : '')} + onClick={() => setPortfolioPeriod('daily' as Period)} + > + 1D + </button> + <button + className={clsx(portfolioPeriod === 'weekly' ? selectClassName : '')} + onClick={() => setPortfolioPeriod('weekly' as Period)} + > + 1W + </button> + <button + className={clsx(portfolioPeriod === 'monthly' ? selectClassName : '')} + onClick={() => setPortfolioPeriod('monthly' as Period)} + > + 1M + </button> + <button + className={clsx(portfolioPeriod === 'allTime' ? selectClassName : '')} + onClick={() => setPortfolioPeriod('allTime' as Period)} + > + ALL + </button> + </Row> + ) +} + +export function GraphToggle(props: { + setGraphMode: (mode: 'profit' | 'value') => void + graphMode: string +}) { + const { setGraphMode, graphMode } = props + return ( + <Row className="relative mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2"> + <PillButton + selected={graphMode === 'value'} + onSelect={() => { + setGraphMode('value') + }} + xs={true} + className="z-50" + > + Value + </PillButton> + <PillButton + selected={graphMode === 'profit'} + onSelect={() => { + setGraphMode('profit') + }} + xs={true} + className="z-50" + > + Profit + </PillButton> + </Row> + ) +} diff --git a/web/components/profile/user-likes-button.tsx b/web/components/profile/user-likes-button.tsx index 3d4fa9ac..666036a8 100644 --- a/web/components/profile/user-likes-button.tsx +++ b/web/components/profile/user-likes-button.tsx @@ -10,15 +10,15 @@ import { XIcon } from '@heroicons/react/outline' import { unLikeContract } from 'web/lib/firebase/likes' import { contractPath } from 'web/lib/firebase/contracts' -export function UserLikesButton(props: { user: User }) { - const { user } = props +export function UserLikesButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const likedContracts = useUserLikedContracts(user.id) return ( <> - <TextButton onClick={() => setIsOpen(true)}> + <TextButton onClick={() => setIsOpen(true)} className={className}> <span className="font-semibold">{likedContracts?.length ?? ''}</span>{' '} Likes </TextButton> diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index b164e10c..9a548031 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -13,14 +13,18 @@ import { getUser, updateUser } from 'web/lib/firebase/users' import { TextButton } from 'web/components/text-button' import { UserLink } from 'web/components/user-link' -export function ReferralsButton(props: { user: User; currentUser?: User }) { - const { user, currentUser } = props +export function ReferralsButton(props: { + user: User + currentUser?: User + className?: string +}) { + const { user, currentUser, className } = props const [isOpen, setIsOpen] = useState(false) const referralIds = useReferrals(user.id) return ( <> - <TextButton onClick={() => setIsOpen(true)}> + <TextButton onClick={() => setIsOpen(true)} className={className}> <span className="font-semibold">{referralIds?.length ?? ''}</span>{' '} Referrals </TextButton> diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index f9845fbe..fde75607 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -1,8 +1,13 @@ import clsx from 'clsx' import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' +import { NextRouter, useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' -import { PencilIcon } from '@heroicons/react/outline' +import { + ChatIcon, + FolderIcon, + PencilIcon, + ScaleIcon, +} from '@heroicons/react/outline' import { User } from 'web/lib/firebase/users' import { useUser } from 'web/hooks/use-user' @@ -24,39 +29,23 @@ import { FollowersButton, FollowingButton } from './following-button' import { UserFollowButton } from './follow-button' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' -import { ReferralsButton } from 'web/components/referrals-button' import { formatMoney } from 'common/util/format' -import { ShareIconButton } from 'web/components/share-icon-button' -import { ENV_CONFIG } from 'common/envs/constants' import { BettingStreakModal, hasCompletedStreakToday, } from 'web/components/profile/betting-streak-modal' -import { REFERRAL_AMOUNT } from 'common/economy' import { LoansModal } from './profile/loans-modal' -import { UserLikesButton } from 'web/components/profile/user-likes-button' -import { PAST_BETS } from 'common/user' -import { capitalize } from 'lodash' export function UserPage(props: { user: User }) { const { user } = props const router = useRouter() const currentUser = useUser() const isCurrentUser = user.id === currentUser?.id - const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) const [showConfetti, setShowConfetti] = useState(false) - const [showBettingStreakModal, setShowBettingStreakModal] = useState(false) - const [showLoansModal, setShowLoansModal] = useState(false) useEffect(() => { const claimedMana = router.query['claimed-mana'] === 'yes' - const showBettingStreak = router.query['show'] === 'betting-streak' - setShowBettingStreakModal(showBettingStreak) - setShowConfetti(claimedMana || showBettingStreak) - - const showLoansModel = router.query['show'] === 'loans' - setShowLoansModal(showLoansModel) - + setShowConfetti(claimedMana) const query = { ...router.query } if (query.claimedMana || query.show) { delete query['claimed-mana'] @@ -85,218 +74,159 @@ export function UserPage(props: { user: User }) { {showConfetti && ( <FullscreenConfetti recycle={false} numberOfPieces={300} /> )} - <BettingStreakModal - isOpen={showBettingStreakModal} - setOpen={setShowBettingStreakModal} - currentUser={currentUser} - /> - {showLoansModal && ( - <LoansModal isOpen={showLoansModal} setOpen={setShowLoansModal} /> - )} - {/* Banner image up top, with an circle avatar overlaid */} - <div - className="h-32 w-full bg-cover bg-center sm:h-40" - style={{ - backgroundImage: `url(${bannerUrl})`, - }} - ></div> - <div className="relative mb-20"> - <div className="absolute -top-10 left-4"> + <Col className="relative"> + <Row className="relative px-4 pt-4"> <Avatar username={user.username} avatarUrl={user.avatarUrl} size={24} - className="bg-white ring-4 ring-white" + className="bg-white shadow-sm shadow-indigo-300" /> - </div> - - {/* Top right buttons (e.g. edit, follow) */} - <div className="absolute right-0 top-0 mt-2 mr-4"> - {!isCurrentUser && <UserFollowButton userId={user.id} />} {isCurrentUser && ( - <SiteLink className="btn-sm btn" href="/profile"> - <PencilIcon className="h-5 w-5" />{' '} - <div className="ml-2">Edit</div> - </SiteLink> + <div className="absolute z-50 ml-16 mt-16 rounded-full bg-indigo-600 p-2 text-white shadow-sm shadow-indigo-300"> + <SiteLink href="/profile"> + <PencilIcon className="h-5" />{' '} + </SiteLink> + </div> )} - </div> - </div> - {/* Profile details: name, username, bio, and link to twitter/discord */} - <Col className="mx-4 -mt-6"> - <Row className={'flex-wrap justify-between gap-y-2'}> - <Col> - <span className="break-anywhere text-2xl font-bold"> - {user.name} - </span> - <span className="text-gray-500">@{user.username}</span> - </Col> - <Col className={'justify-center'}> - <Row className={'gap-3'}> - <Col className={'items-center text-gray-500'}> - <span - className={clsx( - 'text-md', - profit >= 0 ? 'text-green-600' : 'text-red-400' - )} - > - {formatMoney(profit)} + <Col className="w-full gap-4 pl-5"> + <div className="flex flex-col gap-2 sm:flex-row sm:justify-between"> + <Col> + <span className="break-anywhere text-lg font-bold sm:text-2xl"> + {user.name} </span> - <span>profit</span> - </Col> - <Col - className={clsx( - 'cursor-pointer items-center text-gray-500', - isCurrentUser && !hasCompletedStreakToday(user) - ? 'grayscale' - : 'grayscale-0' - )} - onClick={() => setShowBettingStreakModal(true)} - > - <span>🔥 {user.currentBettingStreak ?? 0}</span> - <span>streak</span> - </Col> - <Col - className={ - 'flex-shrink-0 cursor-pointer items-center text-gray-500' - } - onClick={() => setShowLoansModal(true)} - > - <span className="text-green-600"> - 🏦 {formatMoney(user.nextLoanCached ?? 0)} + <span className="sm:text-md text-greyscale-4 text-sm"> + @{user.username} </span> - <span>next loan</span> </Col> - </Row> + {isCurrentUser && ( + <ProfilePrivateStats + currentUser={currentUser} + profit={profit} + user={user} + router={router} + /> + )} + {!isCurrentUser && <UserFollowButton userId={user.id} />} + </div> + <ProfilePublicStats + className="sm:text-md text-greyscale-6 hidden text-sm md:inline" + user={user} + /> </Col> </Row> - <Spacer h={4} /> - {user.bio && ( - <> - <div> - <Linkify text={user.bio}></Linkify> - </div> - <Spacer h={4} /> - </> - )} - {(user.website || user.twitterHandle || user.discordHandle) && ( - <Row className="mb-5 flex-wrap items-center gap-2 sm:gap-4"> - {user.website && ( - <SiteLink - href={ - 'https://' + - user.website.replace('http://', '').replace('https://', '') - } - > - <Row className="items-center gap-1"> - <LinkIcon className="h-4 w-4" /> - <span className="text-sm text-gray-500">{user.website}</span> - </Row> - </SiteLink> - )} - - {user.twitterHandle && ( - <SiteLink - href={`https://twitter.com/${user.twitterHandle - .replace('https://www.twitter.com/', '') - .replace('https://twitter.com/', '') - .replace('www.twitter.com/', '') - .replace('twitter.com/', '')}`} - > - <Row className="items-center gap-1"> - <img - src="/twitter-logo.svg" - className="h-4 w-4" - alt="Twitter" - /> - <span className="text-sm text-gray-500"> - {user.twitterHandle} - </span> - </Row> - </SiteLink> - )} - - {user.discordHandle && ( - <SiteLink href="https://discord.com/invite/eHQBNBqXuh"> - <Row className="items-center gap-1"> - <img - src="/discord-logo.svg" - className="h-4 w-4" - alt="Discord" - /> - <span className="text-sm text-gray-500"> - {user.discordHandle} - </span> - </Row> - </SiteLink> - )} - </Row> - )} - {currentUser?.id === user.id && REFERRAL_AMOUNT > 0 && ( - <Row - className={ - 'mb-5 w-full items-center justify-center gap-2 rounded-md border-2 border-indigo-100 bg-indigo-50 p-2 text-indigo-600' - } - > - <span> - <SiteLink href="/referrals"> - Earn {formatMoney(REFERRAL_AMOUNT)} when you refer a friend! - </SiteLink>{' '} - You've gotten{' '} - <ReferralsButton user={user} currentUser={currentUser} /> - </span> - <ShareIconButton - copyPayload={`https://${ENV_CONFIG.domain}?referrer=${currentUser.username}`} - toastClassName={'sm:-left-40 -left-40 min-w-[250%]'} - buttonClassName={'h-10 w-10'} - iconClassName={'h-8 w-8 text-indigo-700'} - /> - </Row> - )} - <QueryUncontrolledTabs - className="mb-4" - currentPageForAnalytics={'profile'} - labelClassName={'pb-2 pt-1 '} - tabs={[ - { - title: 'Markets', - content: ( - <CreatorContractsList user={currentUser} creator={user} /> - ), - }, - { - title: 'Comments', - content: ( - <Col> - <UserCommentsList user={user} /> - </Col> - ), - }, - { - title: capitalize(PAST_BETS), - content: ( - <> - <BetsList user={user} /> - </> - ), - }, - { - title: 'Stats', - content: ( - <Col className="mb-8"> - <Row className="mb-8 flex-wrap items-center gap-x-6 gap-y-2"> - <FollowingButton user={user} /> - <FollowersButton user={user} /> - <ReferralsButton user={user} /> - <GroupsButton user={user} /> - <UserLikesButton user={user} /> + <Col className="mx-4 mt-2"> + <Spacer h={1} /> + <ProfilePublicStats + className="text-greyscale-6 text-sm md:hidden" + user={user} + /> + <Spacer h={1} /> + {user.bio && ( + <> + <div className="sm:text-md mt-2 text-sm sm:mt-0"> + <Linkify text={user.bio}></Linkify> + </div> + <Spacer h={2} /> + </> + )} + {(user.website || user.twitterHandle || user.discordHandle) && ( + <Row className="mb-2 flex-wrap items-center gap-2 sm:gap-4"> + {user.website && ( + <SiteLink + href={ + 'https://' + + user.website.replace('http://', '').replace('https://', '') + } + > + <Row className="items-center gap-1"> + <LinkIcon className="h-4 w-4" /> + <span className="text-greyscale-4 text-sm"> + {user.website} + </span> </Row> - <PortfolioValueSection userId={user.id} /> - </Col> - ), - }, - ]} - /> + </SiteLink> + )} + + {user.twitterHandle && ( + <SiteLink + href={`https://twitter.com/${user.twitterHandle + .replace('https://www.twitter.com/', '') + .replace('https://twitter.com/', '') + .replace('www.twitter.com/', '') + .replace('twitter.com/', '')}`} + > + <Row className="items-center gap-1"> + <img + src="/twitter-logo.svg" + className="h-4 w-4" + alt="Twitter" + /> + <span className="text-greyscale-4 text-sm"> + {user.twitterHandle} + </span> + </Row> + </SiteLink> + )} + + {user.discordHandle && ( + <SiteLink href="https://discord.com/invite/eHQBNBqXuh"> + <Row className="items-center gap-1"> + <img + src="/discord-logo.svg" + className="h-4 w-4" + alt="Discord" + /> + <span className="text-greyscale-4 text-sm"> + {user.discordHandle} + </span> + </Row> + </SiteLink> + )} + </Row> + )} + <QueryUncontrolledTabs + currentPageForAnalytics={'profile'} + labelClassName={'pb-2 pt-1 sm:pt-4 '} + tabs={[ + { + title: 'Markets', + tabIcon: <ScaleIcon className="h-5" />, + content: ( + <> + <Spacer h={4} /> + <CreatorContractsList user={currentUser} creator={user} /> + </> + ), + }, + { + title: 'Portfolio', + tabIcon: <FolderIcon className="h-5" />, + content: ( + <> + <Spacer h={4} /> + <PortfolioValueSection userId={user.id} /> + <Spacer h={4} /> + <BetsList user={user} /> + </> + ), + }, + { + title: 'Comments', + tabIcon: <ChatIcon className="h-5" />, + content: ( + <> + <Spacer h={4} /> + <Col> + <UserCommentsList user={user} /> + </Col> + </> + ), + }, + ]} + /> + </Col> </Col> </Page> ) @@ -314,3 +244,88 @@ export function defaultBannerUrl(userId: string) { ] return defaultBanner[genHash(userId)() % defaultBanner.length] } + +export function ProfilePrivateStats(props: { + currentUser: User | null | undefined + profit: number + user: User + router: NextRouter +}) { + const { currentUser, profit, user, router } = props + const [showBettingStreakModal, setShowBettingStreakModal] = useState(false) + const [showLoansModal, setShowLoansModal] = useState(false) + + useEffect(() => { + const showBettingStreak = router.query['show'] === 'betting-streak' + setShowBettingStreakModal(showBettingStreak) + + const showLoansModel = router.query['show'] === 'loans' + setShowLoansModal(showLoansModel) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( + <> + <Row className={'justify-between gap-4 sm:justify-end'}> + <Col className={'text-greyscale-4 text-md sm:text-lg'}> + <span + className={clsx(profit >= 0 ? 'text-green-600' : 'text-red-400')} + > + {formatMoney(profit)} + </span> + <span className="mx-auto text-xs sm:text-sm">profit</span> + </Col> + <Col + className={clsx('text-,d cursor-pointer sm:text-lg ')} + onClick={() => setShowBettingStreakModal(true)} + > + <span + className={clsx( + !hasCompletedStreakToday(user) + ? 'opacity-50 grayscale' + : 'grayscale-0' + )} + > + 🔥 {user.currentBettingStreak ?? 0} + </span> + <span className="text-greyscale-4 mx-auto text-xs sm:text-sm"> + streak + </span> + </Col> + <Col + className={ + 'text-greyscale-4 text-md flex-shrink-0 cursor-pointer sm:text-lg' + } + onClick={() => setShowLoansModal(true)} + > + <span className="text-green-600"> + 🏦 {formatMoney(user.nextLoanCached ?? 0)} + </span> + <span className="mx-auto text-xs sm:text-sm">next loan</span> + </Col> + </Row> + {BettingStreakModal && ( + <BettingStreakModal + isOpen={showBettingStreakModal} + setOpen={setShowBettingStreakModal} + currentUser={currentUser} + /> + )} + {showLoansModal && ( + <LoansModal isOpen={showLoansModal} setOpen={setShowLoansModal} /> + )} + </> + ) +} + +export function ProfilePublicStats(props: { user: User; className?: string }) { + const { user, className } = props + return ( + <Row className={'flex-wrap items-center gap-3'}> + <FollowingButton user={user} className={className} /> + <FollowersButton user={user} className={className} /> + {/* <ReferralsButton user={user} className={className} /> */} + <GroupsButton user={user} className={className} /> + {/* <UserLikesButton user={user} className={className} /> */} + </Row> + ) +} diff --git a/web/components/warning-confirmation-button.tsx b/web/components/warning-confirmation-button.tsx index 7b707098..7c546c3b 100644 --- a/web/components/warning-confirmation-button.tsx +++ b/web/components/warning-confirmation-button.tsx @@ -4,8 +4,12 @@ import React from 'react' import { Row } from './layout/row' import { ConfirmationButton } from './confirmation-button' import { ExclamationIcon } from '@heroicons/react/solid' +import { formatMoney } from 'common/util/format' export function WarningConfirmationButton(props: { + amount: number | undefined + outcome?: 'YES' | 'NO' | undefined + marketType: 'freeResponse' | 'binary' warning?: string onSubmit: () => void disabled?: boolean @@ -14,26 +18,36 @@ export function WarningConfirmationButton(props: { submitButtonClassName?: string }) { const { + amount, onSubmit, warning, disabled, isSubmitting, openModalButtonClass, submitButtonClassName, + outcome, + marketType, } = props - if (!warning) { return ( <button className={clsx( openModalButtonClass, - isSubmitting ? 'loading' : '', - disabled && 'btn-disabled' + isSubmitting ? 'loading btn-disabled' : '', + disabled && 'btn-disabled', + marketType === 'binary' + ? !outcome + ? 'btn-disabled bg-greyscale-2' + : '' + : '' )} onClick={onSubmit} - disabled={disabled} > - {isSubmitting ? 'Submitting...' : 'Submit'} + {isSubmitting + ? 'Submitting...' + : amount + ? `Wager ${formatMoney(amount)}` + : 'Wager'} </button> ) } @@ -45,7 +59,7 @@ export function WarningConfirmationButton(props: { openModalButtonClass, isSubmitting && 'btn-disabled loading' ), - label: 'Submit', + label: amount ? `Wager ${formatMoney(amount)}` : 'Wager', }} cancelBtn={{ label: 'Cancel', diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index 719308bf..f73cdef2 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -35,10 +35,11 @@ export function YesNoSelector(props: { <button className={clsx( commonClassNames, - 'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white', selected == 'YES' - ? 'bg-primary text-white' - : 'text-primary bg-white', + ? 'border-teal-500 bg-teal-500 text-white' + : selected == 'NO' + ? 'border-greyscale-3 text-greyscale-3 bg-white hover:border-teal-500 hover:text-teal-500' + : 'border-teal-500 bg-white text-teal-500 hover:bg-teal-50', btnClassName )} onClick={() => onSelect('YES')} @@ -52,10 +53,11 @@ export function YesNoSelector(props: { <button className={clsx( commonClassNames, - 'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white', selected == 'NO' - ? 'bg-red-400 text-white' - : 'bg-white text-red-400', + ? 'border-red-400 bg-red-400 text-white' + : selected == 'YES' + ? 'border-greyscale-3 text-greyscale-3 bg-white hover:border-red-400 hover:text-red-400' + : 'border-red-400 bg-white text-red-400 hover:bg-red-50', btnClassName )} onClick={() => onSelect('NO')} diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index fb71ca60..11aae65c 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -9,12 +9,10 @@ import { getUserBetContractsQuery, listAllContracts, trendingContractsQuery, - getContractsQuery, } from 'web/lib/firebase/contracts' import { QueryClient, useQuery, useQueryClient } from 'react-query' -import { MINUTE_MS } from 'common/util/time' +import { MINUTE_MS, sleep } from 'common/util/time' import { query, limit } from 'firebase/firestore' -import { Sort } from 'web/components/contract-search' import { dailyScoreIndex } from 'web/lib/service/algolia' import { CPMMBinaryContract } from 'common/contract' import { zipObject } from 'lodash' @@ -66,19 +64,6 @@ export const useTrendingContracts = (maxContracts: number) => { return result.data } -export const useContractsQuery = ( - sort: Sort, - maxContracts: number, - filters: { groupSlug?: string } = {}, - visibility?: 'public' -) => { - const result = useFirestoreQueryData( - ['contracts-query', sort, maxContracts, filters], - getContractsQuery(sort, maxContracts, filters, visibility) - ) - return result.data -} - export const useInactiveContracts = () => { const [contracts, setContracts] = useState<Contract[] | undefined>() @@ -101,7 +86,7 @@ export const usePrefetchUserBetContracts = (userId: string) => { const queryClient = useQueryClient() return queryClient.prefetchQuery( ['contracts', 'bets', userId], - () => getUserBetContracts(userId), + () => sleep(1000).then(() => getUserBetContracts(userId)), { staleTime: 5 * MINUTE_MS } ) } diff --git a/web/hooks/use-is-mobile.ts b/web/hooks/use-is-mobile.ts index 9ce0133c..7e99a97d 100644 --- a/web/hooks/use-is-mobile.ts +++ b/web/hooks/use-is-mobile.ts @@ -1,7 +1,13 @@ -import { useWindowSize } from 'web/hooks/use-window-size' +import { useEffect, useState } from 'react' -// matches talwind sm breakpoint -export function useIsMobile() { - const { width } = useWindowSize() - return (width ?? 0) < 640 +export function useIsMobile(threshold?: number) { + const [isMobile, setIsMobile] = useState<boolean>() + useEffect(() => { + // 640 matches tailwind sm breakpoint + const onResize = () => setIsMobile(window.innerWidth < (threshold ?? 640)) + onResize() + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, [threshold]) + return isMobile } diff --git a/web/hooks/use-portfolio-history.ts b/web/hooks/use-portfolio-history.ts index 6cc1a84e..1ae5e7ee 100644 --- a/web/hooks/use-portfolio-history.ts +++ b/web/hooks/use-portfolio-history.ts @@ -1,6 +1,6 @@ import { useQueryClient } from 'react-query' import { useFirestoreQueryData } from '@react-query-firebase/firestore' -import { DAY_MS, HOUR_MS, MINUTE_MS } from 'common/util/time' +import { DAY_MS, HOUR_MS, MINUTE_MS, sleep } from 'common/util/time' import { getPortfolioHistory, getPortfolioHistoryQuery, @@ -17,7 +17,7 @@ export const usePrefetchPortfolioHistory = (userId: string, period: Period) => { const cutoff = getCutoff(period) return queryClient.prefetchQuery( ['portfolio-history', userId, cutoff], - () => getPortfolioHistory(userId, cutoff), + () => sleep(1000).then(() => getPortfolioHistory(userId, cutoff)), { staleTime: 15 * MINUTE_MS } ) } diff --git a/web/hooks/use-post.ts b/web/hooks/use-post.ts index 9daf2d22..ff7bf6b9 100644 --- a/web/hooks/use-post.ts +++ b/web/hooks/use-post.ts @@ -11,3 +11,29 @@ export const usePost = (postId: string | undefined) => { return post } + +export const usePosts = (postIds: string[]) => { + const [posts, setPosts] = useState<Post[]>([]) + useEffect(() => { + if (postIds.length === 0) return + setPosts([]) + + const unsubscribes = postIds.map((postId) => + listenForPost(postId, (post) => { + if (post) { + setPosts((posts) => [...posts, post]) + } + }) + ) + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()) + } + }, [postIds]) + + return posts + .filter( + (post, index, self) => index === self.findIndex((t) => t.id === post.id) + ) + .sort((a, b) => b.createdTime - a.createdTime) +} diff --git a/web/hooks/use-prefetch.ts b/web/hooks/use-prefetch.ts index 46d78b3c..0e83613b 100644 --- a/web/hooks/use-prefetch.ts +++ b/web/hooks/use-prefetch.ts @@ -1,5 +1,4 @@ import { usePrefetchUserBetContracts } from './use-contracts' -import { usePrefetchPortfolioHistory } from './use-portfolio-history' import { usePrefetchUserBets } from './use-user-bets' export function usePrefetch(userId: string | undefined) { @@ -7,6 +6,5 @@ export function usePrefetch(userId: string | undefined) { return Promise.all([ usePrefetchUserBets(maybeUserId), usePrefetchUserBetContracts(maybeUserId), - usePrefetchPortfolioHistory(maybeUserId, 'weekly'), ]) } diff --git a/web/hooks/use-prob-changes.tsx b/web/hooks/use-prob-changes.tsx index f2f3ce13..132cfd64 100644 --- a/web/hooks/use-prob-changes.tsx +++ b/web/hooks/use-prob-changes.tsx @@ -41,7 +41,7 @@ export const useProbChanges = ( const hits = uniqBy( [...positiveChanges.hits, ...negativeChanges.hits], (c) => c.id - ) + ).filter((c) => c.probChanges) return sortBy(hits, (c) => Math.abs(c.probChanges.day)).reverse() } diff --git a/web/hooks/use-user-bets.ts b/web/hooks/use-user-bets.ts index 3731fb07..c28b453d 100644 --- a/web/hooks/use-user-bets.ts +++ b/web/hooks/use-user-bets.ts @@ -7,13 +7,13 @@ import { getUserBetsQuery, listenForUserContractBets, } from 'web/lib/firebase/bets' -import { MINUTE_MS } from 'common/util/time' +import { MINUTE_MS, sleep } from 'common/util/time' export const usePrefetchUserBets = (userId: string) => { const queryClient = useQueryClient() return queryClient.prefetchQuery( ['bets', userId], - () => getUserBets(userId), + () => sleep(1000).then(() => getUserBets(userId)), { staleTime: MINUTE_MS } ) } diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts index 6b1b43d8..8aa7a067 100644 --- a/web/lib/firebase/api.ts +++ b/web/lib/firebase/api.ts @@ -90,6 +90,10 @@ export function getCurrentUser(params: any) { return call(getFunctionUrl('getcurrentuser'), 'GET', params) } -export function createPost(params: { title: string; content: JSONContent }) { +export function createPost(params: { + title: string + content: JSONContent + groupId?: string +}) { return call(getFunctionUrl('createpost'), 'POST', params) } diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 927f7187..d7f6cd88 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -24,7 +24,6 @@ import { Bet } from 'common/bet' import { Comment } from 'common/comment' import { ENV_CONFIG } from 'common/envs/constants' import { getBinaryProb } from 'common/contract-details' -import { Sort } from 'web/components/contract-search' export const contracts = coll<Contract>('contracts') @@ -321,51 +320,6 @@ export const getTopGroupContracts = async ( return await getValues<Contract>(creatorContractsQuery) } -const sortToField = { - newest: 'createdTime', - score: 'popularityScore', - 'most-traded': 'volume', - '24-hour-vol': 'volume24Hours', - 'prob-change-day': 'probChanges.day', - 'last-updated': 'lastUpdated', - liquidity: 'totalLiquidity', - 'close-date': 'closeTime', - 'resolve-date': 'resolutionTime', - 'prob-descending': 'prob', - 'prob-ascending': 'prob', -} as const - -const sortToDirection = { - newest: 'desc', - score: 'desc', - 'most-traded': 'desc', - '24-hour-vol': 'desc', - 'prob-change-day': 'desc', - 'last-updated': 'desc', - liquidity: 'desc', - 'close-date': 'asc', - 'resolve-date': 'desc', - 'prob-ascending': 'asc', - 'prob-descending': 'desc', -} as const - -export const getContractsQuery = ( - sort: Sort, - maxItems: number, - filters: { groupSlug?: string } = {}, - visibility?: 'public' -) => { - const { groupSlug } = filters - return query( - contracts, - where('isResolved', '==', false), - ...(visibility ? [where('visibility', '==', visibility)] : []), - ...(groupSlug ? [where('groupSlugs', 'array-contains', groupSlug)] : []), - orderBy(sortToField[sort], sortToDirection[sort]), - limit(maxItems) - ) -} - export const getRecommendedContracts = async ( contract: Contract, excludeBettorId: string, diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 61424b8f..17e41c53 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -43,6 +43,7 @@ export function groupPath( | 'about' | typeof GROUP_CHAT_SLUG | 'leaderboards' + | 'posts' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } diff --git a/web/lib/firebase/posts.ts b/web/lib/firebase/posts.ts index 162933af..36007048 100644 --- a/web/lib/firebase/posts.ts +++ b/web/lib/firebase/posts.ts @@ -39,3 +39,8 @@ export function listenForPost( ) { return listenForValue(doc(posts, postId), setPost) } + +export async function listPosts(postIds?: string[]) { + if (postIds === undefined) return [] + return Promise.all(postIds.map(getPost)) +} diff --git a/web/next.config.js b/web/next.config.js index 21b375ba..cf727fd4 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -9,9 +9,6 @@ module.exports = { reactStrictMode: true, optimizeFonts: false, experimental: { - images: { - allowFutureImage: true, - }, scrollRestoration: true, externalDir: true, modularizeImports: { diff --git a/web/package.json b/web/package.json index 6ee29183..3ccbc96c 100644 --- a/web/package.json +++ b/web/package.json @@ -23,9 +23,9 @@ "@floating-ui/react-dom-interactions": "0.9.2", "@headlessui/react": "1.6.1", "@heroicons/react": "1.0.5", - "@nivo/core": "0.74.0", - "@nivo/line": "0.74.0", - "@nivo/tooltip": "0.74.0", + "@nivo/core": "0.80.0", + "@nivo/line": "0.80.0", + "@nivo/tooltip": "0.80.0", "@react-query-firebase/firestore": "0.4.2", "@tiptap/core": "2.0.0-beta.182", "@tiptap/extension-character-count": "2.0.0-beta.31", @@ -58,6 +58,7 @@ "react-instantsearch-hooks-web": "6.24.1", "react-query": "3.39.0", "react-twitter-embed": "4.0.4", + "react-masonry-css": "1.0.16", "string-similarity": "^4.0.4", "tippy.js": "6.3.7" }, diff --git a/web/pages/create-post.tsx b/web/pages/create-post.tsx deleted file mode 100644 index 01147cc0..00000000 --- a/web/pages/create-post.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react' -import { Spacer } from 'web/components/layout/spacer' -import { Page } from 'web/components/page' -import { Title } from 'web/components/title' -import Textarea from 'react-expanding-textarea' - -import { TextEditor, useTextEditor } from 'web/components/editor' -import { createPost } from 'web/lib/firebase/api' -import clsx from 'clsx' -import Router from 'next/router' -import { MAX_POST_TITLE_LENGTH } from 'common/post' -import { postPath } from 'web/lib/firebase/posts' - -export default function CreatePost() { - const [title, setTitle] = useState('') - const [error, setError] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - const { editor, upload } = useTextEditor({ - disabled: isSubmitting, - }) - - const isValid = editor && title.length > 0 && editor.isEmpty === false - - async function savePost(title: string) { - if (!editor) return - const newPost = { - title: title, - content: editor.getJSON(), - } - - const result = await createPost(newPost).catch((e) => { - console.log(e) - setError('There was an error creating the post, please try again') - return e - }) - if (result.post) { - await Router.push(postPath(result.post.slug)) - } - } - - return ( - <Page> - <div className="mx-auto w-full max-w-3xl"> - <div className="rounded-lg px-6 py-4 sm:py-0"> - <Title className="!mt-0" text="Create a post" /> - <form> - <div className="form-control w-full"> - <label className="label"> - <span className="mb-1"> - Title<span className={'text-red-700'}> *</span> - </span> - </label> - <Textarea - placeholder="e.g. Elon Mania Post" - className="input input-bordered resize-none" - autoFocus - maxLength={MAX_POST_TITLE_LENGTH} - value={title} - onChange={(e) => setTitle(e.target.value || '')} - /> - <Spacer h={6} /> - <label className="label"> - <span className="mb-1"> - Content<span className={'text-red-700'}> *</span> - </span> - </label> - <TextEditor editor={editor} upload={upload} /> - <Spacer h={6} /> - - <button - type="submit" - className={clsx( - 'btn btn-primary normal-case', - isSubmitting && 'loading disabled' - )} - disabled={isSubmitting || !isValid || upload.isLoading} - onClick={async () => { - setIsSubmitting(true) - await savePost(title) - setIsSubmitting(false) - }} - > - {isSubmitting ? 'Creating...' : 'Create a post'} - </button> - {error !== '' && <div className="text-red-700">{error}</div>} - </div> - </form> - </div> - </div> - </Page> - ) -} diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index f06247cd..a23ce602 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -16,7 +16,7 @@ import { import { Row } from 'web/components/layout/row' import { firebaseLogin, getUser, User } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' -import { useUser } from 'web/hooks/use-user' +import { useUser, useUserById } from 'web/hooks/use-user' import { useGroup, useGroupContractIds, @@ -42,10 +42,10 @@ import { GroupComment } from 'common/comment' import { REFERRAL_AMOUNT } from 'common/economy' import { UserLink } from 'web/components/user-link' import { GroupAboutPost } from 'web/components/groups/group-about-post' -import { getPost } from 'web/lib/firebase/posts' +import { getPost, listPosts, postPath } from 'web/lib/firebase/posts' import { Post } from 'common/post' import { Spacer } from 'web/components/layout/spacer' -import { usePost } from 'web/hooks/use-post' +import { usePost, usePosts } from 'web/hooks/use-post' import { useAdmin } from 'web/hooks/use-admin' import { track } from '@amplitude/analytics-browser' import { ArrowLeftIcon } from '@heroicons/react/solid' @@ -53,6 +53,10 @@ import { SelectMarketsModal } from 'web/components/contract-select-modal' import { BETTORS } from 'common/user' import { Page } from 'web/components/page' import { Tabs } from 'web/components/layout/tabs' +import { Avatar } from 'web/components/avatar' +import { Title } from 'web/components/title' +import { fromNow } from 'web/lib/util/time' +import { CreatePost } from 'web/components/create-post' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -70,7 +74,8 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { ? 'all' : 'open' const aboutPost = - group && group.aboutPostId != null && (await getPost(group.aboutPostId)) + group && group.aboutPostId != null ? await getPost(group.aboutPostId) : null + const messages = group && (await listAllCommentsOnGroup(group.id)) const cachedTopTraderIds = @@ -83,6 +88,9 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const creator = await creatorPromise + const posts = ((group && (await listPosts(group.postIds))) ?? []).filter( + (p) => p != null + ) as Post[] return { props: { group, @@ -93,6 +101,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { messages, aboutPost, suggestedFilter, + posts, }, revalidate: 60, // regenerate after a minute @@ -107,17 +116,19 @@ const groupSubpages = [ 'markets', 'leaderboards', 'about', + 'posts', ] as const export default function GroupPage(props: { group: Group | null memberIds: string[] - creator: User + creator: User | null topTraders: { user: User; score: number }[] topCreators: { user: User; score: number }[] messages: GroupComment[] - aboutPost: Post + aboutPost: Post | null suggestedFilter: 'open' | 'all' + posts: Post[] }) { props = usePropz(props, getStaticPropz) ?? { group: null, @@ -127,26 +138,36 @@ export default function GroupPage(props: { topCreators: [], messages: [], suggestedFilter: 'open', + posts: [], } - const { creator, topTraders, topCreators, suggestedFilter } = props + const { creator, topTraders, topCreators, suggestedFilter, posts } = props const router = useRouter() const { slugs } = router.query as { slugs: string[] } const page = slugs?.[1] as typeof groupSubpages[number] + const tabIndex = ['markets', 'leaderboard', 'about', 'posts'].indexOf( + page ?? 'markets' + ) const group = useGroup(props.group?.id) ?? props.group const aboutPost = usePost(props.aboutPost?.id) ?? props.aboutPost + let groupPosts = usePosts(group?.postIds ?? []) ?? posts + + if (aboutPost != null) { + groupPosts = [aboutPost, ...groupPosts] + } + const user = useUser() const isAdmin = useAdmin() const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds useSaveReferral(user, { - defaultReferrerUsername: creator.username, + defaultReferrerUsername: creator?.username, groupId: group?.id, }) - if (group === null || !groupSubpages.includes(page) || slugs[2]) { + if (group === null || !groupSubpages.includes(page) || slugs[2] || !creator) { return <Custom404 /> } const isCreator = user && group && user.id === group.creatorId @@ -172,6 +193,16 @@ export default function GroupPage(props: { </Col> ) + const postsPage = ( + <> + <Col> + <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> + {posts && <GroupPosts posts={groupPosts} group={group} />} + </div> + </Col> + </> + ) + const aboutTab = ( <Col> {(group.aboutPostId != null || isCreator || isAdmin) && ( @@ -234,6 +265,10 @@ export default function GroupPage(props: { title: 'About', content: aboutTab, }, + { + title: 'Posts', + content: postsPage, + }, ] return ( @@ -245,7 +280,8 @@ export default function GroupPage(props: { /> <TopGroupNavBar group={group} /> <div className={'relative p-2 pt-0 md:pt-2'}> - <Tabs className={'mb-2'} tabs={tabs} /> + {/* TODO: Switching tabs should also update the group path */} + <Tabs className={'mb-2'} tabs={tabs} defaultIndex={tabIndex} /> </div> </Page> ) @@ -413,6 +449,84 @@ function GroupLeaderboard(props: { ) } +function GroupPosts(props: { posts: Post[]; group: Group }) { + const { posts, group } = props + const [showCreatePost, setShowCreatePost] = useState(false) + const user = useUser() + + const createPost = <CreatePost group={group} /> + + const postList = ( + <div className=" align-start w-full items-start"> + <Row className="flex justify-between"> + <Col> + <Title text={'Posts'} className="!mt-0" /> + </Col> + <Col> + {user && ( + <Button + className="btn-md" + onClick={() => setShowCreatePost(!showCreatePost)} + > + Add a Post + </Button> + )} + </Col> + </Row> + + <div className="mt-2"> + {posts.map((post) => ( + <PostCard key={post.id} post={post} /> + ))} + {posts.length === 0 && ( + <div className="text-center text-gray-500">No posts yet</div> + )} + </div> + </div> + ) + + return showCreatePost ? createPost : postList +} + +function PostCard(props: { post: Post }) { + const { post } = props + const creatorId = post.creatorId + + const user = useUserById(creatorId) + + if (!user) return <> </> + + return ( + <div className="py-1"> + <Link href={postPath(post.slug)}> + <Row + className={ + 'relative gap-3 rounded-lg bg-white p-2 shadow-md hover:cursor-pointer hover:bg-gray-100' + } + > + <div className="flex-shrink-0"> + <Avatar className="h-12 w-12" username={user?.username} /> + </div> + <div className=""> + <div className="text-sm text-gray-500"> + <UserLink + className="text-neutral" + name={user?.name} + username={user?.username} + /> + <span className="mx-1">•</span> + <span className="text-gray-500">{fromNow(post.createdTime)}</span> + </div> + <div className="text-lg font-medium text-gray-900"> + {post.title} + </div> + </div> + </Row> + </Link> + </div> + ) +} + function AddContractButton(props: { group: Group; user: User }) { const { group, user } = props const [open, setOpen] = useState(false) diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 49d99d18..d5c73913 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -102,6 +102,32 @@ export default function Groups(props: { className="mb-4" currentPageForAnalytics={'groups'} tabs={[ + { + title: 'All', + content: ( + <Col> + <input + type="text" + onChange={(e) => debouncedQuery(e.target.value)} + placeholder="Search groups" + value={query} + className="input input-bordered mb-4 w-full" + /> + + <div className="flex flex-wrap justify-center gap-4"> + {matchesOrderedByMostContractAndMembers.map((group) => ( + <GroupCard + key={group.id} + group={group} + creator={creatorsDict[group.creatorId]} + user={user} + isMember={memberGroupIds.includes(group.id)} + /> + ))} + </div> + </Col> + ), + }, ...(user ? [ { @@ -136,32 +162,6 @@ export default function Groups(props: { }, ] : []), - { - title: 'All', - content: ( - <Col> - <input - type="text" - onChange={(e) => debouncedQuery(e.target.value)} - placeholder="Search groups" - value={query} - className="input input-bordered mb-4 w-full" - /> - - <div className="flex flex-wrap justify-center gap-4"> - {matchesOrderedByMostContractAndMembers.map((group) => ( - <GroupCard - key={group.id} - group={group} - creator={creatorsDict[group.creatorId]} - user={user} - isMember={memberGroupIds.includes(group.id)} - /> - ))} - </div> - </Col> - ), - }, ]} /> </Col> diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index d9f83351..ba2851bf 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -45,6 +45,7 @@ import { Title } from 'web/components/title' import { CPMMBinaryContract } from 'common/contract' import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts' import { ProfitBadge } from 'web/components/profit-badge' +import { LoadingIndicator } from 'web/components/loading-indicator' export default function Home() { const user = useUser() @@ -54,6 +55,13 @@ export default function Home() { useSaveReferral() usePrefetch(user?.id) + useEffect(() => { + if (user === null) { + // Go to landing page if not logged in. + Router.push('/') + } + }) + const groups = useMemberGroupsSubscription(user) const { sections } = getHomeItems(groups ?? [], user?.homeSections ?? []) @@ -82,13 +90,17 @@ export default function Home() { <DailyStats user={user} /> </Row> - <> - {sections.map((section) => - renderSection(section, user, groups, groupContracts) - )} + {!user ? ( + <LoadingIndicator /> + ) : ( + <> + {sections.map((section) => + renderSection(section, user, groups, groupContracts) + )} - <TrendingGroupsSection user={user} /> - </> + <TrendingGroupsSection user={user} /> + </> + )} </Col> <button type="button" @@ -106,9 +118,9 @@ export default function Home() { const HOME_SECTIONS = [ { label: 'Daily movers', id: 'daily-movers' }, + { label: 'Daily trending', id: 'daily-trending' }, { label: 'Trending', id: 'score' }, { label: 'New', id: 'newest' }, - { label: 'Recently updated', id: 'recently-updated-for-you' }, ] export const getHomeItems = (groups: Group[], sections: string[]) => { @@ -127,6 +139,10 @@ export const getHomeItems = (groups: Group[], sections: string[]) => { const sectionItems = filterDefined(sections.map((id) => itemsById[id])) + // Add new home section items to the top. + sectionItems.unshift( + ...HOME_SECTIONS.filter((item) => !sectionItems.includes(item)) + ) // Add unmentioned items to the end. sectionItems.push(...items.filter((item) => !sectionItems.includes(item))) @@ -138,20 +154,20 @@ export const getHomeItems = (groups: Group[], sections: string[]) => { function renderSection( section: { id: string; label: string }, - user: User | null | undefined, + user: User, groups: Group[] | undefined, groupContracts: Dictionary<CPMMBinaryContract[]> | undefined ) { const { id, label } = section if (id === 'daily-movers') { - return <DailyMoversSection key={id} userId={user?.id} /> + return <DailyMoversSection key={id} userId={user.id} /> } - if (id === 'recently-updated-for-you') + if (id === 'daily-trending') return ( <SearchSection key={id} label={label} - sort={'last-updated'} + sort={'daily-score'} pill="personal" user={user} /> @@ -210,7 +226,7 @@ function SectionHeader(props: { function SearchSection(props: { label: string - user: User | null | undefined | undefined + user: User sort: Sort pill?: string }) { @@ -236,7 +252,7 @@ function SearchSection(props: { function GroupSection(props: { group: Group - user: User | null | undefined | undefined + user: User contracts: CPMMBinaryContract[] }) { const { group, user, contracts } = props @@ -247,18 +263,16 @@ function GroupSection(props: { <Button color="gray-white" onClick={() => { - if (user) { - const homeSections = (user.homeSections ?? []).filter( - (id) => id !== group.id - ) - updateUser(user.id, { homeSections }) + const homeSections = (user.homeSections ?? []).filter( + (id) => id !== group.id + ) + updateUser(user.id, { homeSections }) - toast.promise(leaveGroup(group, user.id), { - loading: 'Unfollowing group...', - success: `Unfollowed ${group.name}`, - error: "Couldn't unfollow group, try again?", - }) - } + toast.promise(leaveGroup(group, user.id), { + loading: 'Unfollowing group...', + success: `Unfollowed ${group.name}`, + error: "Couldn't unfollow group, try again?", + }) }} > <XCircleIcon className={'h-5 w-5 flex-shrink-0'} aria-hidden="true" /> diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 907157f9..4f9700dd 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -971,6 +971,8 @@ function ContractResolvedNotification(props: { const { sourceText, data } = notification const { userInvestment, userPayout } = (data as ContractResolutionData) ?? {} const subtitle = 'resolved the market' + const profitable = userPayout >= userInvestment + const ROI = (userPayout - userInvestment) / userInvestment const resolutionDescription = () => { if (!sourceText) return <div /> @@ -1002,23 +1004,21 @@ function ContractResolvedNotification(props: { const description = userInvestment && userPayout !== undefined ? ( - <Row className={'gap-1 '}> - Resolved: {resolutionDescription()} - Invested: + <> + Resolved: {resolutionDescription()} Invested: <span className={'text-primary'}>{formatMoney(userInvestment)} </span> Payout: <span className={clsx( - userPayout > 0 ? 'text-primary' : 'text-red-500', - 'truncate' + profitable ? 'text-primary' : 'text-red-500', + 'truncate text-ellipsis' )} > {formatMoney(userPayout)} - {` (${userPayout > 0 ? '+' : ''}${Math.round( - ((userPayout - userInvestment) / userInvestment) * 100 - )}%)`} + {userPayout > 0 && + ` (${profitable ? '+' : ''}${Math.round(ROI * 100)}%)`} </span> - </Row> + </> ) : ( <span>Resolved {resolutionDescription()}</span> ) @@ -1038,9 +1038,7 @@ function ContractResolvedNotification(props: { highlighted={highlighted} subtitle={subtitle} > - <Row> - <span>{description}</span> - </Row> + <Row className={'line-clamp-2 space-x-1'}>{description}</Row> </NotificationFrame> ) } diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 2c095db6..caa9f47a 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -13,7 +13,6 @@ import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { SiteLink } from 'web/components/site-link' import { Title } from 'web/components/title' -import { defaultBannerUrl } from 'web/components/user-page' import { generateNewApiKey } from 'web/lib/api/api-key' import { changeUserInfo } from 'web/lib/firebase/api' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' @@ -176,27 +175,6 @@ export default function ProfilePage(props: { onBlur={updateUsername} /> </div> - - {/* TODO: Allow users with M$ 2000 of assets to set custom banners */} - {/* <EditUserField - user={user} - field="bannerUrl" - label="Banner Url" - isEditing={isEditing} - /> */} - <label className="label"> - Banner image{' '} - <span className="text-sm text-gray-400">Not editable for now</span> - </label> - <div - className="h-32 w-full bg-cover bg-center sm:h-40" - style={{ - backgroundImage: `url(${ - user.bannerUrl || defaultBannerUrl(user.id) - })`, - }} - /> - {( [ ['bio', 'Bio'], diff --git a/web/pages/tournaments/index.tsx b/web/pages/tournaments/index.tsx index 8378b185..0b9dbc80 100644 --- a/web/pages/tournaments/index.tsx +++ b/web/pages/tournaments/index.tsx @@ -107,7 +107,14 @@ const tourneys: Tourney[] = [ groupId: 'SxGRqXRpV3RAQKudbcNb', }, - // Tournaments without awards get featured belows + // Tournaments without awards get featured below + { + title: 'Criticism and Red Teaming Contest', + blurb: + 'Which criticisms of Effective Altruism have been the most valuable?', + endTime: toDate('Sep 30, 2022'), + groupId: 'K86LmEmidMKdyCHdHNv4', + }, { title: 'SF 2022 Ballot', blurb: 'Which ballot initiatives will pass this year in SF and CA?', diff --git a/web/pages/twitch.tsx b/web/pages/twitch.tsx index 22d3152d..6508a69e 100644 --- a/web/pages/twitch.tsx +++ b/web/pages/twitch.tsx @@ -146,7 +146,8 @@ function TwitchPlaysManifoldMarkets(props: { <div> Instead of Twitch channel points we use our own play money, mana (M$). All viewers start with M$1,000 and can earn more for free by betting - well. + well. Just like channel points, mana cannot be converted to real + money. </div> </Col> </div> @@ -176,35 +177,47 @@ function TwitchChatCommands() { <Col className="gap-4"> <Subtitle text="For Chat" /> <Command - command="bet yes #" - desc="Bets an amount of M$ on yes, for example !bet yes 20" + command="y#" + desc="Bets # amount of M$ on yes, for example !y20 would bet M$20 on yes." + /> + <Command + command="n#" + desc="Bets # amount of M$ on no, for example !n30 would bet M$30 on no." /> - <Command command="bet no #" desc="Bets an amount of M$ on no." /> <Command command="sell" desc="Sells all shares you own. Using this command causes you to - cash out early before the market resolves. This could be profitable - (if the probability has moved towards the direction you bet) or cause - a loss, although at least you keep some mana. For maximum profit (but - also risk) it is better to not sell and wait for a favourable - resolution." + cash out early based on the current probability. + Shares will always be worth the most if you wait for a favourable resolution. But, selling allows you to lower risk, or trade throughout the event which can maximise earnings." /> - <Command command="balance" desc="Shows how much M$ you have." /> - <Command command="allin yes" desc="Bets your entire balance on yes." /> - <Command command="allin no" desc="Bets your entire balance on no." /> + <Command + command="position" + desc="Shows how many shares you own in the current market and what your fixed payout is." + /> + <Command command="balance" desc="Shows how much M$ your account has." /> <div className="mb-4" /> <Subtitle text="For Mods/Streamer" /> + + <div> + We recommend streamers sharing the link to the control dock with their + mods. Alternatively, chat commands can be used to control markets.{' '} + </div> + <Command - command="create <question>" - desc="Creates and features the question. Be careful... this will override any question that is currently featured." + command="create [question]" + desc="Creates and features a question. Be careful, this will replace any question that is currently featured." /> <Command command="resolve yes" desc="Resolves the market as 'Yes'." /> <Command command="resolve no" desc="Resolves the market as 'No'." /> <Command - command="resolve n/a" - desc="Resolves the market as 'N/A' and refunds everyone their mana." + command="resolve na" + desc="Cancels the market and refunds everyone their mana." + /> + <Command + command="unfeature" + desc="Unfeatures the market. The market will still be open on our site and available to be refeatured again. If you plan to never interact with a market again we recommend resolving to N/A and not this command." /> </Col> </div> @@ -384,8 +397,8 @@ function SetUpBot(props: { buttonOnClick={copyOverlayLink} > Create a new browser source in your streaming software such as OBS. - Paste in the above link and resize it to your liking. We recommend - setting the size to 400x400. + Paste in the above link and type in the desired size. We recommend + 450x375. </BotSetupStep> <BotSetupStep stepNum={3} @@ -397,6 +410,10 @@ function SetUpBot(props: { your OBS as a custom dock. </BotSetupStep> </div> + <div> + Need help? Contact SirSalty#5770 in Discord or email + david@manifold.markets + </div> </Col> </> ) diff --git a/web/public/praying-mantis-light.svg b/web/public/praying-mantis-light.svg deleted file mode 100644 index cc82cd53..00000000 --- a/web/public/praying-mantis-light.svg +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - version="1.1" - id="svg2" - xml:space="preserve" - width="2829.3333" - height="2829.3333" - viewBox="0 0 2829.3333 2829.3333" - sodipodi:docname="shutterstock_2073140717.eps"><metadata - id="metadata8"><rdf:RDF><cc:Work - rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs - id="defs6" /><sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="640" - inkscape:window-height="480" - id="namedview4" /><g - id="g10" - inkscape:groupmode="layer" - inkscape:label="ink_ext_XXXXXX" - transform="matrix(1.3333333,0,0,-1.3333333,0,2829.3333)"><g - id="g12" - transform="scale(0.1)"><path - d="m 10622.5,11248.2 c -81.6,-54.3 -178.4,-122.5 -292.3,-197.1 -114.1,-74.3 -241.9,-159.2 -384.07,-247.8 -140.7,-90.6 -296.56,-183.4 -460.04,-283.5 -81.17,-50.9 -167.57,-98.4 -254.29,-149.2 -87.66,-49.5 -174.65,-104.3 -266.68,-153.9 -91.29,-50.7 -184.14,-102.4 -278.09,-154.6 -93.94,-52.3 -191.33,-100.9 -287.65,-153.3 -192.27,-105.6 -393.64,-201 -592.62,-301 -99.96,-49 -201.33,-94.9 -301.6,-142.3 -100.32,-47.3 -200.08,-94.8 -301.41,-136.4 -100.82,-42.9 -200.55,-86.1 -298.87,-129.4 -98.47,-42.8 -198.16,-79 -294.96,-118 -97.38,-37.3 -191.91,-77.8 -286.76,-110.5 -94.68,-33.6 -187.3,-66.4 -277.42,-98.3 -45.11,-15.9 -89.61,-31.7 -133.44,-47.3 -44.37,-13.7 -88.12,-27.1 -131.09,-40.4 -86.05,-26.7 -169.14,-52.4 -248.91,-77.2 -39.96,-12.3 -79.06,-24.4 -117.3,-36.2 -38.71,-9.8 -76.56,-19.5 -113.44,-28.9 -73.75,-19.2 -143.71,-37.5 -209.45,-54.6 -65.9,-16.6 -127.23,-34.6 -184.73,-46.9 -57.61,-11.8 -110.58,-22.6 -158.43,-32.4 -191.41,-40.4 -300.86,-63.6 -301.14,-63.6 0.2,0 111.49,12.4 306.1,34.2 48.79,5.8 102.77,12.2 161.6,19.2 58.83,7.2 121.13,21.7 188.51,33.5 67.27,12.6 138.87,26.1 214.34,40.3 37.73,7.3 76.44,14.7 116.05,22.4 39.11,9.8 79.11,19.8 119.97,29.9 81.75,20.6 166.87,42 255.03,64.2 44.07,11.2 88.95,22.5 134.18,35 44.77,13.9 90.28,28.1 136.37,42.4 92.31,28.7 187.07,58.2 283.99,88.3 48.78,14.2 96.99,31.6 145.43,49.7 48.63,17.8 97.69,35.8 147.18,53.9 98.64,37.1 200.35,71.4 300.78,112.2 100.47,40.8 201.92,82.9 303.87,125.8 102.74,41.4 203.75,88.5 305.43,135.4 101.49,47.1 204.38,92.5 305.51,141.4 200,102.5 402.34,200.4 595.35,308.4 195.7,103 382.34,214.9 563.79,321.6 91.25,52.3 178.28,108.5 264.06,162.3 85.31,54.6 170.2,105.7 249.92,160 80.24,53.6 158.25,105.6 233.75,155.9 75.39,50.4 145.9,102.7 214.81,150.9 138.27,95.7 260.57,190 369.47,272.5 109.4,81.8 200.6,159.2 277.3,220.1 76,61.8 132.8,113.9 172.9,147.7 39.7,34.4 60.8,52.8 60.8,52.8 0,0 -91.6,-64.5 -251.8,-177.2" - style="fill:#668a29;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path14" /><path - d="m 15228.8,15825 c -102.2,-173.3 -176.3,-358 -218.3,-550.5 -19.6,-17.6 -37.4,-37 -53.4,-58 -163.6,59.3 -342.5,63.1 -484.2,31 54.1,715.2 441.9,1360.2 1047.5,1839.3 653.8,517 1560.1,838.1 2563.2,838 v 99.8 c -1024.4,-0.1 -1951.8,-327.4 -2625.1,-859.5 -610.2,-482.2 -1010.9,-1135.1 -1080.6,-1863.1 24.7,-17.7 47.3,-38.1 67.4,-60.7 64.3,-72.4 103.6,-167.4 103.6,-271.7 0,-169.6 -103,-315.2 -250,-377.3 -49.1,-20.7 -102.9,-32.2 -159.4,-32.2 -188.5,0 -346.9,127.6 -394.5,301 -361.9,-310.9 -784.7,-754 -1230.9,-1104.8 -123.3,-97 -259.3,-238.4 -399.4,-403.4 -69.3,-51.5 -131.7,-122.5 -178.1,-219.4 -456.2,-583.6 -916.7,-1325.1 -1094.3,-1547.3 -457.6,-216.4 -911.4,-311.1 -1384.14,-404.1 -205.43,-40.3 -414.41,-80.5 -628.86,-130 -130.39,-30.2 -262.89,-64.1 -397.74,-103.4 -167.58,-48.9 -338.83,-106.5 -514.76,-177.3 -199.85,-80.5 -405.55,-177.8 -618.36,-298.2 -39.92,-22.7 -78.24,-45.2 -117.42,-67.8 -174.61,-100.7 -341.76,-201.1 -501.61,-300.9 C 6201.64,9806 5787.19,9513.4 5425,9237.8 c -215.78,-164.2 -413.55,-322.4 -595.2,-472.1 -135.85,-111.9 -262.73,-218.9 -381.99,-320.3 0.28,0 109.73,23.2 301.14,63.6 47.85,9.8 100.82,20.6 158.43,32.4 57.5,12.3 118.83,30.3 184.73,46.9 65.74,17.1 135.7,35.4 209.45,54.6 36.88,9.4 74.73,19.1 113.44,28.9 38.24,11.8 77.34,23.9 117.3,36.2 79.77,24.8 162.86,50.5 248.91,77.2 42.97,13.3 86.72,26.7 131.09,40.4 43.83,15.6 88.33,31.4 133.44,47.3 90.12,31.9 182.74,64.7 277.42,98.3 94.85,32.7 189.38,73.2 286.76,110.5 96.8,39 196.49,75.2 294.96,118 98.32,43.3 198.05,86.5 298.87,129.4 101.33,41.6 201.09,89.1 301.41,136.4 100.27,47.4 201.64,93.3 301.6,142.3 198.98,100 400.35,195.4 592.62,301 96.32,52.4 193.71,101 287.65,153.3 93.95,52.2 186.8,103.9 278.09,154.6 92.03,49.6 179.02,104.4 266.68,153.9 86.72,50.8 173.12,98.3 254.29,149.2 163.48,100.1 319.34,192.9 460.04,283.5 142.17,88.6 269.97,173.5 384.07,247.8 113.9,74.6 210.7,142.8 292.3,197.1 160.2,112.7 251.8,177.2 251.8,177.2 0,0 -21.1,-18.4 -60.8,-52.8 -40.1,-33.8 -96.9,-85.9 -172.9,-147.7 -76.7,-60.9 -167.9,-138.3 -277.3,-220.1 -108.9,-82.5 -231.2,-176.8 -369.47,-272.5 -68.91,-48.2 -139.42,-100.5 -214.81,-150.9 -75.5,-50.3 -153.51,-102.3 -233.75,-155.9 -79.72,-54.3 -164.61,-105.4 -249.92,-160 -85.78,-53.8 -172.81,-110 -264.06,-162.3 -181.45,-106.7 -368.09,-218.6 -563.79,-321.6 -193.01,-108 -395.35,-205.9 -595.35,-308.4 -101.13,-48.9 -204.02,-94.3 -305.51,-141.4 -101.68,-46.9 -202.69,-94 -305.43,-135.4 -101.95,-42.9 -203.4,-85 -303.87,-125.8 -100.43,-40.8 -202.14,-75.1 -300.78,-112.2 -49.49,-18.1 -98.55,-36.1 -147.18,-53.9 -48.44,-18.1 -96.65,-35.5 -145.43,-49.7 -96.92,-30.1 -191.68,-59.6 -283.99,-88.3 -46.09,-14.3 -91.6,-28.5 -136.37,-42.4 -45.23,-12.5 -90.11,-23.8 -134.18,-35 -88.16,-22.2 -173.28,-43.6 -255.03,-64.2 -40.86,-10.1 -80.86,-20.1 -119.97,-29.9 -39.61,-7.7 -78.32,-15.1 -116.05,-22.4 -75.47,-14.2 -147.07,-27.7 -214.34,-40.3 -67.38,-11.8 -129.68,-26.3 -188.51,-33.5 -58.83,-7 -112.81,-13.4 -161.6,-19.2 -194.61,-21.8 -305.9,-34.2 -306.1,-34.2 -663.4,-564.2 -1086.87,-950 -1454.8,-983.5 72.89,-128.8 730.04,15.1 1664.29,339.7 141.8,49.3 289.85,102.7 443.36,160 51.41,19.1 103.44,38.8 156.02,58.8 52.23,-252.7 180.66,-592.2 394.45,-911.7 142.27,-212.7 291.06,-397.8 427.27,-538.4 l -3.79,-2.3 C 5872.3,6432.5 3914.65,3342.3 4005.23,3206.8 c 90.63,-135.4 2195.32,2735.1 2397.7,2870.4 202.34,135.4 292.97,354.9 202.3,490.5 -12.93,19.3 -29.18,35.6 -47.61,49.4 -63.56,215.9 -210.74,513.2 -415.86,819.8 -123.63,184.9 -164.26,536.4 -199.73,855.9 569.34,235 1177.93,507.5 1784.73,805.2 87.85,-408.4 251.83,-803.5 371.33,-959.7 97.34,-159.8 600.11,-435.7 1207.54,-515.4 253.75,-33.3 490.97,-43.5 686.6,-33.8 l -0.75,-4.3 c -31.64,-241.4 1000.52,-3751 1162.12,-3772.2 161.7,-21.2 -608.5,3453.9 -576.8,3695.4 31.7,241.3 -73.7,454.2 -235.3,475.4 -23.1,3.1 -46,1.6 -68.6,-3.1 -204.1,94.5 -524.66,179.9 -890.44,227.9 -242.62,31.8 -570.78,272.4 -853.59,468.9 40.08,153.4 74.26,327.7 98.67,514 17.73,135 28.87,265.3 33.98,387.3 141.41,76.3 281.53,153.6 419.73,231.8 190.35,107.7 373.91,215.7 551.95,323.5 148.87,90.2 293.4,180.2 433.4,269.7 798.4,510.8 1451.5,1007.5 1939,1436.7 86.8,-272.8 194.7,-506.2 280,-617.7 97.3,-159.7 600.1,-435.6 1207.6,-515.3 253.7,-33.4 490.9,-43.5 686.6,-33.8 l -0.8,-4.4 c -31.7,-241.4 1000.5,-3750.9 1162.2,-3772.1 161.6,-21.2 -608.6,3453.9 -576.9,3695.3 31.7,241.4 -73.7,454.3 -235.3,475.5 -23.1,3 -46,1.5 -68.5,-3.2 -204.2,94.5 -524.8,179.9 -890.5,227.9 -242.6,31.9 -570.8,272.5 -853.6,469 40.1,153.4 74.2,327.7 98.7,513.9 17.4,132.5 28.4,260.4 33.7,380.4 0.9,20.9 1.6,41.5 2.2,61.9 102.2,140.7 160.6,254.7 171.7,334.9 146.6,101.3 290.1,206.2 425.5,312.6 94.5,74.3 186.3,144.3 274.9,210.9 323.5,243.3 601.7,440.3 795.1,626.5 239.4,-336.9 587.3,-500.1 789.4,-364.5 144.8,97.1 192.2,434.7 126.7,751.4 151.3,60.1 258.4,207.6 258.4,380.3 0,226.1 -183.3,409.5 -409.5,409.5 -53.4,0 -104.3,-10.6 -151.1,-29.2 41.3,147 102.8,289 182.7,424.4 236.5,401.5 634.1,745.9 1137.5,989.6 503.4,243.7 1111.9,386.5 1767.9,386.5 v 99.8 c -893.9,-0.1 -1703.5,-259 -2292.2,-680.9 -294.2,-210.9 -533.4,-462.9 -699.2,-744.3" - style="fill:#bfe142;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path16" /><path - d="m 14096.4,15241.1 c -14.7,4.9 -30.1,8.2 -46.6,8.2 -81.6,0 -147.8,-66.2 -147.8,-148 0,-4.7 0.9,-9.3 1.4,-14 7.1,-75 69.6,-133.8 146.4,-133.8 81.7,0 148,66.2 148,147.8 0,65.3 -42.6,120.1 -101.4,139.8" - style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path18" /><path - d="m 14139.5,14802.3 c -153.4,0 -277.7,222.5 -277.7,222.5 0,0 15.3,27.3 41.6,62.5 -0.5,4.7 -1.4,9.3 -1.4,14 0,81.8 66.2,148 147.8,148 16.5,0 31.9,-3.3 46.6,-8.2 14.1,3.6 28.4,6.2 43.1,6.2 153.3,0 277.6,-222.5 277.6,-222.5 0,0 -124.3,-222.5 -277.6,-222.5 z m 238.4,499.7 c -67.2,48.3 -149.3,77.1 -238.4,77.1 -226.1,0 -409.5,-183.4 -409.5,-409.5 0,-37.7 5.5,-73.9 15,-108.5 47.6,-173.4 206,-301 394.5,-301 56.5,0 110.3,11.5 159.4,32.2 147,62.1 250,207.7 250,377.3 0,104.3 -39.3,199.3 -103.6,271.7 -20.1,22.6 -42.7,43 -67.4,60.7" - style="fill:#d4e61d;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path20" /><path - d="m 14139.5,15247.3 c -14.7,0 -29,-2.6 -43.1,-6.2 58.8,-19.7 101.4,-74.5 101.4,-139.8 0,-81.6 -66.3,-147.8 -148,-147.8 -76.8,0 -139.3,58.8 -146.4,133.8 -26.3,-35.2 -41.6,-62.5 -41.6,-62.5 0,0 124.3,-222.5 277.7,-222.5 153.3,0 277.6,222.5 277.6,222.5 0,0 -124.3,222.5 -277.6,222.5" - style="fill:#293519;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path22" /><path - d="m 5843.09,10913.9 1.01,4.3 c 120.16,-58.3 516.88,-510.3 835.31,-813.7 159.85,99.8 327,200.2 501.61,300.9 -364.5,119.2 -1220.16,989.6 -1442,982.7 -18.82,13.2 -39.37,23.6 -61.83,29.8 -157.03,43.9 -337.35,-110.8 -402.78,-345.3 -27.89,-100 -231.91,-1141.6 -444.61,-2306.9 181.65,149.7 379.42,307.9 595.2,472.1 221.99,884.7 395.16,1594 418.09,1676.1" - style="fill:#96d42f;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path24" /><path - d="m 4332.03,5382.5 c 76.92,-21.4 425.39,1249.4 768.63,2579.1 -153.51,-57.3 -301.56,-110.7 -443.36,-160 C 4437.54,6545 4260.27,5402.5 4332.03,5382.5" - style="fill:#96d42f;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path26" /><path - d="m 8577.89,11337.9 2.89,-3.4 c 23.01,-24.8 112.07,-126.4 248.52,-282.4 214.45,49.5 423.43,89.7 628.86,130 -243.75,309.5 -410.74,517.5 -444.37,553.8 -165.47,178.5 -396.56,233.5 -516.09,122.7 -17.15,-15.9 -30.67,-34.5 -41.37,-54.8 -133.2,-63.4 -338.01,-599.7 -539.53,-1032.4 175.93,70.8 347.18,128.4 514.76,177.3 58.36,189 109.38,339.9 146.33,389.2" - style="fill:#96d42f;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path28" /><path - d="m 12455.6,7107.7 c 78.1,72.5 -1347,1953 -2389,3295.7 -140,-89.5 -284.53,-179.5 -433.4,-269.7 1121.5,-1276.9 2742.7,-3099.8 2822.4,-3026" - style="fill:#96d42f;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path30" /><path - d="m 14364.1,11431.3 c 253.8,-33.3 491,-43.5 686.6,-33.8 l -0.7,-4.4 c -31.7,-241.4 1000.4,-3750.9 1162.1,-3772.1 161.7,-21.2 -608.5,3453.9 -576.8,3695.3 31.7,241.4 -73.7,454.3 -235.3,475.5 -23.1,3.1 -46.1,1.6 -68.6,-3.1 -204.2,94.5 -524.7,179.9 -890.5,227.9 -242.6,31.9 -570.8,272.5 -853.6,469 40.1,153.3 74.3,327.7 98.7,513.9 27.3,207.8 39,404.5 37.1,576.4 -88.6,-66.6 -180.4,-136.6 -274.9,-210.9 -135.4,-106.4 -278.9,-211.3 -425.5,-312.6 -11.1,-80.2 -69.5,-194.2 -171.7,-334.9 -0.6,-20.4 -1.3,-41 -2.2,-61.9 90.5,-311.4 213.1,-585.2 307.7,-709 97.4,-159.7 600.2,-435.6 1207.6,-515.3" - style="fill:#96d42f;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path32" /></g></g></svg> diff --git a/web/public/world-trading-background.webp b/web/public/world-trading-background.webp deleted file mode 100644 index 502beb29..00000000 Binary files a/web/public/world-trading-background.webp and /dev/null differ diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 7bea3ec2..ef7220ec 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -15,10 +15,8 @@ module.exports = { } ), extend: { - backgroundImage: { - 'world-trading': "url('/world-trading-background.webp')", - }, colors: { + 'red-25': '#FDF7F6', 'greyscale-1': '#FBFBFF', 'greyscale-2': '#E7E7F4', 'greyscale-3': '#D8D8EB', diff --git a/yarn.lock b/yarn.lock index 81cf80fa..9829f0b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2553,102 +2553,99 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== -"@nivo/annotations@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.74.0.tgz#f4a3474fdf8812c3812c30e08d3277e209bec0f6" - integrity sha512-nxZLKDi9YEy2zZUsOtbYL/2oAgsxK5SVZ1P3Csll+cQ96uLU6sU7jmb67AwK0nDbYk7BD3sZf/O/A9r/MCK4Ow== +"@nivo/annotations@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.80.0.tgz#127e4801fff7370dcfb9acfe1e335781dd65cfd5" + integrity sha512-bC9z0CLjU07LULTMWsqpjovRtHxP7n8oJjqBQBLmHOGB4IfiLbrryBfu9+aEZH3VN2jXHhdpWUz+HxeZzOzsLg== dependencies: - "@nivo/colors" "0.74.0" - "@react-spring/web" "9.2.6" + "@nivo/colors" "0.80.0" + "@react-spring/web" "9.4.5" lodash "^4.17.21" -"@nivo/axes@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.74.0.tgz#cf7cf2277b7aca5449a040ddf3e0cf9891971199" - integrity sha512-27o1H+Br0AaeUTiRhy7OebqzYEWr1xznHOxd+Hn2Xz9kK1alGBiPgwXrkXV0Q9CtrsroQFnX2QR3JxRgOtC5fA== +"@nivo/axes@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.80.0.tgz#22788855ddc45bb6a619dcd03d62d4bd8c0fc35f" + integrity sha512-AsUyaSHGwQVSEK8QXpsn8X+poZxvakLMYW7crKY1xTGPNw+SU4SSBohPVumm2jMH3fTSLNxLhAjWo71GBJXfdA== dependencies: - "@nivo/scales" "0.74.0" - "@react-spring/web" "9.2.6" + "@nivo/scales" "0.80.0" + "@react-spring/web" "9.4.5" d3-format "^1.4.4" d3-time-format "^3.0.0" -"@nivo/colors@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.74.0.tgz#29d1e7c6f3bcab4e872a168651b3a90cfba03a4f" - integrity sha512-5ClckmBm3x2XdJqHMylr6erY+scEL/twoGVfyXak/L+AIhL+Gf9PQxyxyfl3Lbtc3SPeAQe0ZAO1+VrmTn7qlA== +"@nivo/colors@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.80.0.tgz#5b70b4979df246d9d0d69fb638bba9764dd88b52" + integrity sha512-T695Zr411FU4RPo7WDINOAn8f79DPP10SFJmDdEqELE+cbzYVTpXqLGZ7JMv88ko7EOf9qxLQgcBqY69rp9tHQ== dependencies: d3-color "^2.0.0" d3-scale "^3.2.3" d3-scale-chromatic "^2.0.0" lodash "^4.17.21" - react-motion "^0.5.2" -"@nivo/core@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.74.0.tgz#7634c78a36a8bd50a0c04c6b6f12b779a88ec2f4" - integrity sha512-LZ3kN1PiEW0KU4PTBgaHFO757amyKZkEL4mKdAzvyNQtpq5idB3OhC/sYrBxhJaLqYcX19MgNfhIel/0KygHAg== +"@nivo/core@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.80.0.tgz#d180cb2622158eb7bc5f984131ff07984f12297e" + integrity sha512-6caih0RavXdWWSfde+rC2pk17WrX9YQlqK26BrxIdXzv3Ydzlh5SkrC7dR2TEvMGBhunzVeLOfiC2DWT1S8CFg== dependencies: - "@nivo/recompose" "0.74.0" - "@react-spring/web" "9.2.6" + "@nivo/recompose" "0.80.0" + "@react-spring/web" "9.4.5" d3-color "^2.0.0" d3-format "^1.4.4" - d3-hierarchy "^1.1.8" d3-interpolate "^2.0.1" d3-scale "^3.2.3" d3-scale-chromatic "^2.0.0" d3-shape "^1.3.5" d3-time-format "^3.0.0" lodash "^4.17.21" - resize-observer-polyfill "^1.5.1" -"@nivo/legends@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.74.0.tgz#8e5e04b2a3f980c2073a394d94c4d89fa8bc8724" - integrity sha512-Bfk392ngre1C8UaGoymwqK0acjjzuk0cglUSNsr0z8BAUQIVGUPthtfcxbq/yUYGJL/cxWky2QKxi9r3C0FbmA== +"@nivo/legends@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.80.0.tgz#49edc54000075b4df055f86794a8c32810269d06" + integrity sha512-h0IUIPGygpbKIZZZWIxkkxOw4SO0rqPrqDrykjaoQz4CvL4HtLIUS3YRA4akKOVNZfS5agmImjzvIe0s3RvqlQ== -"@nivo/line@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.74.0.tgz#f1f430d64a81d2fe1a5fd49e5cfaa61242066927" - integrity sha512-uJssLII1UTfxrZkPrkki054LFUpSKeqS35ttwK6VLvyqs5r3SrSXn223vDRNaaxuop5oT/L3APUJQwQDqUcj3w== +"@nivo/line@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.80.0.tgz#ba541b0fcfd53b3a7ce865feb43c993b7cf4a7d4" + integrity sha512-6UAD/y74qq3DDRnVb+QUPvXYojxMtwXMipGSNvCGk8omv1QZNTaUrbV+eQacvn9yh//a0yZcWipnpq0tGJyJCA== dependencies: - "@nivo/annotations" "0.74.0" - "@nivo/axes" "0.74.0" - "@nivo/colors" "0.74.0" - "@nivo/legends" "0.74.0" - "@nivo/scales" "0.74.0" - "@nivo/tooltip" "0.74.0" - "@nivo/voronoi" "0.74.0" - "@react-spring/web" "9.2.6" + "@nivo/annotations" "0.80.0" + "@nivo/axes" "0.80.0" + "@nivo/colors" "0.80.0" + "@nivo/legends" "0.80.0" + "@nivo/scales" "0.80.0" + "@nivo/tooltip" "0.80.0" + "@nivo/voronoi" "0.80.0" + "@react-spring/web" "9.4.5" d3-shape "^1.3.5" -"@nivo/recompose@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.74.0.tgz#057e8e1154073d7f4cb01aa8d165c3914b8bdb54" - integrity sha512-qC9gzGvDIxocrJoozDjqqffOwDpuEZijeMV59KExnztCwIpQbIYVBsDdpvL+tXfWausigSlnGILGfereXJTLUQ== +"@nivo/recompose@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.80.0.tgz#572048aed793321a0bada1fd176b72df5a25282e" + integrity sha512-iL3g7j3nJGD9+mRDbwNwt/IXDXH6E29mhShY1I7SP91xrfusZV9pSFf4EzyYgruNJk/2iqMuaqn+e+TVFra44A== dependencies: react-lifecycles-compat "^3.0.4" -"@nivo/scales@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.74.0.tgz#ede12b899da9e3aee7921ebce40f227e670a430d" - integrity sha512-5mER71NgZGdgs8X2PgilBpAWMMGtTXrUuYOBQWDKDMgtc83MU+mphhiYfLv5e6ViZyUB5ebfEkfeIgStLqrcEA== +"@nivo/scales@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.80.0.tgz#39313fb97c8ae9633c2aa1e17adb57cb851e8a50" + integrity sha512-4y2pQdCg+f3n4TKXC2tYuq71veZM+xPRQbOTgGYJpuBvMc7pQsXF9T5z7ryeIG9hkpXkrlyjecU6XcAG7tLSNg== dependencies: d3-scale "^3.2.3" d3-time "^1.0.11" d3-time-format "^3.0.0" lodash "^4.17.21" -"@nivo/tooltip@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.74.0.tgz#60d94b0fecc2fc179ada3efa380e7e456982b4a5" - integrity sha512-h3PUgNFF5HUeQFfx19MWS1uGK8iUDymZNY+5PyaCWDFT+0/ldXBu8uw5WYRui2KwNdTym6F0E/aT7JKczDd85w== +"@nivo/tooltip@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.80.0.tgz#07ebef47eb708a0612bd6297d5ad156bbec19d34" + integrity sha512-qGmrreRwnCsYjn/LAuwBtxBn/tvG8y+rwgd4gkANLBAoXd3bzJyvmkSe+QJPhUG64bq57ibDK+lO2pC48a3/fw== dependencies: - "@react-spring/web" "9.2.6" + "@react-spring/web" "9.4.5" -"@nivo/voronoi@0.74.0": - version "0.74.0" - resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.74.0.tgz#4b427955ddabd86934a2bbb95a62ff53ee97c575" - integrity sha512-Q3267T1+Tlufn8LbmSYnO8x9gL+h/iwH2Uqc5CENHSZu2KPD0PB82vxpQnbDVhjadulI0rlrPA9fU3VY3q1zKg== +"@nivo/voronoi@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.80.0.tgz#59cc7ed253dc1a5bbcf614a5ac37d2468d561599" + integrity sha512-zaJV3I3cRu1gHpsXCIEvp6GGlGY8P7D9CwAVCjYDGrz3W/+GKN0kA7qGyHTC97zVxJtfefxSPlP/GtOdxac+qw== dependencies: d3-delaunay "^5.3.0" d3-scale "^3.2.3" @@ -2747,50 +2744,51 @@ resolved "https://registry.yarnpkg.com/@react-query-firebase/firestore/-/firestore-0.4.2.tgz#6ae52768715aa0a5c0d903dd4fd953ed417ba635" integrity sha512-7eYp905+sfBRcBTdj7W7BAc3bI3V0D0kKca4/juOTnN4gyoNyaCNOCjLPY467dTq325hGs7BX0ol7Pw3JENdHA== -"@react-spring/animated@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e" - integrity sha512-xjL6nmixYNDvnpTs1FFMsMfSC0tURwPCU3b2jWNriYGLfwZ7c/TcyaEZA7yiNnmdFnuR3f3Z27AqIgaFC083Cw== +"@react-spring/animated@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54" + integrity sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA== dependencies: - "@react-spring/shared" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/core@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.6.tgz#ae22338fe55d070caf03abb4293b5519ba620d93" - integrity sha512-uPHUxmu+w6mHJrfQTMtmGJ8iZEwiVxz9kH7dRyk69bkZJt9z+w0Oj3UF4J3VcECZsbm3HRhN2ogXSAaqGjwhQw== +"@react-spring/core@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.4.5.tgz#4616e1adc18dd10f5731f100ebdbe9518b89ba3c" + integrity sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ== dependencies: - "@react-spring/animated" "~9.2.6-beta.0" - "@react-spring/shared" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" + "@react-spring/animated" "~9.4.5" + "@react-spring/rafz" "~9.4.5" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/rafz@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.6.tgz#d97484003875bf5fb5e6ec22dee97cc208363e48" - integrity sha512-62SivLKEpo7EfHPkxO5J3g9Cr9LF6+1A1RVOMJhkcpEYtbdbmma/d63Xp8qpMPEpk7uuWxaTb6jjyxW33pW3sg== +"@react-spring/rafz@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.4.5.tgz#84f809f287f2a66bbfbc66195db340482f886bd7" + integrity sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ== -"@react-spring/shared@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.6.tgz#2c84e62cc0cfbbbbeb5546acd46c1f4b248bc562" - integrity sha512-Qrm9fopKG/RxZ3Rw+4euhrpnB3uXSyiON9skHbcBfmkkzagpkUR66MX1YLrhHw0UchcZuSDnXs0Lonzt1rpWag== +"@react-spring/shared@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.4.5.tgz#4c3ad817bca547984fb1539204d752a412a6d829" + integrity sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA== dependencies: - "@react-spring/rafz" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" + "@react-spring/rafz" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/types@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.6.tgz#f60722fcf9f8492ae16d0bdc47f0ea3c2a16d2cf" - integrity sha512-l7mCw182DtDMnCI8CB9orgTAEoFZRtdQ6aS6YeEAqYcy3nQZPmPggIHH9DxyLw7n7vBPRSzu9gCvUMgXKpTflg== +"@react-spring/types@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.4.5.tgz#9c71e5ff866b5484a7ef3db822bf6c10e77bdd8c" + integrity sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg== -"@react-spring/web@9.2.6": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.6.tgz#c4fba69e1b1b43bd1d6a62346530cfb07f2be09b" - integrity sha512-0HkRsEYR/CO3Uw46FWDWaF2wg2rUXcWE2R9AoZXthEYLUn5w9uE1mf2Jel7BxBxWGQ73owkqSQv+klA1Hb+ViQ== +"@react-spring/web@9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.4.5.tgz#b92f05b87cdc0963a59ee149e677dcaff09f680e" + integrity sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA== dependencies: - "@react-spring/animated" "~9.2.6-beta.0" - "@react-spring/core" "~9.2.6-beta.0" - "@react-spring/shared" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" + "@react-spring/animated" "~9.4.5" + "@react-spring/core" "~9.4.5" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" "@rushstack/eslint-patch@^1.1.3": version "1.1.3" @@ -3566,6 +3564,13 @@ dependencies: "@types/node" "*" +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@5.36.0": version "5.36.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.0.tgz#8f159c4cdb3084eb5d4b72619a2ded942aa109e5" @@ -4318,7 +4323,7 @@ base16@^1.0.0: resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ== -base64-js@^1.3.0: +base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4348,6 +4353,15 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.7.1: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -4461,6 +4475,11 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 node-releases "^2.0.3" picocolors "^1.0.0" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -4471,6 +4490,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.2.1, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4650,6 +4677,11 @@ chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -5016,7 +5048,7 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-fetch@^3.1.5: +cross-fetch@3.1.5, cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== @@ -5232,11 +5264,6 @@ d3-format@^1.4.4: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== -d3-hierarchy@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" - integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== - "d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" @@ -5343,7 +5370,7 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5499,6 +5526,11 @@ detective@^5.2.1: defined "^1.0.0" minimist "^1.2.6" +devtools-protocol@0.0.1036444: + version "0.0.1036444" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz#a570d3cdde61527c82f9b03919847b8ac7b1c2b9" + integrity sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw== + dicer@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.1.tgz#abf28921e3475bc5e801e74e0159fd94f927ba97" @@ -6239,6 +6271,17 @@ extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -6321,6 +6364,13 @@ fbjs@^3.0.0, fbjs@^3.0.1: setimmediate "^1.0.5" ua-parser-js "^0.7.30" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + feed@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" @@ -6580,6 +6630,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^10.0.1: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -7298,6 +7353,14 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + https-proxy-agent@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" @@ -7306,14 +7369,6 @@ https-proxy-agent@^3.0.0: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -7336,6 +7391,11 @@ idb@7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + 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" @@ -7409,7 +7469,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8564,6 +8624,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" @@ -9207,15 +9272,10 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== picocolors@^1.0.0: version "1.0.0" @@ -9639,6 +9699,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +progress@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -9661,7 +9726,7 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9829,7 +9894,7 @@ proxy-agent@^3.0.3: proxy-from-env "^1.0.0" socks-proxy-agent "^4.0.1" -proxy-from-env@^1.0.0: +proxy-from-env@1.1.0, proxy-from-env@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -9878,6 +9943,23 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" +puppeteer@18.0.5: + version "18.0.5" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-18.0.5.tgz#873223b17b92345182c5b5e8cfbd6f3117f1547d" + integrity sha512-s4erjxU0VtKojPvF+KvLKG6OHUPw7gO2YV1dtOsoryyCbhrs444fXb4QZqGWuTv3V/rgSCUzeixxu34g0ZkSMA== + dependencies: + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.1036444" + extract-zip "2.0.1" + https-proxy-agent "5.0.1" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.8.1" + pure-color@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" @@ -9922,13 +10004,6 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== -raf@^3.1.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -10128,15 +10203,6 @@ react-masonry-css@1.0.16: resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c" integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ== -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - react-query@3.39.0: version "3.39.0" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050" @@ -10274,7 +10340,7 @@ readable-stream@2, readable-stream@^2.0.1, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -10538,11 +10604,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -11367,6 +11428,27 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + teeny-request@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" @@ -11404,6 +11486,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + thunkify@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" @@ -11627,6 +11714,14 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" @@ -12234,6 +12329,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + ws@>=7.4.6: version "8.6.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" @@ -12314,6 +12414,14 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"