Merge branch 'main' into atlas2

This commit is contained in:
Austin Chen 2022-07-04 16:05:22 -07:00
commit 1890ea0287
10 changed files with 229 additions and 137 deletions

View File

@ -21,16 +21,15 @@ service cloud.firestore {
allow update: if resource.data.id == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
allow update: if resource.data.id == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId']) .hasOnly(['referredByUserId'])
// only one referral allowed per user // only one referral allowed per user
&& !("referredByUserId" in resource.data) && !("referredByUserId" in resource.data)
// user can't refer themselves // user can't refer themselves
&& (resource.data.id != request.resource.data.referredByUserId) && !(resource.data.id == request.resource.data.referredByUserId);
// user can't refer someone who referred them quid pro quo // quid pro quos enabled (only once though so nbd) - bc I can't make this work:
&& get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id; // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
} }
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {

View File

@ -5,7 +5,7 @@
"firestore": "dev-mantic-markets.appspot.com" "firestore": "dev-mantic-markets.appspot.com"
}, },
"scripts": { "scripts": {
"build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist", "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env dist",
"compile": "tsc -b", "compile": "tsc -b",
"watch": "tsc -w", "watch": "tsc -w",
"shell": "yarn build && firebase functions:shell", "shell": "yarn build && firebase functions:shell",

View File

@ -1,4 +1,4 @@
import { UserIcon } from '@heroicons/react/outline' import { UserIcon, XIcon } from '@heroicons/react/outline'
import { useUsers } from 'web/hooks/use-users' import { useUsers } from 'web/hooks/use-users'
import { User } from 'common/user' import { User } from 'common/user'
import { Fragment, useMemo, useState } from 'react' import { Fragment, useMemo, useState } from 'react'
@ -6,13 +6,24 @@ import clsx from 'clsx'
import { Menu, Transition } from '@headlessui/react' import { Menu, Transition } from '@headlessui/react'
import { Avatar } from 'web/components/avatar' import { Avatar } from 'web/components/avatar'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page'
export function FilterSelectUsers(props: { export function FilterSelectUsers(props: {
setSelectedUsers: (users: User[]) => void setSelectedUsers: (users: User[]) => void
selectedUsers: User[] selectedUsers: User[]
ignoreUserIds: string[] ignoreUserIds: string[]
showSelectedUsersTitle?: boolean
selectedUsersClassName?: string
maxUsers?: number
}) { }) {
const { ignoreUserIds, selectedUsers, setSelectedUsers } = props const {
ignoreUserIds,
selectedUsers,
setSelectedUsers,
showSelectedUsersTitle,
selectedUsersClassName,
maxUsers,
} = props
const users = useUsers() const users = useUsers()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [filteredUsers, setFilteredUsers] = useState<User[]>([]) const [filteredUsers, setFilteredUsers] = useState<User[]>([])
@ -29,89 +40,118 @@ export function FilterSelectUsers(props: {
}) })
) )
}, [beginQuerying, users, selectedUsers, ignoreUserIds, query]) }, [beginQuerying, users, selectedUsers, ignoreUserIds, query])
const shouldShow = maxUsers ? selectedUsers.length < maxUsers : true
return ( return (
<div> <div>
<div className="relative mt-1 rounded-md"> {shouldShow && (
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <>
<UserIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> <div className="relative mt-1 rounded-md">
</div> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<input <UserIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
type="text" </div>
name="user name" <input
id="user name" type="text"
value={query} name="user name"
onChange={(e) => setQuery(e.target.value)} id="user name"
className="input input-bordered block w-full pl-10 focus:border-gray-300 " value={query}
placeholder="Austin Chen" onChange={(e) => setQuery(e.target.value)}
/> className="input input-bordered block w-full pl-10 focus:border-gray-300 "
</div> placeholder="Austin Chen"
<Menu />
as="div" </div>
className={clsx( <Menu
'relative inline-block w-full overflow-y-scroll text-right', as="div"
beginQuerying && 'h-36' className={clsx(
)} 'relative inline-block w-full overflow-y-scroll text-right',
> beginQuerying && 'h-36'
{({}) => ( )}
<Transition
show={beginQuerying}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items {({}) => (
static={true} <Transition
className="absolute right-0 mt-2 w-full origin-top-right cursor-pointer divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" show={beginQuerying}
> as={Fragment}
<div className="py-1"> enter="transition ease-out duration-100"
{filteredUsers.map((user: User) => ( enterFrom="transform opacity-0 scale-95"
<Menu.Item key={user.id}> enterTo="transform opacity-100 scale-100"
{({ active }) => ( leave="transition ease-in duration-75"
<span leaveFrom="transform opacity-100 scale-100"
className={clsx( leaveTo="transform opacity-0 scale-95"
active >
? 'bg-gray-100 text-gray-900' <Menu.Items
: 'text-gray-700', static={true}
'group flex items-center px-4 py-2 text-sm' className="absolute right-0 mt-2 w-full origin-top-right cursor-pointer divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="py-1">
{filteredUsers.map((user: User) => (
<Menu.Item key={user.id}>
{({ active }) => (
<span
className={clsx(
active
? 'bg-gray-100 text-gray-900'
: 'text-gray-700',
'group flex items-center px-4 py-2 text-sm'
)}
onClick={() => {
setQuery('')
setSelectedUsers([...selectedUsers, user])
}}
>
<Avatar
username={user.username}
avatarUrl={user.avatarUrl}
size={'xs'}
className={'mr-2'}
/>
{user.name}
</span>
)} )}
onClick={() => { </Menu.Item>
setQuery('') ))}
setSelectedUsers([...selectedUsers, user]) </div>
}} </Menu.Items>
> </Transition>
<Avatar )}
username={user.username} </Menu>
avatarUrl={user.avatarUrl} </>
size={'xs'} )}
className={'mr-2'}
/>
{user.name}
</span>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
)}
</Menu>
{selectedUsers.length > 0 && ( {selectedUsers.length > 0 && (
<> <>
<div className={'mb-2'}>Added members:</div> <div className={'mb-2'}>
<Row className="mt-0 grid grid-cols-6 gap-2"> {showSelectedUsersTitle && 'Added members:'}
</div>
<Row
className={clsx(
'mt-0 grid grid-cols-6 gap-2',
selectedUsersClassName
)}
>
{selectedUsers.map((user: User) => ( {selectedUsers.map((user: User) => (
<div key={user.id} className="col-span-2 flex items-center"> <div
<Avatar key={user.id}
username={user.username} className="col-span-2 flex flex-row items-center justify-between"
avatarUrl={user.avatarUrl} >
size={'sm'} <Row className={'items-center'}>
<Avatar
username={user.username}
avatarUrl={user.avatarUrl}
size={'sm'}
/>
<UserLink
username={user.username}
className="ml-2"
name={user.name}
/>
</Row>
<XIcon
onClick={() =>
setSelectedUsers([
...selectedUsers.filter((u) => u.id != user.id),
])
}
className=" h-5 w-5 cursor-pointer text-gray-400"
aria-hidden="true"
/> />
<span className="ml-2">{user.name}</span>
</div> </div>
))} ))}
</Row> </Row>

View File

@ -63,6 +63,7 @@ export function BottomNavBar() {
currentPage={currentPage} currentPage={currentPage}
item={{ item={{
name: formatMoney(user.balance), name: formatMoney(user.balance),
trackingEventName: 'profile',
href: `/${user.username}?tab=bets`, href: `/${user.username}?tab=bets`,
icon: () => ( icon: () => (
<Avatar <Avatar
@ -94,6 +95,7 @@ export function BottomNavBar() {
function NavBarItem(props: { item: Item; currentPage: string }) { function NavBarItem(props: { item: Item; currentPage: string }) {
const { item, currentPage } = props const { item, currentPage } = props
const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`)
return ( return (
<Link href={item.href}> <Link href={item.href}>
@ -102,7 +104,7 @@ function NavBarItem(props: { item: Item; currentPage: string }) {
'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700', 'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700',
currentPage === item.href && 'bg-gray-200 text-indigo-700' currentPage === item.href && 'bg-gray-200 text-indigo-700'
)} )}
onClick={trackCallback('navbar: ' + item.name)} onClick={track}
> >
{item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />} {item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />}
{item.name} {item.name}

View File

@ -120,6 +120,7 @@ function getMoreMobileNav() {
export type Item = { export type Item = {
name: string name: string
trackingEventName?: string
href: string href: string
icon?: React.ComponentType<{ className?: string }> icon?: React.ComponentType<{ className?: string }>
} }

View File

@ -10,9 +10,11 @@ import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar' import { Avatar } from 'web/components/avatar'
import { UserLink } from 'web/components/user-page' import { UserLink } from 'web/components/user-page'
import { useReferrals } from 'web/hooks/use-referrals' import { useReferrals } from 'web/hooks/use-referrals'
import { FilterSelectUsers } from 'web/components/filter-select-users'
import { getUser, updateUser } from 'web/lib/firebase/users'
export function ReferralsButton(props: { user: User }) { export function ReferralsButton(props: { user: User; currentUser?: User }) {
const { user } = props const { user, currentUser } = props
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const referralIds = useReferrals(user.id) const referralIds = useReferrals(user.id)
@ -28,6 +30,7 @@ export function ReferralsButton(props: { user: User }) {
referralIds={referralIds ?? []} referralIds={referralIds ?? []}
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
currentUser={currentUser}
/> />
</> </>
) )
@ -38,8 +41,21 @@ function ReferralsDialog(props: {
referralIds: string[] referralIds: string[]
isOpen: boolean isOpen: boolean
setIsOpen: (isOpen: boolean) => void setIsOpen: (isOpen: boolean) => void
currentUser?: User
}) { }) {
const { user, referralIds, isOpen, setIsOpen } = props const { user, referralIds, isOpen, setIsOpen, currentUser } = props
const [referredBy, setReferredBy] = useState<User[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const [errorText, setErrorText] = useState('')
const [referredByUser, setReferredByUser] = useState<User | null>()
useEffect(() => {
if (isOpen && !referredByUser && user?.referredByUserId) {
getUser(user.referredByUserId).then((user) => {
setReferredByUser(user)
})
}
}, [isOpen, referredByUser, user.referredByUserId])
useEffect(() => { useEffect(() => {
prefetchUsers(referralIds) prefetchUsers(referralIds)
@ -56,6 +72,75 @@ function ReferralsDialog(props: {
title: 'Referrals', title: 'Referrals',
content: <ReferralsList userIds={referralIds} />, content: <ReferralsList userIds={referralIds} />,
}, },
{
title: 'Referred by',
content: (
<>
{user.id === currentUser?.id && !referredByUser ? (
<>
<FilterSelectUsers
setSelectedUsers={setReferredBy}
selectedUsers={referredBy}
ignoreUserIds={[currentUser.id]}
showSelectedUsersTitle={false}
selectedUsersClassName={'grid-cols-2 '}
maxUsers={1}
/>
<Row className={'mt-0 justify-end'}>
<button
className={
referredBy.length === 0
? 'hidden'
: 'btn btn-primary btn-md my-2 w-24 normal-case'
}
disabled={referredBy.length === 0 || isSubmitting}
onClick={() => {
setIsSubmitting(true)
updateUser(currentUser.id, {
referredByUserId: referredBy[0].id,
})
.then(async () => {
setErrorText('')
setIsSubmitting(false)
setReferredBy([])
setIsOpen(false)
})
.catch((error) => {
setIsSubmitting(false)
setErrorText(error.message)
})
}}
>
Save
</button>
</Row>
<span className={'text-warning'}>
{referredBy.length > 0 &&
'Careful: you can only set who referred you once!'}
</span>
<span className={'text-error'}>{errorText}</span>
</>
) : (
<div className="justify-center text-gray-700">
{referredByUser ? (
<Row className={'items-center gap-2 p-2'}>
<Avatar
username={referredByUser.username}
avatarUrl={referredByUser.avatarUrl}
/>
<UserLink
username={referredByUser.username}
name={referredByUser.name}
/>
</Row>
) : (
<span className={'text-gray-500'}>No one...</span>
)}
</div>
)}
</>
),
},
]} ]}
/> />
</Col> </Col>

View File

@ -159,7 +159,7 @@ export function UserPage(props: {
<Avatar <Avatar
username={user.username} username={user.username}
avatarUrl={user.avatarUrl} avatarUrl={user.avatarUrl}
size={20} size={24}
className="bg-white ring-4 ring-white" className="bg-white ring-4 ring-white"
/> />
</div> </div>
@ -202,7 +202,7 @@ export function UserPage(props: {
<Row className="gap-4"> <Row className="gap-4">
<FollowingButton user={user} /> <FollowingButton user={user} />
<FollowersButton user={user} /> <FollowersButton user={user} />
<ReferralsButton user={user} /> <ReferralsButton user={user} currentUser={currentUser} />
<GroupsButton user={user} /> <GroupsButton user={user} />
</Row> </Row>

View File

@ -1,41 +0,0 @@
import React from 'react'
import { Page } from 'web/components/page'
import { UserPage } from 'web/components/user-page'
import { useUser } from 'web/hooks/use-user'
import { firebaseLogin } from 'web/lib/firebase/users'
function SignInCard() {
return (
<div className="card glass sm:card-side text-neutral-content mx-4 my-12 max-w-sm bg-green-600 shadow-xl transition-all hover:bg-green-600 hover:shadow-xl sm:mx-auto">
<div className="p-4">
<img
src="/logo-bg-white.png"
className="h-20 w-20 rounded-lg shadow-lg"
/>
</div>
<div className="card-body max-w-md">
<h2 className="card-title font-major-mono">Welcome!</h2>
<p>Sign in to get started</p>
<div className="card-actions">
<button
className="btn glass rounded-full hover:bg-green-500"
onClick={firebaseLogin}
>
Sign in with Google
</button>
</div>
</div>
</div>
)
}
export default function Account() {
const user = useUser()
return user ? (
<UserPage user={user} currentUser={user} />
) : (
<Page>
<SignInCard />
</Page>
)
}

View File

@ -62,13 +62,19 @@ function UsersTable() {
class="hover:underline hover:decoration-indigo-400 hover:decoration-2" class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
href="/${cell}">@${cell}</a>`), href="/${cell}">@${cell}</a>`),
}, },
{
id: 'name',
name: 'Name',
formatter: (cell) =>
html(`<span class="whitespace-nowrap">${cell}</span>`),
},
{ {
id: 'email', id: 'email',
name: 'Email', name: 'Email',
}, },
{ {
id: 'createdTime', id: 'createdTime',
name: 'Created Time', name: 'Created',
formatter: (cell) => formatter: (cell) =>
html( html(
`<span class="whitespace-nowrap">${dayjs(cell as number).format( `<span class="whitespace-nowrap">${dayjs(cell as number).format(

View File

@ -46,7 +46,7 @@ export default function ClaimPage() {
if (result.data.status == 'error') { if (result.data.status == 'error') {
throw new Error(result.data.message) throw new Error(result.data.message)
} }
router.push('/account?claimed-mana=yes') user && router.push(`/${user.username}?claimed-mana=yes`)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
const message = const message =