Fix up several pages to load user data on the server (#722)

* Fix up several pages to load user data on the server

* Add key prop to `EditUserField`
This commit is contained in:
Marshall Polaris 2022-08-08 22:43:04 -07:00 committed by GitHub
parent 5649161348
commit e7f1d3924b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 119 deletions

View File

@ -52,6 +52,11 @@ export async function getUser(userId: string) {
return (await getDoc(doc(users, userId))).data()!
}
export async function getPrivateUser(userId: string) {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return (await getDoc(doc(users, userId))).data()!
}
export async function getUserByUsername(username: string) {
// Find a user whose username matches the given username, or null if no such user exists.
const q = query(users, where('username', '==', username), limit(1))

View File

@ -4,7 +4,7 @@ import clsx from 'clsx'
import dayjs from 'dayjs'
import Textarea from 'react-expanding-textarea'
import { Spacer } from 'web/components/layout/spacer'
import { useUser } from 'web/hooks/use-user'
import { getUser } from 'web/lib/firebase/users'
import { Contract, contractPath } from 'web/lib/firebase/contracts'
import { createMarket } from 'web/lib/firebase/api'
import { FIXED_ANTE } from 'common/antes'
@ -33,7 +33,10 @@ import { Title } from 'web/components/title'
import { SEO } from 'web/components/SEO'
import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers'
export const getServerSideProps = redirectIfLoggedOut('/')
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
const user = await getUser(creds.user.uid)
return { props: { user } }
})
type NewQuestionParams = {
groupId?: string
@ -49,8 +52,9 @@ type NewQuestionParams = {
initValue?: string
}
export default function Create() {
export default function Create(props: { user: User }) {
useTracking('view create page')
const { user } = props
const router = useRouter()
const params = router.query as NewQuestionParams
// TODO: Not sure why Question is pulled out as its own component;
@ -60,8 +64,7 @@ export default function Create() {
setQuestion(params.q ?? '')
}, [params.q])
const creator = useUser()
if (!router.isReady || !creator) return <div />
if (!router.isReady) return <div />
return (
<Page>
@ -93,7 +96,7 @@ export default function Create() {
</div>
</form>
<Spacer h={6} />
<NewContract question={question} params={params} creator={creator} />
<NewContract question={question} params={params} creator={user} />
</div>
</div>
</Page>
@ -102,7 +105,7 @@ export default function Create() {
// Allow user to create a new contract
export function NewContract(props: {
creator?: User | null
creator: User
question: string
params?: NewQuestionParams
}) {
@ -120,14 +123,14 @@ export function NewContract(props: {
const [answers, setAnswers] = useState<string[]>([]) // for multiple choice
useEffect(() => {
if (groupId && creator)
if (groupId)
getGroup(groupId).then((group) => {
if (group && canModifyGroupContracts(group, creator.id)) {
setSelectedGroup(group)
setShowGroupSelector(false)
}
})
}, [creator, groupId])
}, [creator.id, groupId])
const [ante, _setAnte] = useState(FIXED_ANTE)
// If params.closeTime is set, extract out the specified date and time
@ -152,7 +155,7 @@ export function NewContract(props: {
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
: undefined
const balance = creator?.balance || 0
const balance = creator.balance || 0
const min = minString ? parseFloat(minString) : undefined
const max = maxString ? parseFloat(maxString) : undefined
@ -214,7 +217,7 @@ export function NewContract(props: {
async function submit() {
// TODO: Tell users why their contract is invalid
if (!creator || !isValid) return
if (!isValid) return
setIsSubmitting(true)
try {
const result = await createMarket(
@ -249,8 +252,6 @@ export function NewContract(props: {
}
}
if (!creator) return <></>
return (
<div>
<label className="label">

View File

@ -11,10 +11,11 @@ import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title'
import { Subtitle } from 'web/components/subtitle'
import { useUser } from 'web/hooks/use-user'
import { getUser } from 'web/lib/firebase/users'
import { useUserManalinks } from 'web/lib/firebase/manalinks'
import { useUserById } from 'web/hooks/use-user'
import { ManalinkTxn } from 'common/txn'
import { User } from 'common/user'
import { Avatar } from 'web/components/avatar'
import { RelativeTimestamp } from 'web/components/relative-timestamp'
import { UserLink } from 'web/components/user-page'
@ -27,15 +28,19 @@ import { Manalink } from 'common/manalink'
import { REFERRAL_AMOUNT } from 'common/user'
const LINKS_PER_PAGE = 24
export const getServerSideProps = redirectIfLoggedOut('/')
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
const user = await getUser(creds.user.uid)
return { props: { user } }
})
export function getManalinkUrl(slug: string) {
return `${location.protocol}//${location.host}/link/${slug}`
}
export default function LinkPage() {
const user = useUser()
const links = useUserManalinks(user?.id ?? '')
export default function LinkPage(props: { user: User }) {
const { user } = props
const links = useUserManalinks(user.id ?? '')
// const manalinkTxns = useManalinkTxns(user?.id ?? '')
const [highlightedSlug, setHighlightedSlug] = useState('')
const unclaimedLinks = links.filter(
@ -44,10 +49,6 @@ export default function LinkPage() {
(l.expiresTime == null || l.expiresTime > Date.now())
)
if (user == null) {
return null
}
return (
<Page>
<SEO

View File

@ -1,5 +1,4 @@
import { Tabs } from 'web/components/layout/tabs'
import { usePrivateUser } from 'web/hooks/use-user'
import React, { useEffect, useMemo, useState } from 'react'
import { Notification, notification_source_types } from 'common/notification'
import { Avatar, EmptyAvatar } from 'web/components/avatar'
@ -13,9 +12,8 @@ import {
MANIFOLD_AVATAR_URL,
MANIFOLD_USERNAME,
PrivateUser,
User,
} from 'common/user'
import { getUser } from 'web/lib/firebase/users'
import { getPrivateUser } from 'web/lib/firebase/users'
import clsx from 'clsx'
import { RelativeTimestamp } from 'web/components/relative-timestamp'
import { Linkify } from 'web/components/linkify'
@ -35,7 +33,6 @@ import { formatMoney } from 'common/util/format'
import { groupPath } from 'web/lib/firebase/groups'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy, sum, uniq } from 'lodash'
import Custom404 from 'web/pages/404'
import { track } from '@amplitude/analytics-browser'
import { Pagination } from 'web/components/pagination'
import { useWindowSize } from 'web/hooks/use-window-size'
@ -49,13 +46,12 @@ const MULTIPLE_USERS_KEY = 'multipleUsers'
const HIGHLIGHT_CLASS = 'bg-indigo-50'
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
const user = await getUser(creds.user.uid)
return { props: { user } }
const privateUser = await getPrivateUser(creds.user.uid)
return { props: { privateUser } }
})
export default function Notifications(props: { user: User }) {
const { user } = props
const privateUser = usePrivateUser(user?.id)
export default function Notifications(props: { privateUser: PrivateUser }) {
const { privateUser } = props
const local = safeLocalStorage()
let localNotifications = [] as Notification[]
const localSavedNotificationGroups = local?.getItem('notification-groups')
@ -67,7 +63,6 @@ export default function Notifications(props: { user: User }) {
.flat()
}
if (!user) return <Custom404 />
return (
<Page>
<div className={'px-2 pt-4 sm:px-4 lg:pt-0'}>
@ -81,17 +76,11 @@ export default function Notifications(props: { user: User }) {
tabs={[
{
title: 'Notifications',
content: privateUser ? (
content: (
<NotificationsList
privateUser={privateUser}
cachedNotifications={localNotifications}
/>
) : (
<div className={'min-h-[100vh]'}>
<RenderNotificationGroups
notificationGroups={localNotificationGroups}
/>
</div>
),
},
{

View File

@ -1,25 +1,35 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { RefreshIcon } from '@heroicons/react/outline'
import { AddFundsButton } from 'web/components/add-funds-button'
import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title'
import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { changeUserInfo } from 'web/lib/firebase/api'
import { uploadImage } from 'web/lib/firebase/storage'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { User } from 'common/user'
import { updateUser, updatePrivateUser } from 'web/lib/firebase/users'
import { User, PrivateUser } from 'common/user'
import {
getUser,
getPrivateUser,
updateUser,
updatePrivateUser,
} from 'web/lib/firebase/users'
import { defaultBannerUrl } from 'web/components/user-page'
import { SiteLink } from 'web/components/site-link'
import Textarea from 'react-expanding-textarea'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
export const getServerSideProps = redirectIfLoggedOut('/')
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
const [user, privateUser] = await Promise.all([
getUser(creds.user.uid),
getPrivateUser(creds.user.uid),
])
return { props: { user, privateUser } }
})
function EditUserField(props: {
user: User
@ -58,64 +68,45 @@ function EditUserField(props: {
)
}
export default function ProfilePage() {
const user = useUser()
const privateUser = usePrivateUser(user?.id)
const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '')
export default function ProfilePage(props: {
user: User
privateUser: PrivateUser
}) {
const { user, privateUser } = props
const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '')
const [avatarLoading, setAvatarLoading] = useState(false)
const [name, setName] = useState(user?.name || '')
const [username, setUsername] = useState(user?.username || '')
const [apiKey, setApiKey] = useState(privateUser?.apiKey || '')
useEffect(() => {
if (user) {
setAvatarUrl(user.avatarUrl || '')
setName(user.name || '')
setUsername(user.username || '')
}
}, [user])
useEffect(() => {
if (privateUser) {
setApiKey(privateUser.apiKey || '')
}
}, [privateUser])
const [name, setName] = useState(user.name)
const [username, setUsername] = useState(user.username)
const [apiKey, setApiKey] = useState(privateUser.apiKey || '')
const updateDisplayName = async () => {
const newName = cleanDisplayName(name)
if (newName) {
setName(newName)
await changeUserInfo({ name: newName }).catch((_) =>
setName(user?.name || '')
)
await changeUserInfo({ name: newName }).catch((_) => setName(user.name))
} else {
setName(user?.name || '')
setName(user.name)
}
}
const updateUsername = async () => {
const newUsername = cleanUsername(username)
if (newUsername) {
setUsername(newUsername)
await changeUserInfo({ username: newUsername }).catch((_) =>
setUsername(user?.username || '')
setUsername(user.username)
)
} else {
setUsername(user?.username || '')
setUsername(user.username)
}
}
const updateApiKey = async (e: React.MouseEvent) => {
const newApiKey = crypto.randomUUID()
if (user?.id != null) {
setApiKey(newApiKey)
await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => {
setApiKey(privateUser?.apiKey || '')
})
}
setApiKey(newApiKey)
await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => {
setApiKey(privateUser.apiKey || '')
})
e.preventDefault()
}
@ -124,7 +115,7 @@ export default function ProfilePage() {
setAvatarLoading(true)
await uploadImage(user?.username || 'default', file)
await uploadImage(user.username, file)
.then(async (url) => {
await changeUserInfo({ avatarUrl: url })
setAvatarUrl(url)
@ -132,14 +123,10 @@ export default function ProfilePage() {
})
.catch(() => {
setAvatarLoading(false)
setAvatarUrl(user?.avatarUrl || '')
setAvatarUrl(user.avatarUrl || '')
})
}
if (user == null) {
return <></>
}
return (
<Page>
<SEO title="Profile" description="User profile settings" url="/profile" />
@ -147,7 +134,7 @@ export default function ProfilePage() {
<Col className="max-w-lg rounded bg-white p-6 shadow-md sm:mx-auto">
<Row className="justify-between">
<Title className="!mt-0" text="Edit Profile" />
<SiteLink className="btn btn-primary" href={`/${user?.username}`}>
<SiteLink className="btn btn-primary" href={`/${user.username}`}>
Done
</SiteLink>
</Row>
@ -192,54 +179,53 @@ export default function ProfilePage() {
/>
</div>
{user && (
<>
{/* TODO: Allow users with M$ 2000 of assets to set custom banners */}
{/* <EditUserField
{/* 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)
})`,
}}
/>
<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'],
['website', 'Website URL'],
['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'],
] as const
).map(([field, label]) => (
<EditUserField user={user} field={field} label={label} />
))}
</>
)}
{(
[
['bio', 'Bio'],
['website', 'Website URL'],
['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'],
] as const
).map(([field, label]) => (
<EditUserField
key={field}
user={user}
field={field}
label={label}
/>
))}
<div>
<label className="label">Email</label>
<div className="ml-1 text-gray-500">
{privateUser?.email ?? '\u00a0'}
{privateUser.email ?? '\u00a0'}
</div>
</div>
<div>
<label className="label">Balance</label>
<Row className="ml-1 items-start gap-4 text-gray-500">
{formatMoney(user?.balance || 0)}
{formatMoney(user.balance)}
<AddFundsButton />
</Row>
</div>