Merge branch 'main' into atlas2
This commit is contained in:
		
						commit
						1890ea0287
					
				|  | @ -21,16 +21,15 @@ service cloud.firestore { | |||
|       allow update: if resource.data.id == request.auth.uid | ||||
|                        && request.resource.data.diff(resource.data).affectedKeys() | ||||
|                                                                     .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); | ||||
|       allow update: if resource.data.id == request.auth.uid | ||||
|                        && request.resource.data.diff(resource.data).affectedKeys() | ||||
|                         .hasOnly(['referredByUserId']) | ||||
|                         // only one referral allowed per user | ||||
|                         && !("referredByUserId" in resource.data) | ||||
|                         // user can't refer themselves | ||||
|                         && (resource.data.id != request.resource.data.referredByUserId) | ||||
|                         // user can't refer someone who referred them quid pro quo | ||||
|                         && get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id; | ||||
| 
 | ||||
|           allow update: if resource.data.id == request.auth.uid | ||||
|                              && request.resource.data.diff(resource.data).affectedKeys() | ||||
|                               .hasOnly(['referredByUserId']) | ||||
|                               // only one referral allowed per user | ||||
|                               && !("referredByUserId" in resource.data) | ||||
|                               // user can't refer themselves | ||||
|                               && !(resource.data.id == request.resource.data.referredByUserId); | ||||
|                               // 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); | ||||
|     } | ||||
| 
 | ||||
|   	match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
|     "firestore": "dev-mantic-markets.appspot.com" | ||||
|   }, | ||||
|   "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", | ||||
|     "watch": "tsc -w", | ||||
|     "shell": "yarn build && firebase functions:shell", | ||||
|  |  | |||
|  | @ -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 { User } from 'common/user' | ||||
| import { Fragment, useMemo, useState } from 'react' | ||||
|  | @ -6,13 +6,24 @@ import clsx from 'clsx' | |||
| import { Menu, Transition } from '@headlessui/react' | ||||
| import { Avatar } from 'web/components/avatar' | ||||
| import { Row } from 'web/components/layout/row' | ||||
| import { UserLink } from 'web/components/user-page' | ||||
| 
 | ||||
| export function FilterSelectUsers(props: { | ||||
|   setSelectedUsers: (users: User[]) => void | ||||
|   selectedUsers: User[] | ||||
|   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 [query, setQuery] = useState('') | ||||
|   const [filteredUsers, setFilteredUsers] = useState<User[]>([]) | ||||
|  | @ -29,89 +40,118 @@ export function FilterSelectUsers(props: { | |||
|         }) | ||||
|       ) | ||||
|   }, [beginQuerying, users, selectedUsers, ignoreUserIds, query]) | ||||
| 
 | ||||
|   const shouldShow = maxUsers ? selectedUsers.length < maxUsers : true | ||||
|   return ( | ||||
|     <div> | ||||
|       <div className="relative mt-1 rounded-md"> | ||||
|         <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> | ||||
|         <input | ||||
|           type="text" | ||||
|           name="user name" | ||||
|           id="user name" | ||||
|           value={query} | ||||
|           onChange={(e) => setQuery(e.target.value)} | ||||
|           className="input input-bordered block w-full pl-10 focus:border-gray-300 " | ||||
|           placeholder="Austin Chen" | ||||
|         /> | ||||
|       </div> | ||||
|       <Menu | ||||
|         as="div" | ||||
|         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" | ||||
|       {shouldShow && ( | ||||
|         <> | ||||
|           <div className="relative mt-1 rounded-md"> | ||||
|             <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> | ||||
|             <input | ||||
|               type="text" | ||||
|               name="user name" | ||||
|               id="user name" | ||||
|               value={query} | ||||
|               onChange={(e) => setQuery(e.target.value)} | ||||
|               className="input input-bordered block w-full pl-10 focus:border-gray-300 " | ||||
|               placeholder="Austin Chen" | ||||
|             /> | ||||
|           </div> | ||||
|           <Menu | ||||
|             as="div" | ||||
|             className={clsx( | ||||
|               'relative inline-block w-full overflow-y-scroll text-right', | ||||
|               beginQuerying && 'h-36' | ||||
|             )} | ||||
|           > | ||||
|             <Menu.Items | ||||
|               static={true} | ||||
|               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' | ||||
|             {({}) => ( | ||||
|               <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} | ||||
|                   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={() => { | ||||
|                           setQuery('') | ||||
|                           setSelectedUsers([...selectedUsers, user]) | ||||
|                         }} | ||||
|                       > | ||||
|                         <Avatar | ||||
|                           username={user.username} | ||||
|                           avatarUrl={user.avatarUrl} | ||||
|                           size={'xs'} | ||||
|                           className={'mr-2'} | ||||
|                         /> | ||||
|                         {user.name} | ||||
|                       </span> | ||||
|                     )} | ||||
|                   </Menu.Item> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </Menu.Items> | ||||
|           </Transition> | ||||
|         )} | ||||
|       </Menu> | ||||
|                       </Menu.Item> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </Menu.Items> | ||||
|               </Transition> | ||||
|             )} | ||||
|           </Menu> | ||||
|         </> | ||||
|       )} | ||||
|       {selectedUsers.length > 0 && ( | ||||
|         <> | ||||
|           <div className={'mb-2'}>Added members:</div> | ||||
|           <Row className="mt-0 grid grid-cols-6 gap-2"> | ||||
|           <div className={'mb-2'}> | ||||
|             {showSelectedUsersTitle && 'Added members:'} | ||||
|           </div> | ||||
|           <Row | ||||
|             className={clsx( | ||||
|               'mt-0 grid grid-cols-6 gap-2', | ||||
|               selectedUsersClassName | ||||
|             )} | ||||
|           > | ||||
|             {selectedUsers.map((user: User) => ( | ||||
|               <div key={user.id} className="col-span-2 flex items-center"> | ||||
|                 <Avatar | ||||
|                   username={user.username} | ||||
|                   avatarUrl={user.avatarUrl} | ||||
|                   size={'sm'} | ||||
|               <div | ||||
|                 key={user.id} | ||||
|                 className="col-span-2 flex flex-row items-center justify-between" | ||||
|               > | ||||
|                 <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> | ||||
|             ))} | ||||
|           </Row> | ||||
|  |  | |||
|  | @ -63,6 +63,7 @@ export function BottomNavBar() { | |||
|           currentPage={currentPage} | ||||
|           item={{ | ||||
|             name: formatMoney(user.balance), | ||||
|             trackingEventName: 'profile', | ||||
|             href: `/${user.username}?tab=bets`, | ||||
|             icon: () => ( | ||||
|               <Avatar | ||||
|  | @ -94,6 +95,7 @@ export function BottomNavBar() { | |||
| 
 | ||||
| function NavBarItem(props: { item: Item; currentPage: string }) { | ||||
|   const { item, currentPage } = props | ||||
|   const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`) | ||||
| 
 | ||||
|   return ( | ||||
|     <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', | ||||
|           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.name} | ||||
|  |  | |||
|  | @ -120,6 +120,7 @@ function getMoreMobileNav() { | |||
| 
 | ||||
| export type Item = { | ||||
|   name: string | ||||
|   trackingEventName?: string | ||||
|   href: string | ||||
|   icon?: React.ComponentType<{ className?: string }> | ||||
| } | ||||
|  |  | |||
|  | @ -10,9 +10,11 @@ import { Row } from 'web/components/layout/row' | |||
| import { Avatar } from 'web/components/avatar' | ||||
| import { UserLink } from 'web/components/user-page' | ||||
| 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 }) { | ||||
|   const { user } = props | ||||
| export function ReferralsButton(props: { user: User; currentUser?: User }) { | ||||
|   const { user, currentUser } = props | ||||
|   const [isOpen, setIsOpen] = useState(false) | ||||
|   const referralIds = useReferrals(user.id) | ||||
| 
 | ||||
|  | @ -28,6 +30,7 @@ export function ReferralsButton(props: { user: User }) { | |||
|         referralIds={referralIds ?? []} | ||||
|         isOpen={isOpen} | ||||
|         setIsOpen={setIsOpen} | ||||
|         currentUser={currentUser} | ||||
|       /> | ||||
|     </> | ||||
|   ) | ||||
|  | @ -38,8 +41,21 @@ function ReferralsDialog(props: { | |||
|   referralIds: string[] | ||||
|   isOpen: boolean | ||||
|   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(() => { | ||||
|     prefetchUsers(referralIds) | ||||
|  | @ -56,6 +72,75 @@ function ReferralsDialog(props: { | |||
|               title: 'Referrals', | ||||
|               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> | ||||
|  |  | |||
|  | @ -159,7 +159,7 @@ export function UserPage(props: { | |||
|           <Avatar | ||||
|             username={user.username} | ||||
|             avatarUrl={user.avatarUrl} | ||||
|             size={20} | ||||
|             size={24} | ||||
|             className="bg-white ring-4 ring-white" | ||||
|           /> | ||||
|         </div> | ||||
|  | @ -202,7 +202,7 @@ export function UserPage(props: { | |||
|           <Row className="gap-4"> | ||||
|             <FollowingButton user={user} /> | ||||
|             <FollowersButton user={user} /> | ||||
|             <ReferralsButton user={user} /> | ||||
|             <ReferralsButton user={user} currentUser={currentUser} /> | ||||
|             <GroupsButton user={user} /> | ||||
|           </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> | ||||
|   ) | ||||
| } | ||||
|  | @ -62,13 +62,19 @@ function UsersTable() { | |||
|               class="hover:underline hover:decoration-indigo-400 hover:decoration-2" | ||||
|               href="/${cell}">@${cell}</a>`),
 | ||||
|           }, | ||||
|           { | ||||
|             id: 'name', | ||||
|             name: 'Name', | ||||
|             formatter: (cell) => | ||||
|               html(`<span class="whitespace-nowrap">${cell}</span>`), | ||||
|           }, | ||||
|           { | ||||
|             id: 'email', | ||||
|             name: 'Email', | ||||
|           }, | ||||
|           { | ||||
|             id: 'createdTime', | ||||
|             name: 'Created Time', | ||||
|             name: 'Created', | ||||
|             formatter: (cell) => | ||||
|               html( | ||||
|                 `<span class="whitespace-nowrap">${dayjs(cell as number).format( | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ export default function ClaimPage() { | |||
|               if (result.data.status == 'error') { | ||||
|                 throw new Error(result.data.message) | ||||
|               } | ||||
|               router.push('/account?claimed-mana=yes') | ||||
|               user && router.push(`/${user.username}?claimed-mana=yes`) | ||||
|             } catch (e) { | ||||
|               console.log(e) | ||||
|               const message = | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user