Featured items to homepage (#1024)

* Featured items to homepage

* Fix nits
This commit is contained in:
FRC 2022-10-12 15:04:39 +01:00 committed by GitHub
parent 3fc53112b9
commit ff6278b147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 169 additions and 9 deletions

3
common/globalConfig.ts Normal file
View File

@ -0,0 +1,3 @@
export type GlobalConfig = {
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}

View File

@ -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

View File

@ -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"

View File

@ -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>

View 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
}

View File

@ -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[]>()

View 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)
}

View File

@ -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)

View File

@ -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