Featured items to homepage (#1024)
* Featured items to homepage * Fix nits
This commit is contained in:
parent
3fc53112b9
commit
ff6278b147
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;
|
||||
}
|
||||
|
||||
match /globalConfig/globalConfig {
|
||||
allow read;
|
||||
allow update: if isAdmin()
|
||||
allow create: if isAdmin()
|
||||
}
|
||||
|
||||
match /users/{userId} {
|
||||
allow read;
|
||||
allow update: if userId == request.auth.uid
|
||||
|
|
|
@ -231,7 +231,6 @@ export function PinnedItems(props: {
|
|||
return pinned.length > 0 || isEditable ? (
|
||||
<div>
|
||||
<Row className="mb-3 items-center justify-between">
|
||||
<SectionHeader label={'Pinned'} />
|
||||
{isEditable && (
|
||||
<Button
|
||||
color="gray"
|
||||
|
|
|
@ -69,9 +69,9 @@ export function PinnedSelectModal(props: {
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
|
||||
<Col className="h-[85vh] w-full gap-4 rounded-md bg-white">
|
||||
<div className="p-8 pb-0">
|
||||
<Modal open={open} setOpen={setOpen} className={' sm:p-0'} size={'lg'}>
|
||||
<Col className=" h-[85vh] w-full gap-4 overflow-scroll rounded-md bg-white">
|
||||
<div className=" p-8 pb-0">
|
||||
<Row>
|
||||
<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 { 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) => {
|
||||
const [post, setPost] = useState<Post | null | undefined>()
|
||||
|
@ -38,6 +42,14 @@ export const usePosts = (postIds: string[]) => {
|
|||
.sort((a, b) => b.createdTime - a.createdTime)
|
||||
}
|
||||
|
||||
export const useAllPosts = () => {
|
||||
const [posts, setPosts] = useState<Post[]>([])
|
||||
useEffect(() => {
|
||||
getAllPosts().then(setPosts)
|
||||
}, [])
|
||||
return posts
|
||||
}
|
||||
|
||||
export const useDateDocs = () => {
|
||||
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))
|
||||
}
|
||||
|
||||
export function getAllPosts() {
|
||||
return getValues<Post>(posts)
|
||||
}
|
||||
|
||||
export async function getDateDocs() {
|
||||
const q = query(posts, where('type', '==', 'date-doc'))
|
||||
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 {
|
||||
AdjustmentsIcon,
|
||||
|
@ -42,7 +42,7 @@ import { filterDefined } from 'common/util/array'
|
|||
import { updateUser } from 'web/lib/firebase/users'
|
||||
import { isArray, keyBy } from 'lodash'
|
||||
import { usePrefetch } from 'web/hooks/use-prefetch'
|
||||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||
import {
|
||||
useContractsByDailyScoreNotBetOn,
|
||||
useContractsByDailyScoreGroups,
|
||||
|
@ -52,6 +52,16 @@ import {
|
|||
import { ProfitBadge } from 'web/components/profit-badge'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
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() {
|
||||
const user = useUser()
|
||||
|
@ -160,11 +170,12 @@ export default function Home() {
|
|||
}
|
||||
|
||||
const HOME_SECTIONS = [
|
||||
{ label: 'Featured', id: 'featured' },
|
||||
{ label: 'Daily trending', id: 'daily-trending' },
|
||||
{ label: 'Daily movers', id: 'daily-movers' },
|
||||
{ label: 'Trending', id: 'score' },
|
||||
{ label: 'New', id: 'newest' },
|
||||
]
|
||||
] as const
|
||||
|
||||
export const getHomeItems = (sections: string[]) => {
|
||||
// Accommodate old home sections.
|
||||
|
@ -200,17 +211,25 @@ function renderSections(
|
|||
score: CPMMBinaryContract[]
|
||||
}
|
||||
) {
|
||||
type sectionTypes = typeof HOME_SECTIONS[number]['id']
|
||||
|
||||
return (
|
||||
<>
|
||||
{sections.map((s) => {
|
||||
const { id, label } = s as {
|
||||
id: keyof typeof sectionContracts
|
||||
id: sectionTypes
|
||||
label: string
|
||||
}
|
||||
if (id === 'daily-movers') {
|
||||
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]
|
||||
|
||||
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: {
|
||||
group: Group
|
||||
user: User
|
||||
|
|
Loading…
Reference in New Issue
Block a user