Allow edits to their user page
This commit is contained in:
parent
7a87138d1c
commit
527d00cafc
|
@ -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
|
||||
|
|
|
@ -3,10 +3,7 @@ export const randomString = (length = 12) =>
|
|||
.toString(16)
|
||||
.substring(2, length + 2)
|
||||
|
||||
export function createRNG(seed: string) {
|
||||
// https://stackoverflow.com/a/47593316/1592933
|
||||
|
||||
function genHash(str: string) {
|
||||
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)
|
||||
|
@ -19,6 +16,9 @@ export function createRNG(seed: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function createRNG(seed: string) {
|
||||
// https://stackoverflow.com/a/47593316/1592933
|
||||
|
||||
const gen = genHash(seed)
|
||||
let [a, b, c, d] = [gen(), gen(), gen(), gen()]
|
||||
|
||||
|
|
|
@ -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} {
|
||||
|
|
|
@ -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' }}
|
||||
|
|
|
@ -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`}>
|
||||
<Col className="sm:flex-row sm:gap-4">
|
||||
{user.website && (
|
||||
<SiteLink href={user.website}>
|
||||
<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>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span className="text-sm text-gray-500">{user.website}</span>
|
||||
</Row>
|
||||
</a>
|
||||
</SiteLink>
|
||||
)}
|
||||
|
||||
<a href="https://discord.com/invite/eHQBNBqXuh">
|
||||
{user.twitterHandle && (
|
||||
<SiteLink href={`https://twitter.com/${user.twitterHandle}`}>
|
||||
<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>
|
||||
<img
|
||||
src="/twitter-logo.svg"
|
||||
className="h-4 w-4"
|
||||
alt="Twitter"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
{user.twitterHandle}
|
||||
</span>
|
||||
</Row>
|
||||
</a>
|
||||
</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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue
Block a user