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;
|
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"
|
||||||
|
|
|
@ -70,7 +70,7 @@ 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