diff --git a/firestore.rules b/firestore.rules index 894367a6..d52569cd 100644 --- a/firestore.rules +++ b/firestore.rules @@ -47,5 +47,10 @@ service cloud.firestore { allow read; allow update: if request.auth.uid == resource.data.curatorId; } + + match /folds/{foldId}/followers/{userId} { + allow read; + allow write: if request.auth.uid == userId; + } } } \ No newline at end of file diff --git a/web/components/follow-fold-button.tsx b/web/components/follow-fold-button.tsx new file mode 100644 index 00000000..97e29deb --- /dev/null +++ b/web/components/follow-fold-button.tsx @@ -0,0 +1,50 @@ +import clsx from 'clsx' +import { Fold } from '../../common/fold' +import { useFollowingFold } from '../hooks/use-fold' +import { useUser } from '../hooks/use-user' +import { followFold, unfollowFold } from '../lib/firebase/folds' + +export function FollowFoldButton(props: { fold: Fold; className?: string }) { + const { fold, className } = props + + const user = useUser() + const following = useFollowingFold(fold, user) + + const onFollow = () => { + if (user) followFold(fold, user) + } + + const onUnfollow = () => { + if (user) unfollowFold(fold, user) + } + + if (!user || following === undefined) + return ( + + ) + + if (following) { + return ( + + ) + } + + return ( + + ) +} diff --git a/web/hooks/use-fold.ts b/web/hooks/use-fold.ts index f64694df..8caa58d2 100644 --- a/web/hooks/use-fold.ts +++ b/web/hooks/use-fold.ts @@ -1,6 +1,11 @@ import { useEffect, useState } from 'react' import { Fold } from '../../common/fold' -import { listenForFold, listenForFolds } from '../lib/firebase/folds' +import { User } from '../../common/user' +import { + listenForFold, + listenForFolds, + listenForFollow, +} from '../lib/firebase/folds' export const useFold = (foldId: string) => { const [fold, setFold] = useState() @@ -21,3 +26,13 @@ export const useFolds = () => { return folds } + +export const useFollowingFold = (fold: Fold, user: User | null | undefined) => { + const [following, setFollowing] = useState() + + useEffect(() => { + if (user) return listenForFollow(fold, user, setFollowing) + }, [fold, user]) + + return following +} diff --git a/web/lib/firebase/folds.ts b/web/lib/firebase/folds.ts index 3220f884..63d67806 100644 --- a/web/lib/firebase/folds.ts +++ b/web/lib/firebase/folds.ts @@ -1,7 +1,16 @@ -import { collection, doc, query, updateDoc, where } from 'firebase/firestore' +import { + collection, + deleteDoc, + doc, + query, + setDoc, + updateDoc, + where, +} from 'firebase/firestore' import { Fold } from '../../../common/fold' import { Contract, contractCollection } from './contracts' import { db } from './init' +import { User } from './users' import { getValues, listenForValue, listenForValues } from './utils' const foldCollection = collection(db, 'folds') @@ -90,3 +99,24 @@ export function listenForFold( ) { return listenForValue(doc(foldCollection, foldId), setFold) } + +export function followFold(fold: Fold, user: User) { + const followDoc = doc(foldCollection, fold.id, 'followers', user.id) + return setDoc(followDoc, { userId: user.id }) +} + +export function unfollowFold(fold: Fold, user: User) { + const followDoc = doc(foldCollection, fold.id, 'followers', user.id) + return deleteDoc(followDoc) +} + +export function listenForFollow( + fold: Fold, + user: User, + setFollow: (following: boolean) => void +) { + const followDoc = doc(foldCollection, fold.id, 'followers', user.id) + return listenForValue(followDoc, (value) => { + setFollow(!!value) + }) +} diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 4cdb41ca..885c5987 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -31,6 +31,7 @@ import { Leaderboard } from '../../../components/leaderboard' import { formatMoney } from '../../../lib/util/format' import { EditFoldButton } from '../../../components/edit-fold-button' import Custom404 from '../../404' +import { FollowFoldButton } from '../../../components/follow-fold-button' export async function getStaticProps(props: { params: { slugs: string[] } }) { const { slugs } = props.params @@ -155,7 +156,11 @@ export default function FoldPage(props: { - {isCurator && <EditFoldButton className="ml-1" fold={fold} />} + {isCurator ? ( + <EditFoldButton className="ml-1" fold={fold} /> + ) : ( + <FollowFoldButton className="ml-1" fold={fold} /> + )} </Row> <Col className="md:hidden text-gray-500 gap-2 mb-6 px-1"> diff --git a/web/pages/folds.tsx b/web/pages/folds.tsx index f0706b38..ac240def 100644 --- a/web/pages/folds.tsx +++ b/web/pages/folds.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { Fold } from '../../common/fold' import { CreateFoldButton } from '../components/create-fold-button' +import { FollowFoldButton } from '../components/follow-fold-button' import { Col } from '../components/layout/col' import { Row } from '../components/layout/row' import { Page } from '../components/page' @@ -82,9 +83,7 @@ export default function Folds(props: { </Link> <Row className="justify-between items-center gap-2"> <SiteLink href={foldPath(fold)}>{fold.name}</SiteLink> - <button className="btn btn-secondary btn-sm z-10 mb-1"> - Follow - </button> + <FollowFoldButton className="z-10 mb-1" fold={fold} /> </Row> <Row className="items-center gap-2 text-gray-500 text-sm"> <div>12 followers</div>