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()! 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) { export async function getUserByUsername(username: string) {
// Find a user whose username matches the given username, or null if no such user exists. // 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)) const q = query(users, where('username', '==', username), limit(1))

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { Tabs } from 'web/components/layout/tabs' import { Tabs } from 'web/components/layout/tabs'
import { usePrivateUser } from 'web/hooks/use-user'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { Notification, notification_source_types } from 'common/notification' import { Notification, notification_source_types } from 'common/notification'
import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Avatar, EmptyAvatar } from 'web/components/avatar'
@ -13,9 +12,8 @@ import {
MANIFOLD_AVATAR_URL, MANIFOLD_AVATAR_URL,
MANIFOLD_USERNAME, MANIFOLD_USERNAME,
PrivateUser, PrivateUser,
User,
} from 'common/user' } from 'common/user'
import { getUser } from 'web/lib/firebase/users' import { getPrivateUser } from 'web/lib/firebase/users'
import clsx from 'clsx' import clsx from 'clsx'
import { RelativeTimestamp } from 'web/components/relative-timestamp' import { RelativeTimestamp } from 'web/components/relative-timestamp'
import { Linkify } from 'web/components/linkify' import { Linkify } from 'web/components/linkify'
@ -35,7 +33,6 @@ import { formatMoney } from 'common/util/format'
import { groupPath } from 'web/lib/firebase/groups' import { groupPath } from 'web/lib/firebase/groups'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy, sum, uniq } from 'lodash' import { groupBy, sum, uniq } from 'lodash'
import Custom404 from 'web/pages/404'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
import { Pagination } from 'web/components/pagination' import { Pagination } from 'web/components/pagination'
import { useWindowSize } from 'web/hooks/use-window-size' import { useWindowSize } from 'web/hooks/use-window-size'
@ -49,13 +46,12 @@ const MULTIPLE_USERS_KEY = 'multipleUsers'
const HIGHLIGHT_CLASS = 'bg-indigo-50' const HIGHLIGHT_CLASS = 'bg-indigo-50'
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => { export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
const user = await getUser(creds.user.uid) const privateUser = await getPrivateUser(creds.user.uid)
return { props: { user } } return { props: { privateUser } }
}) })
export default function Notifications(props: { user: User }) { export default function Notifications(props: { privateUser: PrivateUser }) {
const { user } = props const { privateUser } = props
const privateUser = usePrivateUser(user?.id)
const local = safeLocalStorage() const local = safeLocalStorage()
let localNotifications = [] as Notification[] let localNotifications = [] as Notification[]
const localSavedNotificationGroups = local?.getItem('notification-groups') const localSavedNotificationGroups = local?.getItem('notification-groups')
@ -67,7 +63,6 @@ export default function Notifications(props: { user: User }) {
.flat() .flat()
} }
if (!user) return <Custom404 />
return ( return (
<Page> <Page>
<div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}>
@ -81,17 +76,11 @@ export default function Notifications(props: { user: User }) {
tabs={[ tabs={[
{ {
title: 'Notifications', title: 'Notifications',
content: privateUser ? ( content: (
<NotificationsList <NotificationsList
privateUser={privateUser} privateUser={privateUser}
cachedNotifications={localNotifications} 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 { RefreshIcon } from '@heroicons/react/outline'
import { AddFundsButton } from 'web/components/add-funds-button' import { AddFundsButton } from 'web/components/add-funds-button'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { changeUserInfo } from 'web/lib/firebase/api' import { changeUserInfo } from 'web/lib/firebase/api'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { User } from 'common/user' import { User, PrivateUser } from 'common/user'
import { updateUser, updatePrivateUser } from 'web/lib/firebase/users' import {
getUser,
getPrivateUser,
updateUser,
updatePrivateUser,
} from 'web/lib/firebase/users'
import { defaultBannerUrl } from 'web/components/user-page' import { defaultBannerUrl } from 'web/components/user-page'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' 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: { function EditUserField(props: {
user: User user: User
@ -58,64 +68,45 @@ function EditUserField(props: {
) )
} }
export default function ProfilePage() { export default function ProfilePage(props: {
const user = useUser() user: User
const privateUser = usePrivateUser(user?.id) privateUser: PrivateUser
}) {
const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '') const { user, privateUser } = props
const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '')
const [avatarLoading, setAvatarLoading] = useState(false) const [avatarLoading, setAvatarLoading] = useState(false)
const [name, setName] = useState(user?.name || '') const [name, setName] = useState(user.name)
const [username, setUsername] = useState(user?.username || '') const [username, setUsername] = useState(user.username)
const [apiKey, setApiKey] = useState(privateUser?.apiKey || '') 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 updateDisplayName = async () => { const updateDisplayName = async () => {
const newName = cleanDisplayName(name) const newName = cleanDisplayName(name)
if (newName) { if (newName) {
setName(newName) setName(newName)
await changeUserInfo({ name: newName }).catch((_) => await changeUserInfo({ name: newName }).catch((_) => setName(user.name))
setName(user?.name || '')
)
} else { } else {
setName(user?.name || '') setName(user.name)
} }
} }
const updateUsername = async () => { const updateUsername = async () => {
const newUsername = cleanUsername(username) const newUsername = cleanUsername(username)
if (newUsername) { if (newUsername) {
setUsername(newUsername) setUsername(newUsername)
await changeUserInfo({ username: newUsername }).catch((_) => await changeUserInfo({ username: newUsername }).catch((_) =>
setUsername(user?.username || '') setUsername(user.username)
) )
} else { } else {
setUsername(user?.username || '') setUsername(user.username)
} }
} }
const updateApiKey = async (e: React.MouseEvent) => { const updateApiKey = async (e: React.MouseEvent) => {
const newApiKey = crypto.randomUUID() const newApiKey = crypto.randomUUID()
if (user?.id != null) { setApiKey(newApiKey)
setApiKey(newApiKey) await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => {
await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => { setApiKey(privateUser.apiKey || '')
setApiKey(privateUser?.apiKey || '') })
})
}
e.preventDefault() e.preventDefault()
} }
@ -124,7 +115,7 @@ export default function ProfilePage() {
setAvatarLoading(true) setAvatarLoading(true)
await uploadImage(user?.username || 'default', file) await uploadImage(user.username, file)
.then(async (url) => { .then(async (url) => {
await changeUserInfo({ avatarUrl: url }) await changeUserInfo({ avatarUrl: url })
setAvatarUrl(url) setAvatarUrl(url)
@ -132,14 +123,10 @@ export default function ProfilePage() {
}) })
.catch(() => { .catch(() => {
setAvatarLoading(false) setAvatarLoading(false)
setAvatarUrl(user?.avatarUrl || '') setAvatarUrl(user.avatarUrl || '')
}) })
} }
if (user == null) {
return <></>
}
return ( return (
<Page> <Page>
<SEO title="Profile" description="User profile settings" url="/profile" /> <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"> <Col className="max-w-lg rounded bg-white p-6 shadow-md sm:mx-auto">
<Row className="justify-between"> <Row className="justify-between">
<Title className="!mt-0" text="Edit Profile" /> <Title className="!mt-0" text="Edit Profile" />
<SiteLink className="btn btn-primary" href={`/${user?.username}`}> <SiteLink className="btn btn-primary" href={`/${user.username}`}>
Done Done
</SiteLink> </SiteLink>
</Row> </Row>
@ -192,54 +179,53 @@ export default function ProfilePage() {
/> />
</div> </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} user={user}
field="bannerUrl" field="bannerUrl"
label="Banner Url" label="Banner Url"
isEditing={isEditing} isEditing={isEditing}
/> */} /> */}
<label className="label"> <label className="label">
Banner image{' '} Banner image{' '}
<span className="text-sm text-gray-400"> <span className="text-sm text-gray-400">Not editable for now</span>
Not editable for now </label>
</span> <div
</label> className="h-32 w-full bg-cover bg-center sm:h-40"
<div style={{
className="h-32 w-full bg-cover bg-center sm:h-40" backgroundImage: `url(${
style={{ user.bannerUrl || defaultBannerUrl(user.id)
backgroundImage: `url(${ })`,
user.bannerUrl || defaultBannerUrl(user.id) }}
})`, />
}}
/>
{( {(
[ [
['bio', 'Bio'], ['bio', 'Bio'],
['website', 'Website URL'], ['website', 'Website URL'],
['twitterHandle', 'Twitter'], ['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'], ['discordHandle', 'Discord'],
] as const ] as const
).map(([field, label]) => ( ).map(([field, label]) => (
<EditUserField user={user} field={field} label={label} /> <EditUserField
))} key={field}
</> user={user}
)} field={field}
label={label}
/>
))}
<div> <div>
<label className="label">Email</label> <label className="label">Email</label>
<div className="ml-1 text-gray-500"> <div className="ml-1 text-gray-500">
{privateUser?.email ?? '\u00a0'} {privateUser.email ?? '\u00a0'}
</div> </div>
</div> </div>
<div> <div>
<label className="label">Balance</label> <label className="label">Balance</label>
<Row className="ml-1 items-start gap-4 text-gray-500"> <Row className="ml-1 items-start gap-4 text-gray-500">
{formatMoney(user?.balance || 0)} {formatMoney(user.balance)}
<AddFundsButton /> <AddFundsButton />
</Row> </Row>
</div> </div>