Allow edits to their user page

This commit is contained in:
Austin Chen 2022-02-16 15:54:56 -08:00
parent 7a87138d1c
commit 527d00cafc
7 changed files with 161 additions and 35 deletions

View File

@ -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

View File

@ -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()]

View File

@ -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} {

View File

@ -12,10 +12,11 @@ export const SiteLink = (props: {
<a
href={href}
className={clsx(
'break-words z-10 hover:underline hover:decoration-indigo-400 hover:decoration-2',
'z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2',
className
)}
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
target="_blank"
onClick={(e) => e.stopPropagation()}
>
{children}
@ -24,7 +25,7 @@ export const SiteLink = (props: {
<Link href={href}>
<a
className={clsx(
'break-words z-10 hover:underline hover:decoration-indigo-400 hover:decoration-2',
'z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2',
className
)}
style={{ /* For iOS safari */ wordBreak: 'break-word' }}

View File

@ -9,6 +9,8 @@ import { Col } from './layout/col'
import { Linkify } from './linkify'
import { Spacer } from './layout/spacer'
import { Row } from './layout/row'
import { LinkIcon } from '@heroicons/react/solid'
import { genHash } from '../../common/util/random'
export function UserLink(props: {
name: string
@ -33,10 +35,9 @@ export function UserPage(props: { user: User; currentUser?: User }) {
const possesive = isCurrentUser ? 'Your ' : `${user.name}'s `
const bannerImageUrl =
'https://images.unsplash.com/photo-1548197253-652ffe79752c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1975&q=80'
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
const placeholderBio = `Hi! Always happy to chat; reach out at akrolsmir@gmail.com, or find a time on https://calendly.com/austinchen/manifold !`
const placeholderBio = `I... haven't gotten around to writing a bio yet 😛`
return (
<Page>
@ -50,10 +51,11 @@ export function UserPage(props: { user: User; currentUser?: User }) {
<div
className="h-32 w-full bg-cover bg-center sm:h-40"
style={{
backgroundImage: `url(${bannerImageUrl})`,
backgroundImage: `url(${bannerUrl})`,
}}
/>
<div className="relative -top-10 left-4">
{/* TODO: add a white ring to the avatar */}
<Avatar username={user.username} avatarUrl={user.avatarUrl} size={20} />
</div>
@ -64,25 +66,50 @@ export function UserPage(props: { user: User; currentUser?: User }) {
<Spacer h={4} />
<div>
<Linkify text={placeholderBio}></Linkify>
<Linkify text={user.bio || placeholderBio}></Linkify>
</div>
<Spacer h={4} />
<Row className="gap-4">
<a href={`https://twitter.com/akrolsmir`}>
<Row className="items-center gap-1">
<img src="/twitter-logo.svg" className="h-4 w-4" alt="Twitter" />
<span className="text-sm text-gray-500">akrolsmir</span>
</Row>
</a>
<Col className="sm:flex-row sm:gap-4">
{user.website && (
<SiteLink href={user.website}>
<Row className="items-center gap-1">
<LinkIcon className="h-4 w-4" />
<span className="text-sm text-gray-500">{user.website}</span>
</Row>
</SiteLink>
)}
<a href="https://discord.com/invite/eHQBNBqXuh">
<Row className="items-center gap-1">
<img src="/discord-logo.svg" className="h-4 w-4" alt="Discord" />
<span className="text-sm text-gray-500">akrolsmir#4125</span>
</Row>
</a>
</Row>
{user.twitterHandle && (
<SiteLink href={`https://twitter.com/${user.twitterHandle}`}>
<Row className="items-center gap-1">
<img
src="/twitter-logo.svg"
className="h-4 w-4"
alt="Twitter"
/>
<span className="text-sm text-gray-500">
{user.twitterHandle}
</span>
</Row>
</SiteLink>
)}
{user.discordHandle && (
<SiteLink href="https://discord.com/invite/eHQBNBqXuh">
<Row className="items-center gap-1">
<img
src="/discord-logo.svg"
className="h-4 w-4"
alt="Discord"
/>
<span className="text-sm text-gray-500">
{user.discordHandle}
</span>
</Row>
</SiteLink>
)}
</Col>
<Spacer h={10} />
@ -91,3 +118,15 @@ export function UserPage(props: { user: User; currentUser?: User }) {
</Page>
)
}
// 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]
}

View File

@ -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<User>) {
await updateDoc(doc(db, 'users', userId), { ...update })
}
export function listenForUser(
userId: string,
setUser: (user: User | null) => void

View File

@ -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 (
<div>
<label className="label">{label}</label>
{isEditing ? (
<input
type="text"
className="input input-bordered"
value={value}
onChange={(e) => setValue(e.target.value || '')}
onBlur={updateField}
/>
) : (
<div className="ml-1 break-words text-gray-500">{value || '-'}</div>
)}
</div>
)
}
export default function ProfilePage() {
const user = useUser()
@ -166,6 +200,42 @@ export default function ProfilePage() {
)}
</div>
{user && (
<>
{/* 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</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'],
].map(([field, label]) => (
<EditUserField
user={user}
// @ts-ignore
field={field}
label={label}
isEditing={isEditing}
/>
))}
</>
)}
<div>
<label className="label">Email</label>
<div className="ml-1 text-gray-500">