Skip to content

Commit

Permalink
Ingawei/add dashboard follows (#2036)
Browse files Browse the repository at this point in the history
* follow functionality
  • Loading branch information
ingawei committed Sep 13, 2023
1 parent 1d03ff2 commit 592e821
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 15 deletions.
2 changes: 2 additions & 0 deletions backend/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { followUser } from './follow-user'
import { report } from './report'
import { createdashboard } from './create-dashboard'
import { getyourdashboards } from './get-your-dashboards'
import { followdashboard } from './follow-dashboard'

const allowCors: RequestHandler = cors({
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
Expand Down Expand Up @@ -209,6 +210,7 @@ app.post('/report', ...apiRoute(report))

app.post('/createdashboard', ...apiRoute(createdashboard))
app.post('/getyourdashboards', ...apiRoute(getyourdashboards))
app.post('/followdashboard', ...apiRoute(followdashboard))

// Catch 404 errors - this should be the last route
app.use(allowCors, (req, res) => {
Expand Down
32 changes: 32 additions & 0 deletions backend/api/src/follow-dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getUserFollowsDashboard } from 'common/supabase/dashboard-follows'
import { run } from 'common/supabase/utils'
import { createSupabaseClient } from 'shared/supabase/init'
import { z } from 'zod'
import { authEndpoint } from './helpers'

const schema = z.object({
dashboardId: z.string(),
})

export const followdashboard = authEndpoint(async (req, auth) => {
const { dashboardId } = schema.parse(req.body)
const followerId = auth.uid

const db = createSupabaseClient()

const isFollowing = await getUserFollowsDashboard(followerId, dashboardId, db)

const query = isFollowing
? db
.from('dashboard_follows')
.delete()
.eq('dashboard_id', dashboardId)
.eq('follower_id', followerId)
: db
.from('dashboard_follows')
.upsert([{ dashboard_id: dashboardId, follower_id: followerId }])

await run(query)

return { isFollowing: !isFollowing }
})
7 changes: 7 additions & 0 deletions backend/supabase/dashboard_follows/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
create table if not exists
dashboard_follows (
dashboard_id text not null,
follower_id text not null,
created_time timestamptz default now(),
primary key (dashboard_id, follower_id)
);
19 changes: 19 additions & 0 deletions common/src/supabase/dashboard-follows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SupabaseClient } from '@supabase/supabase-js'
import { run } from './utils'

export async function getUserFollowsDashboard(
userId: string,
dashboardId: string,
db: SupabaseClient
) {
const { data: follows } = await run(
db
.from('dashboard_follows')
.select('*')
.eq('follower_id', userId)
.eq('dashboard_id', dashboardId)
.limit(1)
)

return follows.length > 0
}
45 changes: 45 additions & 0 deletions web/components/dashboard/follow-dashboard-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { FaBookmark, FaRegBookmark } from 'react-icons/fa6'
import { useUserFollowsDashboard } from 'web/hooks/use-dashboard-follows'
import { useIsAuthorized, useUser } from 'web/hooks/use-user'
import { followDashboard } from 'web/lib/firebase/api'
import { Tooltip } from '../widgets/tooltip'
import { Col } from '../layout/col'
import clsx from 'clsx'

export function FollowDashboardButton(props: {
dashboardId: string
dashboardCreatorId: string
size?: 'sm' | 'md'
}) {
const { dashboardId, dashboardCreatorId, size = 'md' } = props
const user = useUser()
const { isFollowing, setIsFollowing } = useUserFollowsDashboard(
user?.id,
dashboardId
)
const isAuth = useIsAuthorized()
if (
!user ||
!isAuth
|| dashboardCreatorId === user?.id
) {
return null
}
return (
<Tooltip text={'Bookmark'} placement="left-start">
<button
onClick={() =>
followDashboard({ dashboardId: dashboardId }).then((result) => {
setIsFollowing(result.isFollowing)
})
}
>
{isFollowing ? (
<FaBookmark className={'h-5 w-5 text-yellow-500'} />
) : (
<FaRegBookmark className={'text-ink-500 h-5 w-5'} />
)}
</button>
</Tooltip>
)
}
19 changes: 19 additions & 0 deletions web/hooks/use-dashboard-follows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getUserFollowsDashboard } from 'common/supabase/dashboard-follows'
import { get } from 'lodash'
import { useEffect, useState } from 'react'
import { db } from 'web/lib/supabase/db'

export const useUserFollowsDashboard = (
userId: string | undefined,
dashboardId: string
) => {
const [isFollowing, setIsFollowing] = useState<boolean | undefined>(undefined)
useEffect(() => {
if (userId) {
getUserFollowsDashboard(userId, dashboardId, db).then((res) => {
setIsFollowing(res)
})
}
}, [userId, dashboardId])
return { isFollowing, setIsFollowing }
}
4 changes: 4 additions & 0 deletions web/lib/firebase/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,7 @@ export function createDashboard(params: {
export function getYourDashboards() {
return call(getApiUrl('getyourdashboards'), 'POST')
}

export function followDashboard(params: { dashboardId: string }) {
return call(getApiUrl('followdashboard'), 'POST', params)
}
17 changes: 15 additions & 2 deletions web/pages/dashboard/[dashboardSlug].tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Dashboard, DashboardItem } from 'common/dashboard'
import { getDashboardFromSlug } from 'common/supabase/dashboard'
import { getUserFollowsDashboard } from 'common/supabase/dashboard-follows'
import { useState } from 'react'
import { FaBookmark, FaRegBookmark } from 'react-icons/fa6'
import { DashboardContent } from 'web/components/dashboard/dashboard-content'
import { DashboardSidebar } from 'web/components/dashboard/dashboard-sidebar'
import { FollowDashboardButton } from 'web/components/dashboard/follow-dashboard-button'
import { Col } from 'web/components/layout/col'
import { Page } from 'web/components/layout/page'
import { Row } from 'web/components/layout/row'
import { Title } from 'web/components/widgets/title'
import { useUserFollowsDashboard } from 'web/hooks/use-dashboard-follows'
import { useUser } from 'web/hooks/use-user'
import { initSupabaseAdmin } from 'web/lib/supabase/admin-db'

export async function getStaticProps(ctx: {
Expand Down Expand Up @@ -38,6 +44,7 @@ export async function getStaticPaths() {
export default function DashboardPage(props: { dashboard: Dashboard }) {
const { dashboard } = props
const [items, setItems] = useState<DashboardItem[]>(dashboard.items)
const user = useUser()
return (
<Page
trackPageView={'dashboard slug page'}
Expand All @@ -48,10 +55,16 @@ export default function DashboardPage(props: { dashboard: Dashboard }) {
>
<Col className="items-center">
<Col className="w-full max-w-2xl px-1 sm:px-2">
<Title className="mt-4">{dashboard.title}</Title>
<Row className="w-full items-center justify-between">
<Title className="mt-4">{dashboard.title}</Title>
<FollowDashboardButton
dashboardId={dashboard.id}
dashboardCreatorId={dashboard.creator_id}
/>
</Row>
<DashboardSidebar description={dashboard.description} />
<DashboardContent
items={dashboard.items}
items={items}
onRemove={(slugOrUrl: string) => {
setItems((items) => {
return items.filter((item) => {
Expand Down
35 changes: 22 additions & 13 deletions web/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Link from 'next/link'
import { Avatar } from 'web/components/widgets/avatar'
import { UserLink } from 'web/components/widgets/user-link'
import clsx from 'clsx'
import { FollowDashboardButton } from 'web/components/dashboard/follow-dashboard-button'

export default function DashboardPage() {
useRedirectIfSignedOut()
Expand Down Expand Up @@ -63,24 +64,32 @@ function DashboardPreviews(props: { dashboards?: Dashboard[] }) {
function DashboardPreview(props: { dashboard: Dashboard }) {
const { dashboard } = props
const { slug, creator_avatar_url, creator_username, creator_name } = dashboard
const user = useUser()
return (
<Link
href={`/dashboard/${slug}`}
className=" bg-canvas-0 border-canvas-0 hover:border-primary-300 flex flex-col gap-2 rounded-lg border px-4 py-2 transition-colors"
>
<Row className={'text-ink-500 items-center gap-1 text-sm'}>
<Avatar
size={'xs'}
className={'mr-0.5'}
avatarUrl={creator_avatar_url}
username={creator_username}
/>
<UserLink
name={creator_name}
username={creator_username}
className={clsx(
'w-full max-w-[10rem] text-ellipsis sm:max-w-[12rem]'
)}
<Row className="w-full items-center justify-between">
<Row className={'text-ink-500 items-center gap-1 text-sm'}>
<Avatar
size={'xs'}
className={'mr-0.5'}
avatarUrl={creator_avatar_url}
username={creator_username}
/>
<UserLink
name={creator_name}
username={creator_username}
className={clsx(
'w-full max-w-[10rem] text-ellipsis sm:max-w-[12rem]'
)}
/>
</Row>
<FollowDashboardButton
dashboardId={dashboard.id}
dashboardCreatorId={dashboard.creator_id}
size={'sm'}
/>
</Row>
<div className="text-lg">{dashboard.title}</div>
Expand Down

3 comments on commit 592e821

@vercel
Copy link

@vercel vercel bot commented on 592e821 Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs-mantic.vercel.app
docs-pi-teal.vercel.app
docs-git-main-mantic.vercel.app
docs.manifold.markets

@vercel
Copy link

@vercel vercel bot commented on 592e821 Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 592e821 Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dev – ./web

dev-manifold.vercel.app
dev-mantic.vercel.app
dev-git-main-mantic.vercel.app
dev.manifold.markets

Please sign in to comment.