This commit is contained in:
		
						commit
						8a31670063
					
				
							
								
								
									
										3
									
								
								common/globalConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								common/globalConfig.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export type GlobalConfig = { | ||||||
|  |   pinnedItems: { itemId: string; type: 'post' | 'contract' }[] | ||||||
|  | } | ||||||
|  | @ -23,6 +23,12 @@ service cloud.firestore { | ||||||
|       allow read; |       allow read; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     match /globalConfig/globalConfig { | ||||||
|  |       allow read; | ||||||
|  |       allow update: if isAdmin() | ||||||
|  |       allow create: if isAdmin() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     match /users/{userId} { |     match /users/{userId} { | ||||||
|       allow read; |       allow read; | ||||||
|       allow update: if userId == request.auth.uid |       allow update: if userId == request.auth.uid | ||||||
|  |  | ||||||
|  | @ -231,7 +231,6 @@ export function PinnedItems(props: { | ||||||
|   return pinned.length > 0 || isEditable ? ( |   return pinned.length > 0 || isEditable ? ( | ||||||
|     <div> |     <div> | ||||||
|       <Row className="mb-3 items-center justify-between"> |       <Row className="mb-3 items-center justify-between"> | ||||||
|         <SectionHeader label={'Pinned'} /> |  | ||||||
|         {isEditable && ( |         {isEditable && ( | ||||||
|           <Button |           <Button | ||||||
|             color="gray" |             color="gray" | ||||||
|  |  | ||||||
|  | @ -69,9 +69,9 @@ export function PinnedSelectModal(props: { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> |     <Modal open={open} setOpen={setOpen} className={' sm:p-0'} size={'lg'}> | ||||||
|       <Col className="h-[85vh] w-full gap-4 rounded-md bg-white"> |       <Col className=" h-[85vh] w-full gap-4 overflow-scroll rounded-md bg-white"> | ||||||
|         <div className="p-8 pb-0"> |         <div className=" p-8 pb-0"> | ||||||
|           <Row> |           <Row> | ||||||
|             <div className={'text-xl text-indigo-700'}>{title}</div> |             <div className={'text-xl text-indigo-700'}>{title}</div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								web/hooks/use-global-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/hooks/use-global-config.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { GlobalConfig } from 'common/globalConfig' | ||||||
|  | import { useEffect, useState } from 'react' | ||||||
|  | import { listenForGlobalConfig } from 'web/lib/firebase/globalConfig' | ||||||
|  | 
 | ||||||
|  | export const useGlobalConfig = () => { | ||||||
|  |   const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>(null) | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     listenForGlobalConfig(setGlobalConfig) | ||||||
|  |   }, [globalConfig]) | ||||||
|  |   return globalConfig | ||||||
|  | } | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import { DateDoc, Post } from 'common/post' | import { DateDoc, Post } from 'common/post' | ||||||
| import { listenForDateDocs, listenForPost } from 'web/lib/firebase/posts' | import { | ||||||
|  |   getAllPosts, | ||||||
|  |   listenForDateDocs, | ||||||
|  |   listenForPost, | ||||||
|  | } from 'web/lib/firebase/posts' | ||||||
| 
 | 
 | ||||||
| export const usePost = (postId: string | undefined) => { | export const usePost = (postId: string | undefined) => { | ||||||
|   const [post, setPost] = useState<Post | null | undefined>() |   const [post, setPost] = useState<Post | null | undefined>() | ||||||
|  | @ -38,6 +42,14 @@ export const usePosts = (postIds: string[]) => { | ||||||
|     .sort((a, b) => b.createdTime - a.createdTime) |     .sort((a, b) => b.createdTime - a.createdTime) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export const useAllPosts = () => { | ||||||
|  |   const [posts, setPosts] = useState<Post[]>([]) | ||||||
|  |   useEffect(() => { | ||||||
|  |     getAllPosts().then(setPosts) | ||||||
|  |   }, []) | ||||||
|  |   return posts | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const useDateDocs = () => { | export const useDateDocs = () => { | ||||||
|   const [dateDocs, setDateDocs] = useState<DateDoc[]>() |   const [dateDocs, setDateDocs] = useState<DateDoc[]>() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								web/lib/firebase/globalConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/lib/firebase/globalConfig.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import { | ||||||
|  |   CollectionReference, | ||||||
|  |   doc, | ||||||
|  |   collection, | ||||||
|  |   getDoc, | ||||||
|  |   updateDoc, | ||||||
|  | } from 'firebase/firestore' | ||||||
|  | import { db } from 'web/lib/firebase/init' | ||||||
|  | import { GlobalConfig } from 'common/globalConfig' | ||||||
|  | import { listenForValue } from './utils' | ||||||
|  | 
 | ||||||
|  | const globalConfigCollection = collection( | ||||||
|  |   db, | ||||||
|  |   'globalConfig' | ||||||
|  | ) as CollectionReference<GlobalConfig> | ||||||
|  | const globalConfigDoc = doc(globalConfigCollection, 'globalConfig') | ||||||
|  | 
 | ||||||
|  | export const getGlobalConfig = async () => { | ||||||
|  |   return (await getDoc(globalConfigDoc)).data() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function updateGlobalConfig( | ||||||
|  |   globalConfig: GlobalConfig, | ||||||
|  |   updates: Partial<GlobalConfig> | ||||||
|  | ) { | ||||||
|  |   return updateDoc(globalConfigDoc, updates) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function listenForGlobalConfig( | ||||||
|  |   setGlobalConfig: (globalConfig: GlobalConfig | null) => void | ||||||
|  | ) { | ||||||
|  |   return listenForValue(globalConfigDoc, setGlobalConfig) | ||||||
|  | } | ||||||
|  | @ -52,6 +52,10 @@ export async function listPosts(postIds?: string[]) { | ||||||
|   return Promise.all(postIds.map(getPost)) |   return Promise.all(postIds.map(getPost)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function getAllPosts() { | ||||||
|  |   return getValues<Post>(posts) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getDateDocs() { | export async function getDateDocs() { | ||||||
|   const q = query(posts, where('type', '==', 'date-doc')) |   const q = query(posts, where('type', '==', 'date-doc')) | ||||||
|   return getValues<DateDoc>(q) |   return getValues<DateDoc>(q) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import React, { ReactNode, useEffect } from 'react' | import React, { ReactNode, useEffect, useState } from 'react' | ||||||
| import Router from 'next/router' | import Router from 'next/router' | ||||||
| import { | import { | ||||||
|   AdjustmentsIcon, |   AdjustmentsIcon, | ||||||
|  | @ -42,7 +42,7 @@ import { filterDefined } from 'common/util/array' | ||||||
| import { updateUser } from 'web/lib/firebase/users' | import { updateUser } from 'web/lib/firebase/users' | ||||||
| import { isArray, keyBy } from 'lodash' | import { isArray, keyBy } from 'lodash' | ||||||
| import { usePrefetch } from 'web/hooks/use-prefetch' | import { usePrefetch } from 'web/hooks/use-prefetch' | ||||||
| import { CPMMBinaryContract } from 'common/contract' | import { Contract, CPMMBinaryContract } from 'common/contract' | ||||||
| import { | import { | ||||||
|   useContractsByDailyScoreNotBetOn, |   useContractsByDailyScoreNotBetOn, | ||||||
|   useContractsByDailyScoreGroups, |   useContractsByDailyScoreGroups, | ||||||
|  | @ -52,6 +52,16 @@ import { | ||||||
| import { ProfitBadge } from 'web/components/profit-badge' | import { ProfitBadge } from 'web/components/profit-badge' | ||||||
| import { LoadingIndicator } from 'web/components/loading-indicator' | import { LoadingIndicator } from 'web/components/loading-indicator' | ||||||
| import { Input } from 'web/components/input' | import { Input } from 'web/components/input' | ||||||
|  | import { PinnedItems } from 'web/components/groups/group-overview' | ||||||
|  | import { updateGlobalConfig } from 'web/lib/firebase/globalConfig' | ||||||
|  | import { getPost } from 'web/lib/firebase/posts' | ||||||
|  | import { PostCard } from 'web/components/post-card' | ||||||
|  | import { getContractFromId } from 'web/lib/firebase/contracts' | ||||||
|  | import { ContractCard } from 'web/components/contract/contract-card' | ||||||
|  | import { Post } from 'common/post' | ||||||
|  | import { isAdmin } from 'common/envs/constants' | ||||||
|  | import { useAllPosts } from 'web/hooks/use-post' | ||||||
|  | import { useGlobalConfig } from 'web/hooks/use-global-config' | ||||||
| 
 | 
 | ||||||
| export default function Home() { | export default function Home() { | ||||||
|   const user = useUser() |   const user = useUser() | ||||||
|  | @ -160,11 +170,12 @@ export default function Home() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const HOME_SECTIONS = [ | const HOME_SECTIONS = [ | ||||||
|  |   { label: 'Featured', id: 'featured' }, | ||||||
|   { label: 'Daily trending', id: 'daily-trending' }, |   { label: 'Daily trending', id: 'daily-trending' }, | ||||||
|   { label: 'Daily movers', id: 'daily-movers' }, |   { label: 'Daily movers', id: 'daily-movers' }, | ||||||
|   { label: 'Trending', id: 'score' }, |   { label: 'Trending', id: 'score' }, | ||||||
|   { label: 'New', id: 'newest' }, |   { label: 'New', id: 'newest' }, | ||||||
| ] | ] as const | ||||||
| 
 | 
 | ||||||
| export const getHomeItems = (sections: string[]) => { | export const getHomeItems = (sections: string[]) => { | ||||||
|   // Accommodate old home sections.
 |   // Accommodate old home sections.
 | ||||||
|  | @ -200,17 +211,25 @@ function renderSections( | ||||||
|     score: CPMMBinaryContract[] |     score: CPMMBinaryContract[] | ||||||
|   } |   } | ||||||
| ) { | ) { | ||||||
|  |   type sectionTypes = typeof HOME_SECTIONS[number]['id'] | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       {sections.map((s) => { |       {sections.map((s) => { | ||||||
|         const { id, label } = s as { |         const { id, label } = s as { | ||||||
|           id: keyof typeof sectionContracts |           id: sectionTypes | ||||||
|           label: string |           label: string | ||||||
|         } |         } | ||||||
|         if (id === 'daily-movers') { |         if (id === 'daily-movers') { | ||||||
|           return <DailyMoversSection key={id} {...sectionContracts[id]} /> |           return <DailyMoversSection key={id} {...sectionContracts[id]} /> | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (id === 'featured') { | ||||||
|  |           // For now, only admins can see the featured section, until we all agree its ship-ready
 | ||||||
|  |           if (!isAdmin) return <></> | ||||||
|  |           return <FeaturedSection /> | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const contracts = sectionContracts[id] |         const contracts = sectionContracts[id] | ||||||
| 
 | 
 | ||||||
|         if (id === 'daily-trending') { |         if (id === 'daily-trending') { | ||||||
|  | @ -324,6 +343,78 @@ function SearchSection(props: { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function FeaturedSection() { | ||||||
|  |   const [pinned, setPinned] = useState<JSX.Element[]>([]) | ||||||
|  |   const posts = useAllPosts() | ||||||
|  |   const globalConfig = useGlobalConfig() | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const pinnedItems = globalConfig?.pinnedItems | ||||||
|  | 
 | ||||||
|  |     async function getPinned() { | ||||||
|  |       if (pinnedItems == null) { | ||||||
|  |         if (globalConfig != null) { | ||||||
|  |           updateGlobalConfig(globalConfig, { pinnedItems: [] }) | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         const itemComponents = await Promise.all( | ||||||
|  |           pinnedItems.map(async (element) => { | ||||||
|  |             if (element.type === 'post') { | ||||||
|  |               const post = await getPost(element.itemId) | ||||||
|  |               if (post) { | ||||||
|  |                 return <PostCard post={post as Post} /> | ||||||
|  |               } | ||||||
|  |             } else if (element.type === 'contract') { | ||||||
|  |               const contract = await getContractFromId(element.itemId) | ||||||
|  |               if (contract) { | ||||||
|  |                 return <ContractCard contract={contract as Contract} /> | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |         setPinned( | ||||||
|  |           itemComponents.filter( | ||||||
|  |             (element) => element != undefined | ||||||
|  |           ) as JSX.Element[] | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     getPinned() | ||||||
|  |   }, [globalConfig]) | ||||||
|  | 
 | ||||||
|  |   async function onSubmit(selectedItems: { itemId: string; type: string }[]) { | ||||||
|  |     if (globalConfig == null) return | ||||||
|  |     await updateGlobalConfig(globalConfig, { | ||||||
|  |       pinnedItems: [ | ||||||
|  |         ...(globalConfig?.pinnedItems ?? []), | ||||||
|  |         ...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]), | ||||||
|  |       ], | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function onDeleteClicked(index: number) { | ||||||
|  |     if (globalConfig == null) return | ||||||
|  |     const newPinned = globalConfig.pinnedItems.filter((item) => { | ||||||
|  |       return item.itemId !== globalConfig.pinnedItems[index].itemId | ||||||
|  |     }) | ||||||
|  |     updateGlobalConfig(globalConfig, { pinnedItems: newPinned }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Col> | ||||||
|  |       <SectionHeader label={'Featured'} href={`#`} /> | ||||||
|  |       <PinnedItems | ||||||
|  |         posts={posts} | ||||||
|  |         isEditable={true} | ||||||
|  |         pinned={pinned} | ||||||
|  |         onDeleteClicked={onDeleteClicked} | ||||||
|  |         onSubmit={onSubmit} | ||||||
|  |         modalMessage={'Pin posts or markets to the overview of this group.'} | ||||||
|  |       /> | ||||||
|  |     </Col> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function GroupSection(props: { | function GroupSection(props: { | ||||||
|   group: Group |   group: Group | ||||||
|   user: User |   user: User | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user