From 527d00cafc3360d229361f4232ed80e17f6fab1a Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 16 Feb 2022 15:54:56 -0800 Subject: [PATCH] Allow edits to their user page --- common/user.ts | 7 ++++ common/util/random.ts | 26 ++++++------ firestore.rules | 3 ++ web/components/site-link.tsx | 5 ++- web/components/user-page.tsx | 77 +++++++++++++++++++++++++++--------- web/lib/firebase/users.ts | 8 +++- web/pages/profile.tsx | 70 ++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 35 deletions(-) diff --git a/common/user.ts b/common/user.ts index ccaf2414..73186ac8 100644 --- a/common/user.ts +++ b/common/user.ts @@ -6,6 +6,13 @@ export type User = { username: string avatarUrl?: string + // For their user page + bio?: string + bannerUrl?: string + website?: string + twitterHandle?: string + discordHandle?: string + balance: number totalDeposits: number totalPnLCached: number diff --git a/common/util/random.ts b/common/util/random.ts index ce2bc54a..740379e5 100644 --- a/common/util/random.ts +++ b/common/util/random.ts @@ -3,22 +3,22 @@ export const randomString = (length = 12) => .toString(16) .substring(2, length + 2) +export function genHash(str: string) { + // xmur3 + for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) { + h = Math.imul(h ^ str.charCodeAt(i), 3432918353) + h = (h << 13) | (h >>> 19) + } + return function () { + h = Math.imul(h ^ (h >>> 16), 2246822507) + h = Math.imul(h ^ (h >>> 13), 3266489909) + return (h ^= h >>> 16) >>> 0 + } +} + export function createRNG(seed: string) { // https://stackoverflow.com/a/47593316/1592933 - function genHash(str: string) { - // xmur3 - for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) { - h = Math.imul(h ^ str.charCodeAt(i), 3432918353) - h = (h << 13) | (h >>> 19) - } - return function () { - h = Math.imul(h ^ (h >>> 16), 2246822507) - h = Math.imul(h ^ (h >>> 13), 3266489909) - return (h ^= h >>> 16) >>> 0 - } - } - const gen = genHash(seed) let [a, b, c, d] = [gen(), gen(), gen(), gen()] diff --git a/firestore.rules b/firestore.rules index a626ce1f..e766f15f 100644 --- a/firestore.rules +++ b/firestore.rules @@ -13,6 +13,9 @@ service cloud.firestore { match /users/{userId} { allow read; + allow update: if resource.data.id == request.auth.uid + || request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle']); } match /private-users/{userId} { diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index 79f43118..c59e106a 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -12,10 +12,11 @@ export const SiteLink = (props: { e.stopPropagation()} > {children} @@ -24,7 +25,7 @@ export const SiteLink = (props: { @@ -50,10 +51,11 @@ export function UserPage(props: { user: User; currentUser?: User }) {
+ {/* TODO: add a white ring to the avatar */}
@@ -64,25 +66,50 @@ export function UserPage(props: { user: User; currentUser?: User }) {
- +
- -
- - Twitter - akrolsmir - - + + {user.website && ( + + + + {user.website} + + + )} - - - Discord - akrolsmir#4125 - - - + {user.twitterHandle && ( + + + Twitter + + {user.twitterHandle} + + + + )} + + {user.discordHandle && ( + + + Discord + + {user.discordHandle} + + + + )} + @@ -91,3 +118,15 @@ export function UserPage(props: { user: User; currentUser?: User }) { ) } + +// Assign each user to a random default banner based on the hash of userId +export function defaultBannerUrl(userId: string) { + const defaultBanner = [ + 'https://images.unsplash.com/photo-1501523460185-2aa5d2a0f981?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2131&q=80', + 'https://images.unsplash.com/photo-1458682625221-3a45f8a844c7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1974&q=80', + 'https://images.unsplash.com/photo-1558517259-165ae4b10f7f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2080&q=80', + 'https://images.unsplash.com/photo-1563260797-cb5cd70254c8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2069&q=80', + 'https://images.unsplash.com/photo-1603399587513-136aa9398f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1467&q=80', + ] + return defaultBanner[genHash(userId)() % defaultBanner.length] +} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 56403358..04d82571 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -9,6 +9,7 @@ import { limit, getDocs, orderBy, + updateDoc, } from 'firebase/firestore' import { getAuth } from 'firebase/auth' import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage' @@ -21,7 +22,8 @@ import { import { app } from './init' import { PrivateUser, User } from '../../../common/user' import { createUser } from './api-call' -import { getValue, getValues, listenForValue, listenForValues } from './utils' +import { getValues, listenForValue, listenForValues } from './utils' + export type { User } const db = getFirestore(app) @@ -45,6 +47,10 @@ export async function setUser(userId: string, user: User) { await setDoc(doc(db, 'users', userId), user) } +export async function updateUser(userId: string, update: Partial) { + await updateDoc(doc(db, 'users', userId), { ...update }) +} + export function listenForUser( userId: string, setUser: (user: User | null) => void diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 036f2972..34cbe555 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -16,6 +16,40 @@ 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' +import { User } from '../../common/user' +import { defaultBannerUrl, updateUser } from '../lib/firebase/users' + +function EditUserField(props: { + user: User + field: 'bio' | 'bannerUrl' | 'twitterHandle' | 'discordHandle' + label: string + isEditing: boolean +}) { + const { user, field, label, isEditing } = props + const [value, setValue] = useState(user[field] ?? '') + + async function updateField() { + await updateUser(user.id, { [field]: value }) + } + + return ( +
+ + + {isEditing ? ( + setValue(e.target.value || '')} + onBlur={updateField} + /> + ) : ( +
{value || '-'}
+ )} +
+ ) +} export default function ProfilePage() { const user = useUser() @@ -166,6 +200,42 @@ export default function ProfilePage() { )}
+ {user && ( + <> + {/* TODO: Allow users with M$ 2000 of assets to set custom banners */} + {/* */} + +
+ + {[ + ['bio', 'Bio'], + ['website', 'Website URL'], + ['twitterHandle', 'Twitter'], + ['discordHandle', 'Discord'], + ].map(([field, label]) => ( + + ))} + + )} +