Fast fold following (#51)
* fast follow folds * FastFoldFollowing component on homepage
This commit is contained in:
		
							parent
							
								
									693652935d
								
							
						
					
					
						commit
						13727bb19f
					
				
							
								
								
									
										107
									
								
								web/components/fast-fold-following.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								web/components/fast-fold-following.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | import clsx from 'clsx' | ||||||
|  | import { useState } from 'react' | ||||||
|  | import { SearchIcon } from '@heroicons/react/outline' | ||||||
|  | 
 | ||||||
|  | import { User } from '../../common/user' | ||||||
|  | import { followFoldFromSlug, unfollowFoldFromSlug } from '../lib/firebase/folds' | ||||||
|  | import { Row } from './layout/row' | ||||||
|  | import { Spacer } from './layout/spacer' | ||||||
|  | 
 | ||||||
|  | function FollowFoldButton(props: { | ||||||
|  |   fold: { slug: string; name: string } | ||||||
|  |   user: User | null | undefined | ||||||
|  |   isFollowed?: boolean | ||||||
|  | }) { | ||||||
|  |   const { fold, user, isFollowed } = props | ||||||
|  |   const { slug, name } = fold | ||||||
|  | 
 | ||||||
|  |   const [followed, setFollowed] = useState(isFollowed) | ||||||
|  | 
 | ||||||
|  |   const onClick = async () => { | ||||||
|  |     if (followed) { | ||||||
|  |       if (user) await unfollowFoldFromSlug(slug, user.id) | ||||||
|  |       setFollowed(false) | ||||||
|  |     } else { | ||||||
|  |       if (user) await followFoldFromSlug(slug, user.id) | ||||||
|  |       setFollowed(true) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       className={clsx( | ||||||
|  |         'rounded-full border-2 px-4 py-1 shadow-md', | ||||||
|  |         'cursor-pointer', | ||||||
|  |         followed ? 'bg-gray-300 border-gray-300' : 'bg-white' | ||||||
|  |       )} | ||||||
|  |       onClick={onClick} | ||||||
|  |     > | ||||||
|  |       <span className="text-sm text-gray-500">{name}</span> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function FollowFolds(props: { | ||||||
|  |   folds: { slug: string; name: string }[] | ||||||
|  |   followedFoldSlugs: string[] | ||||||
|  |   noLabel?: boolean | ||||||
|  |   className?: string | ||||||
|  |   user: User | null | undefined | ||||||
|  | }) { | ||||||
|  |   const { folds, noLabel, className, user, followedFoldSlugs } = props | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Row className={clsx('flex-wrap items-center gap-2', className)}> | ||||||
|  |       {folds.length > 0 && ( | ||||||
|  |         <> | ||||||
|  |           {!noLabel && <div className="mr-1 text-gray-500">Communities</div>} | ||||||
|  |           {folds.map((fold) => ( | ||||||
|  |             <FollowFoldButton | ||||||
|  |               key={fold.slug + followedFoldSlugs.length} | ||||||
|  |               user={user} | ||||||
|  |               fold={fold} | ||||||
|  |               isFollowed={followedFoldSlugs.includes(fold.slug)} | ||||||
|  |             /> | ||||||
|  |           ))} | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|  |     </Row> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const FastFoldFollowing = (props: { | ||||||
|  |   followedFoldSlugs: string[] | ||||||
|  |   user: User | null | undefined | ||||||
|  | }) => { | ||||||
|  |   const { followedFoldSlugs, user } = props | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Row className="mx-3 mb-3 items-center gap-2 text-sm text-gray-800"> | ||||||
|  |         <SearchIcon className="inline h-5 w-5" aria-hidden="true" /> | ||||||
|  |         Personalize your feed — click on a community to follow | ||||||
|  |       </Row> | ||||||
|  | 
 | ||||||
|  |       <FollowFolds | ||||||
|  |         className="mx-2" | ||||||
|  |         noLabel | ||||||
|  |         user={user} | ||||||
|  |         followedFoldSlugs={followedFoldSlugs} | ||||||
|  |         folds={[ | ||||||
|  |           { name: 'Politics', slug: 'politics' }, | ||||||
|  |           { name: 'Crypto', slug: 'crypto' }, | ||||||
|  |           { name: 'Sports', slug: 'sports' }, | ||||||
|  |           { name: 'Science', slug: 'science' }, | ||||||
|  |           { name: 'Covid', slug: 'covid' }, | ||||||
|  |           { name: 'AI', slug: 'ai' }, | ||||||
|  |           { | ||||||
|  |             name: 'Manifold Markets', | ||||||
|  |             slug: 'manifold-markets', | ||||||
|  |           }, | ||||||
|  |         ]} | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <Spacer h={10} /> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | @ -11,7 +11,7 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) { | ||||||
|   const following = useFollowingFold(fold, user) |   const following = useFollowingFold(fold, user) | ||||||
| 
 | 
 | ||||||
|   const onFollow = () => { |   const onFollow = () => { | ||||||
|     if (user) followFold(fold, user) |     if (user) followFold(fold.id, user.id) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const onUnfollow = () => { |   const onUnfollow = () => { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import clsx from 'clsx' | import clsx from 'clsx' | ||||||
|  | 
 | ||||||
| import { Row } from './layout/row' | import { Row } from './layout/row' | ||||||
| import { SiteLink } from './site-link' | import { SiteLink } from './site-link' | ||||||
| 
 | 
 | ||||||
|  | @ -47,6 +48,7 @@ export function TagsList(props: { | ||||||
| export function FoldTag(props: { fold: { slug: string; name: string } }) { | export function FoldTag(props: { fold: { slug: string; name: string } }) { | ||||||
|   const { fold } = props |   const { fold } = props | ||||||
|   const { slug, name } = fold |   const { slug, name } = fold | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <SiteLink href={`/fold/${slug}`} className="flex items-center"> |     <SiteLink href={`/fold/${slug}`} className="flex items-center"> | ||||||
|       <div |       <div | ||||||
|  |  | ||||||
|  | @ -118,9 +118,9 @@ export function listenForFold( | ||||||
|   return listenForValue(doc(foldCollection, foldId), setFold) |   return listenForValue(doc(foldCollection, foldId), setFold) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function followFold(fold: Fold, user: User) { | export function followFold(foldId: string, userId: string) { | ||||||
|   const followDoc = doc(foldCollection, fold.id, 'followers', user.id) |   const followDoc = doc(foldCollection, foldId, 'followers', userId) | ||||||
|   return setDoc(followDoc, { userId: user.id }) |   return setDoc(followDoc, { userId }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function unfollowFold(fold: Fold, user: User) { | export function unfollowFold(fold: Fold, user: User) { | ||||||
|  | @ -128,6 +128,26 @@ export function unfollowFold(fold: Fold, user: User) { | ||||||
|   return deleteDoc(followDoc) |   return deleteDoc(followDoc) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function followFoldFromSlug(slug: string, userId: string) { | ||||||
|  |   const snap = await getDocs(query(foldCollection, where('slug', '==', slug))) | ||||||
|  |   if (snap.empty) return undefined | ||||||
|  | 
 | ||||||
|  |   const foldDoc = snap.docs[0] | ||||||
|  |   const followDoc = doc(foldDoc.ref, 'followers', userId) | ||||||
|  | 
 | ||||||
|  |   return setDoc(followDoc, { userId }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function unfollowFoldFromSlug(slug: string, userId: string) { | ||||||
|  |   const snap = await getDocs(query(foldCollection, where('slug', '==', slug))) | ||||||
|  |   if (snap.empty) return undefined | ||||||
|  | 
 | ||||||
|  |   const foldDoc = snap.docs[0] | ||||||
|  |   const followDoc = doc(foldDoc.ref, 'followers', userId) | ||||||
|  | 
 | ||||||
|  |   return deleteDoc(followDoc) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function listenForFollow( | export function listenForFollow( | ||||||
|   fold: Fold, |   fold: Fold, | ||||||
|   user: User, |   user: User, | ||||||
|  |  | ||||||
|  | @ -73,7 +73,8 @@ export default function Folds(props: { | ||||||
| 
 | 
 | ||||||
|             <div className="mb-6 text-gray-500"> |             <div className="mb-6 text-gray-500"> | ||||||
|               Communities on Manifold are centered around a collection of |               Communities on Manifold are centered around a collection of | ||||||
|               markets. |               markets. Follow a community to personalize your feed and receive | ||||||
|  |               relevant updates. | ||||||
|             </div> |             </div> | ||||||
|           </Col> |           </Col> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -17,12 +17,10 @@ import { Fold } from '../../common/fold' | ||||||
| import { filterDefined } from '../../common/util/array' | import { filterDefined } from '../../common/util/array' | ||||||
| import { useUserBetContracts } from '../hooks/use-user-bets' | import { useUserBetContracts } from '../hooks/use-user-bets' | ||||||
| import { LoadingIndicator } from '../components/loading-indicator' | import { LoadingIndicator } from '../components/loading-indicator' | ||||||
| import { FoldTagList } from '../components/tags-list' |  | ||||||
| import { SearchIcon } from '@heroicons/react/outline' |  | ||||||
| import { Row } from '../components/layout/row' | import { Row } from '../components/layout/row' | ||||||
| import { SparklesIcon } from '@heroicons/react/solid' | import { SparklesIcon } from '@heroicons/react/solid' | ||||||
| import { useFollowedFolds } from '../hooks/use-fold' | import { useFollowedFolds } from '../hooks/use-fold' | ||||||
| import { SiteLink } from '../components/site-link' | import { FastFoldFollowing } from '../components/fast-fold-following' | ||||||
| 
 | 
 | ||||||
| export async function getStaticProps() { | export async function getStaticProps() { | ||||||
|   let [contracts, folds] = await Promise.all([ |   let [contracts, folds] = await Promise.all([ | ||||||
|  | @ -116,39 +114,19 @@ const Home = (props: { | ||||||
|         <Col className="w-full max-w-3xl"> |         <Col className="w-full max-w-3xl"> | ||||||
|           <FeedCreate user={user ?? undefined} /> |           <FeedCreate user={user ?? undefined} /> | ||||||
|           <Spacer h={6} /> |           <Spacer h={6} /> | ||||||
|           <Row className="mx-3 mb-3 items-center gap-2 text-sm text-gray-800"> | 
 | ||||||
|             <SearchIcon className="inline h-5 w-5" aria-hidden="true" /> |           {followedFolds.length === 0 && ( | ||||||
|             Explore our communities |             <FastFoldFollowing | ||||||
|           </Row> |               user={user} | ||||||
|           <FoldTagList |               followedFoldSlugs={followedFolds.map((f) => f.slug)} | ||||||
|             className="mx-2" |             /> | ||||||
|             noLabel |           )} | ||||||
|             folds={[ | 
 | ||||||
|               { name: 'Politics', slug: 'politics' }, |  | ||||||
|               { name: 'Crypto', slug: 'crypto' }, |  | ||||||
|               { name: 'Sports', slug: 'sports' }, |  | ||||||
|               { name: 'Science', slug: 'science' }, |  | ||||||
|               { name: 'Covid', slug: 'covid' }, |  | ||||||
|               { name: 'AI', slug: 'ai' }, |  | ||||||
|               { |  | ||||||
|                 name: 'Manifold Markets', |  | ||||||
|                 slug: 'manifold-markets', |  | ||||||
|               }, |  | ||||||
|             ]} |  | ||||||
|           /> |  | ||||||
|           <Spacer h={10} /> |  | ||||||
|           <Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row"> |           <Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row"> | ||||||
|             <Row className="gap-2"> |             <Row className="gap-2"> | ||||||
|               <SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> |               <SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> | ||||||
|               <span className="whitespace-nowrap">Recent activity</span> |               <span className="whitespace-nowrap">Recent activity</span> | ||||||
|               <span className="hidden sm:flex">—</span> |  | ||||||
|             </Row> |             </Row> | ||||||
|             <div className="text-gray-500 sm:text-gray-800"> |  | ||||||
|               <SiteLink href="/folds" className="font-semibold"> |  | ||||||
|                 Follow a community |  | ||||||
|               </SiteLink>{' '} |  | ||||||
|               to personalize |  | ||||||
|             </div> |  | ||||||
|           </Col> |           </Col> | ||||||
| 
 | 
 | ||||||
|           {activeContracts ? ( |           {activeContracts ? ( | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user