Allow users to choose who referred them (#611)
* Allow users to choose who referred them * Refactor * Rewording * Match list styles * Match empty text styles
This commit is contained in:
parent
d78bbcb3df
commit
e712ad8289
|
@ -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} {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,26 @@ 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 &&
|
||||||
|
currentUser?.referredByUserId &&
|
||||||
|
currentUser.id === user.id
|
||||||
|
) {
|
||||||
|
getUser(currentUser.referredByUserId).then((user) => {
|
||||||
|
setReferredByUser(user)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [currentUser, isOpen, referredByUser, user.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
prefetchUsers(referralIds)
|
prefetchUsers(referralIds)
|
||||||
|
@ -56,6 +77,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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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 =
|
||||||
|
|
Loading…
Reference in New Issue
Block a user