Merge branch 'main' into cache-profit
This commit is contained in:
commit
4d038b43ad
|
@ -63,6 +63,7 @@ export function getNewContract(
|
|||
tags: [],
|
||||
lowercaseTags: [],
|
||||
visibility,
|
||||
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
|
||||
isResolved: false,
|
||||
createdTime: Date.now(),
|
||||
closeTime,
|
||||
|
|
|
@ -178,31 +178,44 @@ export const getNotificationDestinationsForUser = (
|
|||
reason: notification_reason_types | notification_preference
|
||||
) => {
|
||||
const notificationSettings = privateUser.notificationPreferences
|
||||
let destinations
|
||||
let subscriptionType: notification_preference | undefined
|
||||
if (Object.keys(notificationSettings).includes(reason)) {
|
||||
subscriptionType = reason as notification_preference
|
||||
destinations = notificationSettings[subscriptionType]
|
||||
} else {
|
||||
const key = reason as notification_reason_types
|
||||
subscriptionType = notificationReasonToSubscriptionType[key]
|
||||
destinations = subscriptionType
|
||||
? notificationSettings[subscriptionType]
|
||||
: []
|
||||
}
|
||||
const optOutOfAllSettings = notificationSettings['opt_out_all']
|
||||
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
|
||||
const optedOutOfEmail =
|
||||
optOutOfAllSettings.includes('email') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
const optedOutOfBrowser =
|
||||
optOutOfAllSettings.includes('browser') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
|
||||
return {
|
||||
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
|
||||
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
|
||||
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
|
||||
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
|
||||
try {
|
||||
let destinations
|
||||
let subscriptionType: notification_preference | undefined
|
||||
if (Object.keys(notificationSettings).includes(reason)) {
|
||||
subscriptionType = reason as notification_preference
|
||||
destinations = notificationSettings[subscriptionType]
|
||||
} else {
|
||||
const key = reason as notification_reason_types
|
||||
subscriptionType = notificationReasonToSubscriptionType[key]
|
||||
destinations = subscriptionType
|
||||
? notificationSettings[subscriptionType]
|
||||
: []
|
||||
}
|
||||
const optOutOfAllSettings = notificationSettings['opt_out_all']
|
||||
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
|
||||
const optedOutOfEmail =
|
||||
optOutOfAllSettings.includes('email') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
const optedOutOfBrowser =
|
||||
optOutOfAllSettings.includes('browser') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
return {
|
||||
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
|
||||
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
|
||||
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
|
||||
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail safely
|
||||
console.log(
|
||||
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
|
||||
)
|
||||
return {
|
||||
sendToEmail: false,
|
||||
sendToBrowser: false,
|
||||
unsubscribeUrl: '',
|
||||
urlToManageThisNotification: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ export const exhibitExts = [
|
|||
|
||||
Image,
|
||||
Link,
|
||||
Mention,
|
||||
Mention.extend({ name: 'contract-mention' }),
|
||||
Iframe,
|
||||
TiptapTweet,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { getValueFromBucket } from '../../common/calculate-dpm'
|
|||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||
|
||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||
import { contractUrl, getUser } from './utils'
|
||||
import { contractUrl, getUser, log } from './utils'
|
||||
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||
import { notification_reason_types } from '../../common/notification'
|
||||
import { Dictionary } from 'lodash'
|
||||
|
@ -212,20 +212,16 @@ export const sendOneWeekBonusEmail = async (
|
|||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
!privateUser.notificationPreferences.onboarding_flow.includes('email')
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
|
@ -247,19 +243,15 @@ export const sendCreatorGuideEmail = async (
|
|||
privateUser: PrivateUser,
|
||||
sendTime: string
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
!privateUser.notificationPreferences.onboarding_flow.includes('email')
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Create your own prediction market',
|
||||
|
@ -279,22 +271,16 @@ export const sendThankYouEmail = async (
|
|||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
!privateUser.notificationPreferences.thank_you_for_purchases.includes(
|
||||
'email'
|
||||
)
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'thank_you_for_purchases'
|
||||
)
|
||||
|
||||
if (!sendToEmail) return
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Thanks for your Manifold purchase',
|
||||
|
@ -466,17 +452,13 @@ export const sendInterestingMarketsEmail = async (
|
|||
contractsToSend: Contract[],
|
||||
deliveryTime?: string
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
!privateUser.notificationPreferences.trending_markets.includes('email')
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'trending_markets'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
@ -620,18 +602,15 @@ export const sendWeeklyPortfolioUpdateEmail = async (
|
|||
investments: PerContractInvestmentsData[],
|
||||
overallPerformance: OverallPerformanceData
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
!privateUser.notificationPreferences.profit_loss_updates.includes('email')
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'profit_loss_updates'
|
||||
)
|
||||
|
||||
if (!sendToEmail) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const templateData: Record<string, string> = {
|
||||
|
@ -656,4 +635,5 @@ export const sendWeeklyPortfolioUpdateEmail = async (
|
|||
: 'portfolio-update',
|
||||
templateData
|
||||
)
|
||||
log('Sent portfolio update email to', privateUser.email)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export * from './on-create-user'
|
|||
export * from './on-create-bet'
|
||||
export * from './on-create-comment-on-contract'
|
||||
export * from './on-view'
|
||||
export { updateMetrics } from './update-metrics'
|
||||
export { scheduleUpdateMetrics } from './update-metrics'
|
||||
export * from './update-stats'
|
||||
export * from './update-loans'
|
||||
export * from './backup-db'
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
import { getAllPrivateUsers } from 'functions/src/utils'
|
||||
import { filterDefined } from 'common/lib/util/array'
|
||||
import { getPrivateUser } from '../utils'
|
||||
initAdmin()
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function main() {
|
||||
const privateUsers = await getAllPrivateUsers()
|
||||
// const privateUsers = await getAllPrivateUsers()
|
||||
const privateUsers = filterDefined([
|
||||
await getPrivateUser('ddSo9ALC15N9FAZdKdA2qE3iIvH3'),
|
||||
])
|
||||
await Promise.all(
|
||||
privateUsers.map((privateUser) => {
|
||||
if (!privateUser.id) return Promise.resolve()
|
||||
return firestore
|
||||
.collection('private-users')
|
||||
.doc(privateUser.id)
|
||||
.update({
|
||||
notificationPreferences: {
|
||||
...privateUser.notificationPreferences,
|
||||
opt_out_all: [],
|
||||
},
|
||||
})
|
||||
if (privateUser.notificationPreferences.opt_out_all === undefined) {
|
||||
console.log('updating opt out all', privateUser.id)
|
||||
return firestore
|
||||
.collection('private-users')
|
||||
.doc(privateUser.id)
|
||||
.update({
|
||||
notificationPreferences: {
|
||||
...privateUser.notificationPreferences,
|
||||
opt_out_all: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,8 +27,7 @@ import { getFunctionUrl } from '../../common/api'
|
|||
import { filterDefined } from 'common/util/array'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const updateMetrics = functions.pubsub
|
||||
export const scheduleUpdateMetrics = functions.pubsub
|
||||
.schedule('every 15 minutes')
|
||||
.onRun(async () => {
|
||||
const response = await fetch(getFunctionUrl('updatemetrics'), {
|
||||
|
|
|
@ -112,13 +112,12 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
|
|||
)
|
||||
)
|
||||
)
|
||||
log('Found', contractsUsersBetOn.length, 'contracts')
|
||||
let count = 0
|
||||
await Promise.all(
|
||||
privateUsersToSendEmailsTo.map(async (privateUser) => {
|
||||
const user = await getUser(privateUser.id)
|
||||
// Don't send to a user unless they're over 5 days old
|
||||
if (!user || user.createdTime > Date.now() - 5 * DAY_MS) return
|
||||
if (!user || user.createdTime > Date.now() - 5 * DAY_MS)
|
||||
return await setEmailFlagAsSent(privateUser.id)
|
||||
const userBets = usersBets[privateUser.id] as Bet[]
|
||||
const contractsUserBetOn = contractsUsersBetOn.filter((contract) =>
|
||||
userBets.some((bet) => bet.contractId === contract.id)
|
||||
|
@ -219,13 +218,6 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
|
|||
(differences) => Math.abs(differences.profit)
|
||||
).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.profit) > 1
|
||||
|
@ -245,29 +237,28 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
|
|||
usersToContractsCreated[privateUser.id].length === 0
|
||||
) {
|
||||
log(
|
||||
'No bets in last week, no market movers, no markets created. Not sending an email.'
|
||||
`No bets in last week, no market movers, no markets created. Not sending an email to ${privateUser.email} .`
|
||||
)
|
||||
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||
weeklyPortfolioUpdateEmailSent: true,
|
||||
})
|
||||
return
|
||||
return await setEmailFlagAsSent(privateUser.id)
|
||||
}
|
||||
// Set the flag beforehand just to be safe
|
||||
await setEmailFlagAsSent(privateUser.id)
|
||||
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 users:', count)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function setEmailFlagAsSent(privateUserId: string) {
|
||||
await firestore.collection('private-users').doc(privateUserId).update({
|
||||
weeklyPortfolioUpdateEmailSent: true,
|
||||
})
|
||||
}
|
||||
|
||||
export type PerContractInvestmentsData = {
|
||||
questionTitle: string
|
||||
questionUrl: string
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Col } from './layout/col'
|
|||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { Row } from './layout/row'
|
||||
import { AddFundsModal } from './add-funds-modal'
|
||||
import { Input } from './input'
|
||||
|
||||
export function AmountInput(props: {
|
||||
amount: number | undefined
|
||||
|
@ -44,9 +45,9 @@ export function AmountInput(props: {
|
|||
<span className="text-greyscale-4 absolute top-1/2 my-auto ml-2 -translate-y-1/2">
|
||||
{label}
|
||||
</span>
|
||||
<input
|
||||
<Input
|
||||
className={clsx(
|
||||
'placeholder:text-greyscale-4 border-greyscale-2 rounded-md pl-9',
|
||||
'pl-9',
|
||||
error && 'input-error',
|
||||
'w-24 md:w-auto',
|
||||
inputClassName
|
||||
|
|
|
@ -10,6 +10,7 @@ import { formatPercent } from 'common/util/format'
|
|||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||
import { Linkify } from '../linkify'
|
||||
import { Input } from '../input'
|
||||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
|
@ -74,8 +75,8 @@ export function AnswerItem(props: {
|
|||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||
{!wasResolvedTo &&
|
||||
(showChoice === 'checkbox' ? (
|
||||
<input
|
||||
className="input input-bordered w-24 justify-self-end text-2xl"
|
||||
<Input
|
||||
className="w-24 justify-self-end !text-2xl"
|
||||
type="number"
|
||||
placeholder={`${roundedProb}`}
|
||||
maxLength={9}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { findBestMatch } from 'string-similarity'
|
||||
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
|
@ -26,6 +25,7 @@ import { MAX_ANSWER_LENGTH } from 'common/answer'
|
|||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { lowerCase } from 'lodash'
|
||||
import { Button } from '../button'
|
||||
import { ExpandingInput } from '../expanding-input'
|
||||
|
||||
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||
const { contract } = props
|
||||
|
@ -122,10 +122,10 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
|||
<Col className="gap-4 rounded">
|
||||
<Col className="flex-1 gap-2 px-4 xl:px-0">
|
||||
<div className="mb-1">Add your answer</div>
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
value={text}
|
||||
onChange={(e) => changeAnswer(e.target.value)}
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
className="w-full"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
maxLength={MAX_ANSWER_LENGTH}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
import { ExpandingInput } from '../expanding-input'
|
||||
|
||||
export function MultipleChoiceAnswers(props: {
|
||||
answers: string[]
|
||||
|
@ -27,10 +27,10 @@ export function MultipleChoiceAnswers(props: {
|
|||
{answers.map((answer, i) => (
|
||||
<Row className="mb-2 items-center gap-2 align-middle">
|
||||
{i + 1}.{' '}
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer(i, e.target.value)}
|
||||
className="textarea textarea-bordered ml-2 w-full resize-none"
|
||||
className="ml-2 w-full"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
maxLength={MAX_ANSWER_LENGTH}
|
||||
|
|
|
@ -20,11 +20,11 @@ import { getProbability } from 'common/calculate'
|
|||
import { createMarket } from 'web/lib/firebase/api'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
import { FIXED_ANTE } from 'common/economy'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { useTextEditor } from 'web/components/editor'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { CopyLinkButton } from '../copy-link-button'
|
||||
import { ExpandingInput } from '../expanding-input'
|
||||
|
||||
type challengeInfo = {
|
||||
amount: number
|
||||
|
@ -153,9 +153,9 @@ function CreateChallengeForm(props: {
|
|||
{contract ? (
|
||||
<span className="underline">{contract.question}</span>
|
||||
) : (
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
placeholder="e.g. Will a Democrat be the next president?"
|
||||
className="input input-bordered mt-1 w-full resize-none"
|
||||
className="mt-1 w-full"
|
||||
autoFocus={true}
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={challengeInfo.question}
|
||||
|
|
|
@ -41,6 +41,7 @@ import { AdjustmentsIcon } from '@heroicons/react/solid'
|
|||
import { Button } from './button'
|
||||
import { Modal } from './layout/modal'
|
||||
import { Title } from './title'
|
||||
import { Input } from './input'
|
||||
|
||||
export const SORTS = [
|
||||
{ label: 'Newest', value: 'newest' },
|
||||
|
@ -438,13 +439,13 @@ function ContractSearchControls(props: {
|
|||
return (
|
||||
<Col className={clsx('bg-base-200 top-0 z-20 gap-3 pb-3', className)}>
|
||||
<Row className="gap-1 sm:gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => updateQuery(e.target.value)}
|
||||
onBlur={trackCallback('search', { query: query })}
|
||||
placeholder={'Search'}
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Search"
|
||||
className="w-full"
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
{!isMobile && !query && (
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
import { Contract, MAX_DESCRIPTION_LENGTH } from 'common/contract'
|
||||
import { exhibitExts } from 'common/util/parse'
|
||||
import { useAdmin } from 'web/hooks/use-admin'
|
||||
|
@ -15,6 +13,7 @@ import { Button } from '../button'
|
|||
import { Spacer } from '../layout/spacer'
|
||||
import { Editor, Content as ContentType } from '@tiptap/react'
|
||||
import { insertContent } from '../editor/utils'
|
||||
import { ExpandingInput } from '../expanding-input'
|
||||
|
||||
export function ContractDescription(props: {
|
||||
contract: Contract
|
||||
|
@ -138,8 +137,8 @@ function EditQuestion(props: {
|
|||
|
||||
return editing ? (
|
||||
<div className="mt-4">
|
||||
<Textarea
|
||||
className="textarea textarea-bordered mb-1 h-24 w-full resize-none"
|
||||
<ExpandingInput
|
||||
className="mb-1 h-24 w-full"
|
||||
rows={2}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value || '')}
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
BountiedContractBadge,
|
||||
BountiedContractSmallBadge,
|
||||
} from 'web/components/contract/bountied-contract-badge'
|
||||
import { Input } from '../input'
|
||||
|
||||
export type ShowTime = 'resolve-date' | 'close-date'
|
||||
|
||||
|
@ -445,17 +446,17 @@ function EditableCloseDate(props: {
|
|||
<Col className="rounded bg-white px-8 pb-8">
|
||||
<Subtitle text="Edit market close time" />
|
||||
<Row className="z-10 mr-2 mt-4 w-full shrink-0 flex-wrap items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="date"
|
||||
className="input input-bordered w-full shrink-0 sm:w-fit"
|
||||
className="w-full shrink-0 sm:w-fit"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setCloseDate(e.target.value)}
|
||||
min={Date.now()}
|
||||
value={closeDate}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="time"
|
||||
className="input input-bordered w-full shrink-0 sm:w-max"
|
||||
className="w-full shrink-0 sm:w-max"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
||||
min="00:00"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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'
|
||||
|
@ -10,6 +9,7 @@ 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'
|
||||
import { ExpandingInput } from './expanding-input'
|
||||
|
||||
export function CreatePost(props: { group?: Group }) {
|
||||
const [title, setTitle] = useState('')
|
||||
|
@ -60,9 +60,8 @@ export function CreatePost(props: { group?: Group }) {
|
|||
Title<span className={'text-red-700'}> *</span>
|
||||
</span>
|
||||
</label>
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
placeholder="e.g. Elon Mania Post"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_POST_TITLE_LENGTH}
|
||||
value={title}
|
||||
|
@ -74,9 +73,8 @@ export function CreatePost(props: { group?: Group }) {
|
|||
Subtitle<span className={'text-red-700'}> *</span>
|
||||
</span>
|
||||
</label>
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
placeholder="e.g. How Elon Musk is getting everyone's attention"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_POST_TITLE_LENGTH}
|
||||
value={subtitle}
|
||||
|
|
16
web/components/expanding-input.tsx
Normal file
16
web/components/expanding-input.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import clsx from 'clsx'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
/** Expanding `<textarea>` with same style as input.tsx */
|
||||
export const ExpandingInput = (props: Parameters<typeof Textarea>[0]) => {
|
||||
const { className, ...rest } = props
|
||||
return (
|
||||
<Textarea
|
||||
className={clsx(
|
||||
'textarea textarea-bordered resize-none text-[16px] md:text-[14px]',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -8,6 +8,7 @@ import { Avatar } from 'web/components/avatar'
|
|||
import { Row } from 'web/components/layout/row'
|
||||
import { searchInAny } from 'common/util/parse'
|
||||
import { UserLink } from 'web/components/user-link'
|
||||
import { Input } from './input'
|
||||
|
||||
export function FilterSelectUsers(props: {
|
||||
setSelectedUsers: (users: User[]) => void
|
||||
|
@ -50,13 +51,13 @@ export function FilterSelectUsers(props: {
|
|||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<UserIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
name="user name"
|
||||
id="user name"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="input input-bordered block w-full pl-10 focus:border-gray-300 "
|
||||
className="block w-full pl-10"
|
||||
placeholder="Austin Chen"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Title } from '../title'
|
|||
import { User } from 'common/user'
|
||||
import { MAX_GROUP_NAME_LENGTH } from 'common/group'
|
||||
import { createGroup } from 'web/lib/firebase/api'
|
||||
import { Input } from '../input'
|
||||
|
||||
export function CreateGroupButton(props: {
|
||||
user: User
|
||||
|
@ -104,9 +105,8 @@ export function CreateGroupButton(props: {
|
|||
|
||||
<div className="form-control w-full">
|
||||
<label className="mb-2 ml-1 mt-0">Group name</label>
|
||||
<input
|
||||
<Input
|
||||
placeholder={'Your group name'}
|
||||
className="input input-bordered resize-none"
|
||||
disabled={isSubmitting}
|
||||
value={name}
|
||||
maxLength={MAX_GROUP_NAME_LENGTH}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Modal } from 'web/components/layout/modal'
|
|||
import { FilterSelectUsers } from 'web/components/filter-select-users'
|
||||
import { User } from 'common/user'
|
||||
import { useMemberIds } from 'web/hooks/use-group'
|
||||
import { Input } from '../input'
|
||||
|
||||
export function EditGroupButton(props: { group: Group; className?: string }) {
|
||||
const { group, className } = props
|
||||
|
@ -54,9 +55,8 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
|
|||
<span className="mb-1">Group name</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
<Input
|
||||
placeholder="Your group name"
|
||||
className="input input-bordered resize-none"
|
||||
disabled={isSubmitting}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value || '')}
|
||||
|
|
22
web/components/input.tsx
Normal file
22
web/components/input.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
|
||||
/** Text input. Wraps html `<input>` */
|
||||
export const Input = (props: JSX.IntrinsicElements['input']) => {
|
||||
const { className, ...rest } = props
|
||||
|
||||
return (
|
||||
<input
|
||||
className={clsx('input input-bordered text-base md:text-sm', className)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: replace daisyui style with our own. For reference:
|
||||
|
||||
james: text-lg placeholder:text-gray-400
|
||||
inga: placeholder:text-greyscale-4 border-greyscale-2 rounded-md
|
||||
austin: border-gray-300 text-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm
|
||||
*/
|
|
@ -7,12 +7,13 @@ import { User } from 'common/user'
|
|||
import { ManalinkCard, ManalinkInfo } from 'web/components/manalink-card'
|
||||
import { createManalink } from 'web/lib/firebase/manalinks'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import dayjs from 'dayjs'
|
||||
import { Button } from '../button'
|
||||
import { getManalinkUrl } from 'web/pages/links'
|
||||
import { DuplicateIcon } from '@heroicons/react/outline'
|
||||
import { QRCode } from '../qr-code'
|
||||
import { Input } from '../input'
|
||||
import { ExpandingInput } from '../expanding-input'
|
||||
|
||||
export function CreateLinksButton(props: {
|
||||
user: User
|
||||
|
@ -120,8 +121,8 @@ function CreateManalinkForm(props: {
|
|||
<span className="absolute mx-3 mt-3.5 text-sm text-gray-400">
|
||||
M$
|
||||
</span>
|
||||
<input
|
||||
className="input input-bordered w-full pl-10"
|
||||
<Input
|
||||
className="w-full pl-10"
|
||||
type="number"
|
||||
min="1"
|
||||
value={newManalink.amount}
|
||||
|
@ -136,8 +137,7 @@ function CreateManalinkForm(props: {
|
|||
<div className="flex flex-col gap-2 md:flex-row">
|
||||
<div className="form-control w-full md:w-1/2">
|
||||
<label className="label">Uses</label>
|
||||
<input
|
||||
className="input input-bordered"
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={newManalink.maxUses ?? ''}
|
||||
|
@ -146,7 +146,7 @@ function CreateManalinkForm(props: {
|
|||
return { ...m, maxUses: parseInt(e.target.value) }
|
||||
})
|
||||
}
|
||||
></input>
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full md:w-1/2">
|
||||
<label className="label">Expires in</label>
|
||||
|
@ -165,10 +165,9 @@ function CreateManalinkForm(props: {
|
|||
</div>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">Message</label>
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
placeholder={defaultMessage}
|
||||
maxLength={200}
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
value={newManalink.message}
|
||||
rows="3"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ReactNode } from 'react'
|
|||
import React from 'react'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { Input } from './input'
|
||||
|
||||
export function NumberInput(props: {
|
||||
numberString: string
|
||||
|
@ -32,9 +33,9 @@ export function NumberInput(props: {
|
|||
return (
|
||||
<Col className={className}>
|
||||
<label className="input-group">
|
||||
<input
|
||||
<Input
|
||||
className={clsx(
|
||||
'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400',
|
||||
'max-w-[200px] !text-lg',
|
||||
error && 'input-error',
|
||||
inputClassName
|
||||
)}
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
|||
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { getPseudoProbability } from 'common/pseudo-numeric'
|
||||
import { BucketInput } from './bucket-input'
|
||||
import { Input } from './input'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
||||
|
@ -30,11 +31,8 @@ export function ProbabilityInput(props: {
|
|||
return (
|
||||
<Col className={className}>
|
||||
<label className="input-group">
|
||||
<input
|
||||
className={clsx(
|
||||
'input input-bordered max-w-[200px] text-lg placeholder:text-gray-400',
|
||||
inputClassName
|
||||
)}
|
||||
<Input
|
||||
className={clsx('max-w-[200px] !text-lg', inputClassName)}
|
||||
type="number"
|
||||
max={99}
|
||||
min={1}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Input } from './input'
|
||||
import { Row } from './layout/row'
|
||||
|
||||
export function ProbabilitySelector(props: {
|
||||
|
@ -10,10 +11,10 @@ export function ProbabilitySelector(props: {
|
|||
return (
|
||||
<Row className="items-center gap-2">
|
||||
<label className="input-group input-group-lg text-lg">
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={probabilityInt}
|
||||
className="input input-bordered input-md w-28 text-lg"
|
||||
className="input-md w-28 !text-lg"
|
||||
disabled={isSubmitting}
|
||||
min={1}
|
||||
max={99}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { getUser } from 'web/lib/firebase/users'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { User } from 'common/user'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { Input } from 'web/components/input'
|
||||
|
||||
export async function getStaticProps() {
|
||||
let txns = await getAllCharityTxns()
|
||||
|
@ -171,11 +172,11 @@ export default function Charity(props: {
|
|||
/>
|
||||
<Spacer h={10} />
|
||||
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => debouncedQuery(e.target.value)}
|
||||
placeholder="Find a charity"
|
||||
className="input input-bordered mb-6 w-full"
|
||||
className="mb-6 w-full"
|
||||
/>
|
||||
</Col>
|
||||
<div className="grid max-w-xl grid-flow-row grid-cols-1 gap-4 self-center lg:max-w-full lg:grid-cols-2 xl:grid-cols-3">
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
urlParamStore,
|
||||
} from 'web/hooks/use-persistent-state'
|
||||
import { PAST_BETS } from 'common/user'
|
||||
import { Input } from 'web/components/input'
|
||||
|
||||
const MAX_CONTRACTS_RENDERED = 100
|
||||
|
||||
|
@ -88,12 +89,12 @@ export default function ContractSearchFirestore(props: {
|
|||
<div>
|
||||
{/* Show a search input next to a sort dropdown */}
|
||||
<div className="mt-2 mb-8 flex justify-between gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search markets"
|
||||
className="input input-bordered w-full"
|
||||
className="w-full"
|
||||
/>
|
||||
<select
|
||||
className="select select-bordered"
|
||||
|
|
|
@ -2,7 +2,6 @@ import router, { useRouter } from 'next/router'
|
|||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { Spacer } from 'web/components/layout/spacer'
|
||||
import { getUserAndPrivateUser } from 'web/lib/firebase/users'
|
||||
import { Contract, contractPath } from 'web/lib/firebase/contracts'
|
||||
|
@ -39,6 +38,8 @@ import { SiteLink } from 'web/components/site-link'
|
|||
import { Button } from 'web/components/button'
|
||||
import { AddFundsModal } from 'web/components/add-funds-modal'
|
||||
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||
import { Input } from 'web/components/input'
|
||||
import { ExpandingInput } from 'web/components/expanding-input'
|
||||
|
||||
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
||||
return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
|
||||
|
@ -104,9 +105,8 @@ export default function Create(props: { auth: { user: User } }) {
|
|||
</span>
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
<ExpandingInput
|
||||
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={question}
|
||||
|
@ -329,9 +329,9 @@ export function NewContract(props: {
|
|||
</label>
|
||||
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
className="input input-bordered w-32"
|
||||
className="w-32"
|
||||
placeholder="LOW"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMinString(e.target.value)}
|
||||
|
@ -340,9 +340,9 @@ export function NewContract(props: {
|
|||
disabled={isSubmitting}
|
||||
value={minString ?? ''}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
className="input input-bordered w-32"
|
||||
className="w-32"
|
||||
placeholder="HIGH"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setMaxString(e.target.value)}
|
||||
|
@ -374,9 +374,8 @@ export function NewContract(props: {
|
|||
</label>
|
||||
|
||||
<Row className="gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
className="input input-bordered"
|
||||
placeholder="Initial value"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setInitialValueString(e.target.value)}
|
||||
|
@ -446,19 +445,17 @@ export function NewContract(props: {
|
|||
className={'col-span-4 sm:col-span-2'}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<input
|
||||
<Row className="mt-4 gap-2">
|
||||
<Input
|
||||
type={'date'}
|
||||
className="input input-bordered mt-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setCloseDate(e.target.value)}
|
||||
min={Math.round(Date.now() / MINUTE_MS) * MINUTE_MS}
|
||||
disabled={isSubmitting}
|
||||
value={closeDate}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type={'time'}
|
||||
className="input input-bordered mt-4 ml-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
||||
min={'00:00'}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Router from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
import { DateDoc } from 'common/post'
|
||||
import { useTextEditor, TextEditor } from 'web/components/editor'
|
||||
import { Page } from 'web/components/page'
|
||||
|
@ -17,6 +15,8 @@ import { MAX_QUESTION_LENGTH } from 'common/contract'
|
|||
import { NoSEO } from 'web/components/NoSEO'
|
||||
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
import { Input } from 'web/components/input'
|
||||
import { ExpandingInput } from 'web/components/expanding-input'
|
||||
|
||||
export default function CreateDateDocPage() {
|
||||
const user = useUser()
|
||||
|
@ -94,9 +94,8 @@ export default function CreateDateDocPage() {
|
|||
<Col className="gap-8">
|
||||
<Col className="max-w-[160px] justify-start gap-4">
|
||||
<div className="">Birthday</div>
|
||||
<input
|
||||
<Input
|
||||
type={'date'}
|
||||
className="input input-bordered"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setBirthday(e.target.value)}
|
||||
max={Math.round(Date.now() / MINUTE_MS) * MINUTE_MS}
|
||||
|
@ -122,8 +121,7 @@ export default function CreateDateDocPage() {
|
|||
</Row>
|
||||
|
||||
<Col className="gap-2">
|
||||
<Textarea
|
||||
className="input input-bordered resize-none"
|
||||
<ExpandingInput
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { SEO } from 'web/components/SEO'
|
|||
import { GetServerSideProps } from 'next'
|
||||
import { authenticateOnServer } from 'web/lib/firebase/server-auth'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { Input } from 'web/components/input'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const creds = await authenticateOnServer(ctx)
|
||||
|
@ -106,12 +107,12 @@ export default function Groups(props: {
|
|||
title: 'All',
|
||||
content: (
|
||||
<Col>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => debouncedQuery(e.target.value)}
|
||||
placeholder="Search groups"
|
||||
value={query}
|
||||
className="input input-bordered mb-4 w-full"
|
||||
className="mb-4 w-full"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
|
@ -134,12 +135,12 @@ export default function Groups(props: {
|
|||
title: 'My Groups',
|
||||
content: (
|
||||
<Col>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => debouncedQuery(e.target.value)}
|
||||
placeholder="Search your groups"
|
||||
className="input input-bordered mb-4 w-full"
|
||||
className="mb-4 w-full"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
} from 'web/hooks/use-contracts'
|
||||
import { ProfitBadge } from 'web/components/profit-badge'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { Input } from 'web/components/input'
|
||||
|
||||
export default function Home() {
|
||||
const user = useUser()
|
||||
|
@ -99,10 +100,10 @@ export default function Home() {
|
|||
<Row
|
||||
className={'mb-2 w-full items-center justify-between gap-4 sm:gap-8'}
|
||||
>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'Search'}
|
||||
className="input input-bordered w-full"
|
||||
className="w-full"
|
||||
onClick={() => Router.push('/search')}
|
||||
/>
|
||||
<CustomizeButton justIcon />
|
||||
|
|
|
@ -3,8 +3,9 @@ import { PrivateUser, User } from 'common/user'
|
|||
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
|
||||
import Link from 'next/link'
|
||||
import React, { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { ConfirmationButton } from 'web/components/confirmation-button'
|
||||
import { ExpandingInput } from 'web/components/expanding-input'
|
||||
import { Input } from 'web/components/input'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Page } from 'web/components/page'
|
||||
|
@ -43,16 +44,15 @@ function EditUserField(props: {
|
|||
<label className="label">{label}</label>
|
||||
|
||||
{field === 'bio' ? (
|
||||
<Textarea
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
<ExpandingInput
|
||||
className="w-full"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={updateField}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value || '')}
|
||||
onBlur={updateField}
|
||||
|
@ -152,10 +152,9 @@ export default function ProfilePage(props: {
|
|||
|
||||
<div>
|
||||
<label className="label">Display name</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Display name"
|
||||
className="input input-bordered"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value || '')}
|
||||
onBlur={updateDisplayName}
|
||||
|
@ -164,10 +163,9 @@ export default function ProfilePage(props: {
|
|||
|
||||
<div>
|
||||
<label className="label">Username</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
className="input input-bordered"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value || '')}
|
||||
onBlur={updateUsername}
|
||||
|
@ -199,10 +197,9 @@ export default function ProfilePage(props: {
|
|||
<div>
|
||||
<label className="label">API key</label>
|
||||
<div className="input-group w-full">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Click refresh to generate key"
|
||||
className="input input-bordered w-full"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user