From 1f8c72b4c9270203477f507a8e7049e1292e6bc8 Mon Sep 17 00:00:00 2001 From: FRC Date: Mon, 3 Oct 2022 00:02:31 +0100 Subject: [PATCH] Overview page on groups (#961) * Frontpage on groups wip * Fix James's nits --- common/group.ts | 1 + firestore.rules | 2 +- functions/src/create-group.ts | 1 + functions/src/scripts/convert-tag-to-group.ts | 1 + web/components/contract-search.tsx | 7 +- web/components/contract-select-modal.tsx | 2 +- web/components/contract/contracts-grid.tsx | 8 +- ...about-post.tsx => group-overview-post.tsx} | 2 +- web/components/groups/group-overview.tsx | 378 ++++++++++++++++++ web/components/pinned-select-modal.tsx | 164 ++++++++ web/components/post-card.tsx | 82 ++++ web/pages/group/[...slugs]/index.tsx | 198 ++------- 12 files changed, 668 insertions(+), 178 deletions(-) rename web/components/groups/{group-about-post.tsx => group-overview-post.tsx} (98%) create mode 100644 web/components/groups/group-overview.tsx create mode 100644 web/components/pinned-select-modal.tsx create mode 100644 web/components/post-card.tsx diff --git a/common/group.ts b/common/group.ts index 5220a1e8..8f5728d3 100644 --- a/common/group.ts +++ b/common/group.ts @@ -23,6 +23,7 @@ export type Group = { score: number }[] } + pinnedItems: { itemId: string; type: 'post' | 'contract' }[] } export const MAX_GROUP_NAME_LENGTH = 75 diff --git a/firestore.rules b/firestore.rules index 26649fa6..bf0375e6 100644 --- a/firestore.rules +++ b/firestore.rules @@ -176,7 +176,7 @@ service cloud.firestore { allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) && request.resource.data.diff(resource.data) .affectedKeys() - .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId' ]); + .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId', 'pinnedItems' ]); allow delete: if request.auth.uid == resource.data.creatorId; match /groupContracts/{contractId} { diff --git a/functions/src/create-group.ts b/functions/src/create-group.ts index 76dc1298..4b3f7446 100644 --- a/functions/src/create-group.ts +++ b/functions/src/create-group.ts @@ -62,6 +62,7 @@ export const creategroup = newEndpoint({}, async (req, auth) => { totalContracts: 0, totalMembers: memberIds.length, postIds: [], + pinnedItems: [], } await groupRef.create(group) diff --git a/functions/src/scripts/convert-tag-to-group.ts b/functions/src/scripts/convert-tag-to-group.ts index b2e4c4d8..e1330fe1 100644 --- a/functions/src/scripts/convert-tag-to-group.ts +++ b/functions/src/scripts/convert-tag-to-group.ts @@ -42,6 +42,7 @@ const createGroup = async ( totalContracts: contracts.length, totalMembers: 1, postIds: [], + pinnedItems: [], } await groupRef.create(group) // create a GroupMemberDoc for the creator diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index ba589d0e..43f17599 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -3,10 +3,7 @@ import { SearchOptions } from '@algolia/client-search' import { useRouter } from 'next/router' import { Contract } from 'common/contract' import { PAST_BETS, User } from 'common/user' -import { - ContractHighlightOptions, - ContractsGrid, -} from './contract/contracts-grid' +import { CardHighlightOptions, ContractsGrid } from './contract/contracts-grid' import { ShowTime } from './contract/contract-details' import { Row } from './layout/row' import { @@ -82,7 +79,7 @@ export function ContractSearch(props: { defaultFilter?: filter defaultPill?: string additionalFilter?: AdditionalFilter - highlightOptions?: ContractHighlightOptions + highlightOptions?: CardHighlightOptions onContractClick?: (contract: Contract) => void hideOrderSelector?: boolean cardUIOptions?: { diff --git a/web/components/contract-select-modal.tsx b/web/components/contract-select-modal.tsx index ea08de01..ea0505a8 100644 --- a/web/components/contract-select-modal.tsx +++ b/web/components/contract-select-modal.tsx @@ -91,7 +91,7 @@ export function SelectMarketsModal(props: { noLinkAvatar: true, }} highlightOptions={{ - contractIds: contracts.map((c) => c.id), + itemIds: contracts.map((c) => c.id), highlightClassName: '!bg-indigo-100 outline outline-2 outline-indigo-300', }} diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index d6c9c5fa..d6206766 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -12,8 +12,8 @@ import { VisibilityObserver } from '../visibility-observer' import Masonry from 'react-masonry-css' import { CPMMBinaryContract } from 'common/contract' -export type ContractHighlightOptions = { - contractIds?: string[] +export type CardHighlightOptions = { + itemIds?: string[] highlightClassName?: string } @@ -28,7 +28,7 @@ export function ContractsGrid(props: { noLinkAvatar?: boolean showProbChange?: boolean } - highlightOptions?: ContractHighlightOptions + highlightOptions?: CardHighlightOptions trackingPostfix?: string breakpointColumns?: { [key: string]: number } }) { @@ -43,7 +43,7 @@ export function ContractsGrid(props: { } = props const { hideQuickBet, hideGroupLink, noLinkAvatar, showProbChange } = cardUIOptions || {} - const { contractIds, highlightClassName } = highlightOptions || {} + const { itemIds: contractIds, highlightClassName } = highlightOptions || {} const onVisibilityUpdated = useCallback( (visible) => { if (visible && loadMore) { diff --git a/web/components/groups/group-about-post.tsx b/web/components/groups/group-overview-post.tsx similarity index 98% rename from web/components/groups/group-about-post.tsx rename to web/components/groups/group-overview-post.tsx index 4d3046e9..55f0efca 100644 --- a/web/components/groups/group-about-post.tsx +++ b/web/components/groups/group-overview-post.tsx @@ -13,7 +13,7 @@ import { deletePost, updatePost } from 'web/lib/firebase/posts' import { useState } from 'react' import { usePost } from 'web/hooks/use-post' -export function GroupAboutPost(props: { +export function GroupOverviewPost(props: { group: Group isEditable: boolean post: Post | null diff --git a/web/components/groups/group-overview.tsx b/web/components/groups/group-overview.tsx new file mode 100644 index 00000000..9b0f7240 --- /dev/null +++ b/web/components/groups/group-overview.tsx @@ -0,0 +1,378 @@ +import { track } from '@amplitude/analytics-browser' +import { + ArrowSmRightIcon, + PlusCircleIcon, + XCircleIcon, +} from '@heroicons/react/outline' + +import PencilIcon from '@heroicons/react/solid/PencilIcon' + +import { Contract } from 'common/contract' +import { Group } from 'common/group' +import { Post } from 'common/post' +import { useEffect, useState } from 'react' +import { ReactNode } from 'react' +import { getPost } from 'web/lib/firebase/posts' +import { ContractSearch } from '../contract-search' +import { ContractCard } from '../contract/contract-card' + +import Masonry from 'react-masonry-css' + +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { SiteLink } from '../site-link' +import { GroupOverviewPost } from './group-overview-post' +import { getContractFromId } from 'web/lib/firebase/contracts' +import { groupPath, updateGroup } from 'web/lib/firebase/groups' +import { PinnedSelectModal } from '../pinned-select-modal' +import { Button } from '../button' +import { User } from 'common/user' +import { UserLink } from '../user-link' +import { EditGroupButton } from './edit-group-button' +import { JoinOrLeaveGroupButton } from './groups-button' +import { Linkify } from '../linkify' +import { ChoicesToggleGroup } from '../choices-toggle-group' +import { CopyLinkButton } from '../copy-link-button' +import { REFERRAL_AMOUNT } from 'common/economy' +import toast from 'react-hot-toast' +import { ENV_CONFIG } from 'common/envs/constants' +import { PostCard } from '../post-card' + +const MAX_TRENDING_POSTS = 6 + +export function GroupOverview(props: { + group: Group + isEditable: boolean + posts: Post[] + aboutPost: Post | null + creator: User + user: User | null | undefined + memberIds: string[] +}) { + const { group, isEditable, posts, aboutPost, creator, user, memberIds } = + props + return ( + + + + {(group.aboutPostId != null || isEditable) && ( + <> + + + + )} + + + + + ) +} + +function GroupOverviewPinned(props: { + group: Group + posts: Post[] + isEditable: boolean +}) { + const { group, posts, isEditable } = props + const [pinned, setPinned] = useState([]) + const [open, setOpen] = useState(false) + const [editMode, setEditMode] = useState(false) + + useEffect(() => { + async function getPinned() { + if (group.pinnedItems == null) { + updateGroup(group, { pinnedItems: [] }) + } else { + const itemComponents = await Promise.all( + group.pinnedItems.map(async (element) => { + if (element.type === 'post') { + const post = await getPost(element.itemId) + if (post) { + return + } + } else if (element.type === 'contract') { + const contract = await getContractFromId(element.itemId) + if (contract) { + return + } + } + }) + ) + setPinned( + itemComponents.filter( + (element) => element != undefined + ) as JSX.Element[] + ) + } + } + getPinned() + }, [group, group.pinnedItems]) + + async function onSubmit(selectedItems: { itemId: string; type: string }[]) { + await updateGroup(group, { + pinnedItems: [ + ...group.pinnedItems, + ...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]), + ], + }) + setOpen(false) + } + + return isEditable || pinned.length > 0 ? ( + <> + + + {isEditable && ( + + )} + +
+ + {pinned.length == 0 && !editMode && ( +
+

+ No pinned items yet. Click the edit button to add some! +

+
+ )} + {pinned.map((element, index) => ( +
+ {element} + + {editMode && ( + { + const newPinned = group.pinnedItems.filter((item) => { + return item.itemId !== group.pinnedItems[index].itemId + }) + updateGroup(group, { pinnedItems: newPinned }) + }} + /> + )} +
+ ))} + {editMode && group.pinnedItems && pinned.length < 6 && ( +
+ + + +
+ )} +
+
+ + Pin posts or markets to the overview of this group. + + } + onSubmit={onSubmit} + /> + + ) : ( + <> + ) +} + +function SectionHeader(props: { + label: string + href?: string + children?: ReactNode +}) { + const { label, href, children } = props + const content = ( + <> + {label}{' '} +