User profile (#44)
* add id, userId to comment * change user info cloud function and script; move cleanUsername to common * change user info script * fix rules * add fund button: useLocation hook * profile page * merge * profile stuff * avatar uploading to storage bucket * changeUserInfo: use transaction * Styles for profile page * Edit mode for profile, and more styles Co-authored-by: James Grugett <jahooma@gmail.com>
This commit is contained in:
parent
63de359d22
commit
03f36cf954
|
@ -1,10 +1,14 @@
|
||||||
// Currently, comments are created after the bet, not atomically with the bet.
|
// Currently, comments are created after the bet, not atomically with the bet.
|
||||||
// They're uniquely identified by the pair contractId/betId.
|
// They're uniquely identified by the pair contractId/betId.
|
||||||
export type Comment = {
|
export type Comment = {
|
||||||
|
id: string
|
||||||
contractId: string
|
contractId: string
|
||||||
betId: string
|
betId: string
|
||||||
|
userId: string
|
||||||
|
|
||||||
text: string
|
text: string
|
||||||
createdTime: number
|
createdTime: number
|
||||||
|
|
||||||
// Denormalized, for rendering comments
|
// Denormalized, for rendering comments
|
||||||
userName?: string
|
userName?: string
|
||||||
userUsername?: string
|
userUsername?: string
|
||||||
|
|
12
common/util/clean-username.ts
Normal file
12
common/util/clean-username.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export const cleanUsername = (name: string, maxLength = 25) => {
|
||||||
|
return name
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.normalize('NFD') // split an accented letter in the base letter and the acent
|
||||||
|
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
|
||||||
|
.replace(/[^A-Za-z0-9_]/g, '') // remove all chars not letters, numbers and underscores
|
||||||
|
.substring(0, maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanDisplayName = (displayName: string, maxLength = 25) => {
|
||||||
|
return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim()
|
||||||
|
}
|
101
functions/src/change-user-info.ts
Normal file
101
functions/src/change-user-info.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { getUser, removeUndefinedProps } from './utils'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { Comment } from '../../common/comment'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { cleanUsername } from '../../common/util/clean-username'
|
||||||
|
|
||||||
|
export const changeUserInfo = functions
|
||||||
|
.runWith({ minInstances: 1 })
|
||||||
|
.https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
username?: string
|
||||||
|
name?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const user = await getUser(userId)
|
||||||
|
if (!user) return { status: 'error', message: 'User not found' }
|
||||||
|
|
||||||
|
const { username, name, avatarUrl } = data
|
||||||
|
|
||||||
|
return await changeUser(user, { username, name, avatarUrl })
|
||||||
|
.then(() => {
|
||||||
|
console.log('succesfully changed', user.username, 'to', data)
|
||||||
|
return { status: 'success' }
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log('Error', e.message)
|
||||||
|
return { status: 'error', message: e.message }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const changeUser = async (
|
||||||
|
user: User,
|
||||||
|
update: {
|
||||||
|
username?: string
|
||||||
|
name?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return await firestore.runTransaction(async (transaction) => {
|
||||||
|
if (update.username) {
|
||||||
|
update.username = cleanUsername(update.username)
|
||||||
|
if (!update.username) {
|
||||||
|
throw new Error('Invalid username')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameNameUser = await transaction.get(
|
||||||
|
firestore.collection('users').where('username', '==', update.username)
|
||||||
|
)
|
||||||
|
if (!sameNameUser.empty) {
|
||||||
|
throw new Error('Username already exists')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRef = firestore.collection('users').doc(user.id)
|
||||||
|
const userUpdate: Partial<User> = removeUndefinedProps(update)
|
||||||
|
|
||||||
|
const contractsRef = firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('creatorId', '==', user.id)
|
||||||
|
|
||||||
|
const contracts = await transaction.get(contractsRef)
|
||||||
|
|
||||||
|
const contractUpdate: Partial<Contract> = removeUndefinedProps({
|
||||||
|
creatorName: update.name,
|
||||||
|
creatorUsername: update.username,
|
||||||
|
creatorAvatarUrl: update.avatarUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
const commentSnap = await transaction.get(
|
||||||
|
firestore
|
||||||
|
.collectionGroup('comments')
|
||||||
|
.where('userUsername', '==', user.username)
|
||||||
|
)
|
||||||
|
|
||||||
|
const commentUpdate: Partial<Comment> = removeUndefinedProps({
|
||||||
|
userName: update.name,
|
||||||
|
userUsername: update.username,
|
||||||
|
userAvatarUrl: update.avatarUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
await transaction.update(userRef, userUpdate)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate))
|
||||||
|
)
|
||||||
|
|
||||||
|
await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -9,6 +9,10 @@ import {
|
||||||
} from '../../common/user'
|
} from '../../common/user'
|
||||||
import { getUser, getUserByUsername } from './utils'
|
import { getUser, getUserByUsername } from './utils'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
import {
|
||||||
|
cleanDisplayName,
|
||||||
|
cleanUsername,
|
||||||
|
} from '../../common/util/clean-username'
|
||||||
|
|
||||||
export const createUser = functions
|
export const createUser = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
|
@ -29,7 +33,8 @@ export const createUser = functions
|
||||||
const email = fbUser.email
|
const email = fbUser.email
|
||||||
const emailName = email?.replace(/@.*$/, '')
|
const emailName = email?.replace(/@.*$/, '')
|
||||||
|
|
||||||
const name = fbUser.displayName || emailName || 'User' + randomString(4)
|
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
|
||||||
|
const name = cleanDisplayName(rawName)
|
||||||
let username = cleanUsername(name)
|
let username = cleanUsername(name)
|
||||||
|
|
||||||
const sameNameUser = await getUserByUsername(username)
|
const sameNameUser = await getUserByUsername(username)
|
||||||
|
@ -77,14 +82,6 @@ export const createUser = functions
|
||||||
return { status: 'success', user }
|
return { status: 'success', user }
|
||||||
})
|
})
|
||||||
|
|
||||||
const cleanUsername = (name: string) => {
|
|
||||||
return name
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.normalize('NFD') // split an accented letter in the base letter and the acent
|
|
||||||
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
|
|
||||||
.replace(/[^A-Za-z0-9_]/g, '') // remove all chars not letters, numbers and underscores
|
|
||||||
}
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
|
const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
|
||||||
|
|
|
@ -17,3 +17,4 @@ export * from './unsubscribe'
|
||||||
export * from './update-contract-metrics'
|
export * from './update-contract-metrics'
|
||||||
export * from './update-user-metrics'
|
export * from './update-user-metrics'
|
||||||
export * from './backup-db'
|
export * from './backup-db'
|
||||||
|
export * from './change-user-info'
|
||||||
|
|
50
functions/src/scripts/change-user-info.ts
Normal file
50
functions/src/scripts/change-user-info.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
// Generate your own private key, and set the path below:
|
||||||
|
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
|
|
||||||
|
// const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
||||||
|
const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json')
|
||||||
|
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount),
|
||||||
|
})
|
||||||
|
|
||||||
|
import { getUserByUsername } from '../utils'
|
||||||
|
import { changeUser } from '../change-user-info'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const username = process.argv[2]
|
||||||
|
const name = process.argv[3]
|
||||||
|
const newUsername = process.argv[4]
|
||||||
|
const avatarUrl = process.argv[5]
|
||||||
|
|
||||||
|
if (process.argv.length < 4) {
|
||||||
|
console.log(
|
||||||
|
'syntax: node change-user-info.js [current username] [new name] [new username] [new avatar]'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserByUsername(username)
|
||||||
|
if (!user) {
|
||||||
|
console.log('username', username, 'could not be found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeUser(user, { username: newUsername, name, avatarUrl })
|
||||||
|
.then(() =>
|
||||||
|
console.log(
|
||||||
|
'succesfully changed',
|
||||||
|
user.username,
|
||||||
|
'to',
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
newUsername
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((e) => console.log(e.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) main().then(() => process.exit())
|
|
@ -77,3 +77,13 @@ export const chargeUser = (userId: string, charge: number) => {
|
||||||
|
|
||||||
return updateUserBalance(userId, -charge)
|
return updateUserBalance(userId, -charge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||||
|
let newObj: any = {}
|
||||||
|
|
||||||
|
for (let key of Object.keys(obj)) {
|
||||||
|
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObj
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { checkoutURL } from '../lib/service/stripe'
|
import { checkoutURL } from '../lib/service/stripe'
|
||||||
|
@ -13,6 +13,8 @@ export function AddFundsButton(props: { className?: string }) {
|
||||||
500 | 1000 | 2500 | 10000
|
500 | 1000 | 2500 | 10000
|
||||||
>(500)
|
>(500)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label
|
<label
|
||||||
|
@ -54,11 +56,7 @@ export function AddFundsButton(props: { className?: string }) {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
action={checkoutURL(
|
action={checkoutURL(user?.id || '', amountSelected, location)}
|
||||||
user?.id || '',
|
|
||||||
amountSelected,
|
|
||||||
window.location.href
|
|
||||||
)}
|
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
@ -74,3 +72,13 @@ export function AddFundsButton(props: { className?: string }) {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed in next js
|
||||||
|
// window not loaded at runtime
|
||||||
|
const useLocation = () => {
|
||||||
|
const [href, setHref] = useState('')
|
||||||
|
useEffect(() => {
|
||||||
|
setHref(window.location.href)
|
||||||
|
}, [])
|
||||||
|
return href
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,10 @@ function getNavigationOptions(
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
href: user ? '/home' : '/',
|
href: user ? '/home' : '/',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
href: '/profile',
|
||||||
|
},
|
||||||
...(mobile
|
...(mobile
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { listenForLogin, listenForUser, User } from '../lib/firebase/users'
|
import { PrivateUser } from '../../common/user'
|
||||||
|
import {
|
||||||
|
listenForLogin,
|
||||||
|
listenForPrivateUser,
|
||||||
|
listenForUser,
|
||||||
|
User,
|
||||||
|
} from '../lib/firebase/users'
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const [user, setUser] = useState<User | null | undefined>(undefined)
|
const [user, setUser] = useState<User | null | undefined>(undefined)
|
||||||
|
@ -14,3 +20,15 @@ export const useUser = () => {
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePrivateUser = (userId?: string) => {
|
||||||
|
const [privateUser, setPrivateUser] = useState<
|
||||||
|
PrivateUser | null | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) return listenForPrivateUser(userId, setPrivateUser)
|
||||||
|
}, [userId])
|
||||||
|
|
||||||
|
return privateUser
|
||||||
|
}
|
||||||
|
|
|
@ -33,3 +33,13 @@ export const createUser: () => Promise<User | null> = () => {
|
||||||
.then((r) => (r.data as any)?.user || null)
|
.then((r) => (r.data as any)?.user || null)
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const changeUserInfo = (data: {
|
||||||
|
username?: string
|
||||||
|
name?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}) => {
|
||||||
|
return cloudFunction('changeUserInfo')(data)
|
||||||
|
.then((r) => r.data as { status: string; message?: string })
|
||||||
|
.catch((e) => ({ status: 'error', message: e.message }))
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
|
||||||
import { getValues, listenForValues } from './utils'
|
import { getValues, listenForValues } from './utils'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
|
@ -20,15 +21,20 @@ export async function createComment(
|
||||||
commenter: User
|
commenter: User
|
||||||
) {
|
) {
|
||||||
const ref = doc(getCommentsCollection(contractId), betId)
|
const ref = doc(getCommentsCollection(contractId), betId)
|
||||||
return await setDoc(ref, {
|
|
||||||
|
const comment: Comment = {
|
||||||
|
id: ref.id,
|
||||||
contractId,
|
contractId,
|
||||||
betId,
|
betId,
|
||||||
|
userId: commenter.id,
|
||||||
text,
|
text,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
userName: commenter.name,
|
userName: commenter.name,
|
||||||
userUsername: commenter.username,
|
userUsername: commenter.username,
|
||||||
userAvatarUrl: commenter.avatarUrl,
|
userAvatarUrl: commenter.avatarUrl,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
return await setDoc(ref, comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCommentsCollection(contractId: string) {
|
function getCommentsCollection(contractId: string) {
|
||||||
|
|
49
web/lib/firebase/storage.ts
Normal file
49
web/lib/firebase/storage.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
getStorage,
|
||||||
|
ref,
|
||||||
|
uploadBytesResumable,
|
||||||
|
getDownloadURL,
|
||||||
|
} from 'firebase/storage'
|
||||||
|
|
||||||
|
const storage = getStorage()
|
||||||
|
|
||||||
|
export const uploadImage = async (
|
||||||
|
username: string,
|
||||||
|
file: File,
|
||||||
|
onProgress?: (progress: number, isRunning: boolean) => void
|
||||||
|
) => {
|
||||||
|
const storageRef = ref(storage, `user-images/${username}/${file.name}`)
|
||||||
|
const uploadTask = uploadBytesResumable(storageRef, file)
|
||||||
|
|
||||||
|
let resolvePromise: (url: string) => void
|
||||||
|
let rejectPromise: (reason?: any) => void
|
||||||
|
|
||||||
|
const promise = new Promise<string>((resolve, reject) => {
|
||||||
|
resolvePromise = resolve
|
||||||
|
rejectPromise = reject
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribe = uploadTask.on(
|
||||||
|
'state_changed',
|
||||||
|
(snapshot) => {
|
||||||
|
const progress = snapshot.bytesTransferred / snapshot.totalBytes
|
||||||
|
const isRunning = snapshot.state === 'running'
|
||||||
|
if (onProgress) onProgress(progress, isRunning)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// A full list of error codes is available at
|
||||||
|
// https://firebase.google.com/docs/storage/web/handle-errors
|
||||||
|
rejectPromise(error)
|
||||||
|
unsubscribe()
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||||
|
resolvePromise(downloadURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return await promise
|
||||||
|
}
|
|
@ -53,6 +53,16 @@ export function listenForUser(
|
||||||
return listenForValue<User>(userRef, setUser)
|
return listenForValue<User>(userRef, setUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForPrivateUser(
|
||||||
|
userId: string,
|
||||||
|
setPrivateUser: (privateUser: PrivateUser) => void
|
||||||
|
) {
|
||||||
|
const userRef = doc(db, 'private-users', userId)
|
||||||
|
return onSnapshot(userRef, (userSnap) => {
|
||||||
|
setPrivateUser(userSnap.data() as PrivateUser)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
||||||
|
|
||||||
// used to avoid weird race condition
|
// used to avoid weird race condition
|
||||||
|
|
181
web/pages/profile.tsx
Normal file
181
web/pages/profile.tsx
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { PencilIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
|
import { AddFundsButton } from '../components/add-funds-button'
|
||||||
|
import { Page } from '../components/page'
|
||||||
|
import { SEO } from '../components/SEO'
|
||||||
|
import { Title } from '../components/title'
|
||||||
|
import { usePrivateUser, useUser } from '../hooks/use-user'
|
||||||
|
import { formatMoney } from '../../common/util/format'
|
||||||
|
import {
|
||||||
|
cleanDisplayName,
|
||||||
|
cleanUsername,
|
||||||
|
} from '../../common/util/clean-username'
|
||||||
|
import { changeUserInfo } from '../lib/firebase/api-call'
|
||||||
|
import { uploadImage } from '../lib/firebase/storage'
|
||||||
|
import { Col } from '../components/layout/col'
|
||||||
|
import { Row } from '../components/layout/row'
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const user = useUser()
|
||||||
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '')
|
||||||
|
const [avatarLoading, setAvatarLoading] = useState(false)
|
||||||
|
const [name, setName] = useState(user?.name || '')
|
||||||
|
const [username, setUsername] = useState(user?.username || '')
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setAvatarUrl(user.avatarUrl || '')
|
||||||
|
setName(user.name || '')
|
||||||
|
setUsername(user.username || '')
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const updateDisplayName = async () => {
|
||||||
|
const newName = cleanDisplayName(name)
|
||||||
|
|
||||||
|
if (newName) {
|
||||||
|
setName(newName)
|
||||||
|
|
||||||
|
await changeUserInfo({ name: newName })
|
||||||
|
.catch(() => ({ status: 'error' }))
|
||||||
|
.then((r) => {
|
||||||
|
if (r.status === 'error') setName(user?.name || '')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setName(user?.name || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUsername = async () => {
|
||||||
|
const newUsername = cleanUsername(username)
|
||||||
|
|
||||||
|
if (newUsername) {
|
||||||
|
setUsername(newUsername)
|
||||||
|
await changeUserInfo({ username: newUsername })
|
||||||
|
.catch(() => ({ status: 'error' }))
|
||||||
|
.then((r) => {
|
||||||
|
if (r.status === 'error') setUsername(user?.username || '')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setUsername(user?.username || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileHandler = async (event: any) => {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
|
||||||
|
setAvatarLoading(true)
|
||||||
|
|
||||||
|
await uploadImage(user?.username || 'default', file)
|
||||||
|
.then(async (url) => {
|
||||||
|
await changeUserInfo({ avatarUrl: url })
|
||||||
|
setAvatarUrl(url)
|
||||||
|
setAvatarLoading(false)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAvatarLoading(false)
|
||||||
|
setAvatarUrl(user?.avatarUrl || '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<SEO title="Profile" description="User profile settings" url="/profile" />
|
||||||
|
|
||||||
|
<Col className="max-w-lg p-6 sm:mx-auto bg-white rounded shadow-md">
|
||||||
|
<Row className="justify-between">
|
||||||
|
<Title className="!mt-0" text="Profile" />
|
||||||
|
{isEditing ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="w-5 h-5" />{' '}
|
||||||
|
<div className="ml-2">Edit</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
<Col className="gap-4">
|
||||||
|
<Row className="items-center gap-4">
|
||||||
|
{avatarLoading ? (
|
||||||
|
<button className="btn btn-ghost btn-lg btn-circle loading"></button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="rounded-full bg-gray-400 flex items-center justify-center"
|
||||||
|
/>
|
||||||
|
{isEditing && (
|
||||||
|
<input type="file" name="file" onChange={fileHandler} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">Display name</label>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Display name"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value || '')}
|
||||||
|
onBlur={updateDisplayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="ml-1 text-gray-500">{name}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">Username</label>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value || '')}
|
||||||
|
onBlur={updateUsername}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="ml-1 text-gray-500">{username}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">Email</label>
|
||||||
|
<div className="ml-1 text-gray-500">
|
||||||
|
{privateUser?.email ?? '\u00a0'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">Balance</label>
|
||||||
|
<Row className="ml-1 gap-4 items-start text-gray-500">
|
||||||
|
{formatMoney(user?.balance || 0)}
|
||||||
|
<AddFundsButton />
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user