From e902e42fffc6fbfe46ea8f852a6a731f69097e40 Mon Sep 17 00:00:00 2001
From: ingawei <46611122+ingawei@users.noreply.github.com>
Date: Thu, 7 Sep 2023 18:30:30 -0700
Subject: [PATCH] dashboard starting (#2018)
* dashboard starting
---
backend/api/src/app.ts | 3 +
backend/api/src/create-dashboard.ts | 40 ++++++
backend/supabase/dashboards/create.sql | 11 ++
common/src/dashboard.ts | 11 ++
common/src/envs/constants.ts | 2 +
common/src/supabase/dashboard.ts | 13 ++
.../contract/contract-description.tsx | 20 +--
.../contract/feed-contract-card.tsx | 5 +-
.../dashboard/create-dashboard-button.tsx | 16 +++
.../dashboard/dashboard-sidebar.tsx | 31 ++++
web/lib/firebase/api.ts | 7 +
web/pages/dashboard/[dashboardSlug].tsx | 50 +++++++
web/pages/dashboard/create.tsx | 135 ++++++++++++++++++
web/pages/dashboard/index.tsx | 30 ++++
14 files changed, 358 insertions(+), 16 deletions(-)
create mode 100644 backend/api/src/create-dashboard.ts
create mode 100644 backend/supabase/dashboards/create.sql
create mode 100644 common/src/dashboard.ts
create mode 100644 common/src/supabase/dashboard.ts
create mode 100644 web/components/dashboard/create-dashboard-button.tsx
create mode 100644 web/components/dashboard/dashboard-sidebar.tsx
create mode 100644 web/pages/dashboard/[dashboardSlug].tsx
create mode 100644 web/pages/dashboard/create.tsx
create mode 100644 web/pages/dashboard/index.tsx
diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts
index 6cf647e745..3a657a98ec 100644
--- a/backend/api/src/app.ts
+++ b/backend/api/src/app.ts
@@ -81,6 +81,7 @@ import { castpollvote } from './cast-poll-vote'
import { getsimilargroupstocontract } from 'api/get-similar-groups-to-contract'
import { followUser } from './follow-user'
import { report } from './report'
+import { createdashboard } from './create-dashboard'
const allowCors: RequestHandler = cors({
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
@@ -205,6 +206,8 @@ app.post('/claimdestinysub', ...apiRoute(claimdestinysub))
app.post('/follow-user', ...apiRoute(followUser))
app.post('/report', ...apiRoute(report))
+app.post('/createdashboard', ...apiRoute(createdashboard))
+
// Catch 404 errors - this should be the last route
app.use(allowCors, (req, res) => {
res
diff --git a/backend/api/src/create-dashboard.ts b/backend/api/src/create-dashboard.ts
new file mode 100644
index 0000000000..1b17b2ea9f
--- /dev/null
+++ b/backend/api/src/create-dashboard.ts
@@ -0,0 +1,40 @@
+import { createSupabaseDirectClient } from 'shared/supabase/init'
+import { log } from 'shared/utils'
+import { z } from 'zod'
+import { APIError, authEndpoint, validate } from './helpers'
+import { contentSchema } from 'shared/zod-types'
+import { slugify } from 'common/util/slugify'
+import { randomString } from 'common/util/random'
+
+const schema = z.object({
+ title: z.string(),
+ description: contentSchema.optional(),
+})
+
+export const createdashboard = authEndpoint(async (req, auth) => {
+ const { title, description } = validate(schema, req.body)
+
+ log('creating dashboard')
+ const pg = createSupabaseDirectClient()
+
+ let slug = slugify(title)
+ const data = await pg.manyOrNone(
+ `select slug from dashboards where slug = $1`,
+ [slug]
+ )
+
+ if (data && data.length > 0) {
+ slug = `${slug}-${randomString(8)}`
+ }
+
+ // create if not exists the group invite link row
+ const { id } = await pg.one(
+ `insert into dashboards(slug, creator_id, description, title)
+ values ($1, $2, $3,$4)
+ returning id, slug`,
+ [slug, auth.uid, description, title]
+ )
+
+ // return something
+ return { id: id, slug: slug }
+})
diff --git a/backend/supabase/dashboards/create.sql b/backend/supabase/dashboards/create.sql
new file mode 100644
index 0000000000..f516e246b0
--- /dev/null
+++ b/backend/supabase/dashboards/create.sql
@@ -0,0 +1,11 @@
+create table if not exists
+ dashboards (
+ id text not null primary key default random_alphanumeric (12),
+ slug text not null unique,
+ creator_id text not null,
+ foreign key (creator_id) references users (id),
+ created_time timestamptz not null default now(),
+ views numeric not null default 0,
+ description json,
+ title text not null
+ );
diff --git a/common/src/dashboard.ts b/common/src/dashboard.ts
new file mode 100644
index 0000000000..2cb89774aa
--- /dev/null
+++ b/common/src/dashboard.ts
@@ -0,0 +1,11 @@
+import { JSONContent } from '@tiptap/core'
+
+export type Dashboard = {
+ id: string
+ slug: string
+ creator_id: string
+ created_time: number
+ views: number
+ description: JSONContent
+ title: string
+}
diff --git a/common/src/envs/constants.ts b/common/src/envs/constants.ts
index a12be3e07c..d21fefc326 100644
--- a/common/src/envs/constants.ts
+++ b/common/src/envs/constants.ts
@@ -12,6 +12,8 @@ export const CONFIGS: { [env: string]: EnvConfig } = {
DEV: DEV_CONFIG,
}
+export const DASHBOARD_ENABLED = ENV === 'DEV'
+
export const ENV_CONFIG = CONFIGS[ENV]
export function isAdminId(id: string) {
diff --git a/common/src/supabase/dashboard.ts b/common/src/supabase/dashboard.ts
new file mode 100644
index 0000000000..bba08784da
--- /dev/null
+++ b/common/src/supabase/dashboard.ts
@@ -0,0 +1,13 @@
+import { SupabaseClient } from '@supabase/supabase-js'
+import { run } from './utils'
+
+export async function getDashboardFromSlug(slug: string, db: SupabaseClient) {
+ const { data: dashboard } = await run(
+ db.from('dashboards').select('*').eq('slug', slug).limit(1)
+ )
+
+ if (dashboard && dashboard.length > 0) {
+ return dashboard[0]
+ }
+ return null
+}
diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx
index 335025dee6..0eb5273e1f 100644
--- a/web/components/contract/contract-description.tsx
+++ b/web/components/contract/contract-description.tsx
@@ -13,7 +13,7 @@ import { CollapsibleContent } from '../widgets/collapsible-content'
import { isTrustworthy } from 'common/envs/constants'
import { ContractEditHistoryButton } from 'web/components/contract/contract-edit-history-button'
import { PencilIcon, PlusIcon } from '@heroicons/react/solid'
-import { Editor } from '@tiptap/core'
+import { Editor, JSONContent } from '@tiptap/core'
import { CreateAnswerCpmmPanel } from '../answers/create-answer-panel'
export function ContractDescription(props: {
@@ -224,19 +224,13 @@ function AddAnswerButton(props: {
)
}
-export function descriptionIsEmpty(contract: Contract) {
- const description = contract.description
+export function JSONEmpty(text: string | JSONContent) {
+ if (!text) return true
- if (!description) return true
-
- if (typeof description === 'string') {
- return description === ''
- } else if ('content' in description) {
- return !(
- description.content &&
- description.content.length > 0 &&
- description.content[0].content
- )
+ if (typeof text === 'string') {
+ return text === ''
+ } else if ('content' in text) {
+ return !(text.content && text.content.length > 0 && text.content[0].content)
}
return true
}
diff --git a/web/components/contract/feed-contract-card.tsx b/web/components/contract/feed-contract-card.tsx
index 896f4dba3d..b25ce121fd 100644
--- a/web/components/contract/feed-contract-card.tsx
+++ b/web/components/contract/feed-contract-card.tsx
@@ -31,11 +31,11 @@ import { Col } from '../layout/col'
import { Row } from '../layout/row'
import { PollPanel } from '../poll/poll-panel'
import { ClickFrame } from '../widgets/click-frame'
-import { descriptionIsEmpty } from './contract-description'
import { LikeButton } from './like-button'
import { TradesButton } from './trades-button'
import { FeedDropdown } from '../feed/card-dropdown'
import { GroupTags } from '../feed/feed-timeline-items'
+import { JSONEmpty } from './contract-description'
export function FeedContractCard(props: {
contract: Contract
@@ -229,8 +229,7 @@ export function FeedContractCard(props: {
)}
- {!descriptionIsEmpty(contract) &&
- !small &&
+ {!JSONEmpty(contract.description) &&
(item?.dataType == 'new_contract' || nonTextDescription) && (
+
+ Create a dashboard
+
+ >
+ )
+}
diff --git a/web/components/dashboard/dashboard-sidebar.tsx b/web/components/dashboard/dashboard-sidebar.tsx
new file mode 100644
index 0000000000..7860c46a4c
--- /dev/null
+++ b/web/components/dashboard/dashboard-sidebar.tsx
@@ -0,0 +1,31 @@
+import { JSONContent } from '@tiptap/core'
+import { JSONEmpty } from '../contract/contract-description'
+import { Col } from '../layout/col'
+import { Content } from '../widgets/editor'
+import clsx from 'clsx'
+
+export const DashboardSidebar = (props: {
+ description?: JSONContent
+ inSidebar?: boolean
+}) => {
+ const { description, inSidebar } = props
+
+ if (!description || JSONEmpty(description)) return <>>
+
+ return (
+
+ {description && (
+
+ Additional Context
+
+ )}
+ {description && (
+ <>
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts
index 29146b4ddc..025d7aa416 100644
--- a/web/lib/firebase/api.ts
+++ b/web/lib/firebase/api.ts
@@ -471,3 +471,10 @@ export function unfollowUser(userId: string) {
export function report(params: ReportProps) {
return call(getApiUrl('report'), 'POST', params)
}
+
+export function createDashboard(params: {
+ title: string
+ description?: JSONContent
+}) {
+ return call(getApiUrl('createdashboard'), 'POST', params)
+}
diff --git a/web/pages/dashboard/[dashboardSlug].tsx b/web/pages/dashboard/[dashboardSlug].tsx
new file mode 100644
index 0000000000..ecebb7b8f5
--- /dev/null
+++ b/web/pages/dashboard/[dashboardSlug].tsx
@@ -0,0 +1,50 @@
+import { Dashboard } from 'common/dashboard'
+import { getDashboardFromSlug } from 'common/supabase/dashboard'
+import { DashboardSidebar } from 'web/components/dashboard/dashboard-sidebar'
+import { Col } from 'web/components/layout/col'
+import { Page } from 'web/components/layout/page'
+import { NewsSidebar } from 'web/components/news/news-dashboard'
+import { Content } from 'web/components/widgets/editor'
+import { Title } from 'web/components/widgets/title'
+import { db } from 'web/lib/supabase/db'
+
+export async function getStaticProps(ctx: {
+ params: { dashboardSlug: string }
+}) {
+ const { dashboardSlug } = ctx.params
+
+ try {
+ const dashboard: Dashboard = await getDashboardFromSlug(dashboardSlug, db)
+ return { props: { dashboard } }
+ } catch (e) {
+ if (typeof e === 'object' && e !== null && 'code' in e && e.code === 404) {
+ return {
+ props: { state: 'not found' },
+ revalidate: 60,
+ }
+ }
+ throw e
+ }
+}
+
+export async function getStaticPaths() {
+ return { paths: [], fallback: 'blocking' }
+}
+
+export default function DashboardPage(props: { dashboard: Dashboard }) {
+ const { dashboard } = props
+ return (
+
+ }
+ >
+
+
+ {dashboard.title}
+
+
+
+
+ )
+}
diff --git a/web/pages/dashboard/create.tsx b/web/pages/dashboard/create.tsx
new file mode 100644
index 0000000000..9e39d9ea25
--- /dev/null
+++ b/web/pages/dashboard/create.tsx
@@ -0,0 +1,135 @@
+import { track } from '@amplitude/analytics-browser'
+import clsx from 'clsx'
+import { MAX_DESCRIPTION_LENGTH } from 'common/contract'
+import { removeUndefinedProps } from 'common/util/object'
+import router from 'next/router'
+import { useEffect, useState } from 'react'
+import { SEO } from 'web/components/SEO'
+import { Button } from 'web/components/buttons/button'
+import { Col } from 'web/components/layout/col'
+import { Page } from 'web/components/layout/page'
+import { Spacer } from 'web/components/layout/spacer'
+import { TextEditor, useTextEditor } from 'web/components/widgets/editor'
+import { ExpandingInput } from 'web/components/widgets/expanding-input'
+import { Title } from 'web/components/widgets/title'
+import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'
+import { createDashboard } from 'web/lib/firebase/api'
+
+export default function CreateDashboard() {
+ const [title, setTitle] = usePersistentLocalState(
+ '',
+ 'create dashboard title'
+ )
+
+ const editor = useTextEditor({
+ key: 'create dashbord dsecription',
+ max: MAX_DESCRIPTION_LENGTH,
+ placeholder: 'Optional. Provide background info and details.',
+ })
+
+ const [submitState, setSubmitState] = useState<
+ 'EDITING' | 'LOADING' | 'DONE'
+ >('EDITING')
+
+ const [errorText, setErrorText] = useState('')
+
+ const isValid = title.length > 0
+
+ useEffect(() => {
+ setErrorText('')
+ }, [isValid])
+
+ const resetProperties = () => {
+ editor?.commands.clearContent(true)
+ setTitle('')
+ }
+
+ async function submit() {
+ if (!isValid) return
+ setSubmitState('LOADING')
+ try {
+ const createProps = removeUndefinedProps({
+ title,
+ description: editor?.getJSON(),
+ })
+ const newDashboard = await createDashboard(createProps)
+
+ track('create market', {
+ id: newDashboard.id,
+ slug: newDashboard.slug,
+ })
+
+ resetProperties()
+ setSubmitState('DONE')
+
+ try {
+ await router.push(`/dashboard/${newDashboard.slug}`)
+ } catch (error) {
+ console.error(error)
+ }
+ } catch (e) {
+ console.error('error creating dashboard', e)
+ setErrorText((e as any).message || 'Error creating contract')
+ setSubmitState('EDITING')
+ }
+ }
+
+ return (
+
+
+
+ Create a Dashboard
+
+
+
+ setTitle(e.target.value || '')}
+ className="bg-canvas-50"
+ />
+
+
+
+
+
+
+
+ {errorText}
+
+
+
+
+ )
+}
diff --git a/web/pages/dashboard/index.tsx b/web/pages/dashboard/index.tsx
new file mode 100644
index 0000000000..485075a7b9
--- /dev/null
+++ b/web/pages/dashboard/index.tsx
@@ -0,0 +1,30 @@
+import { DASHBOARD_ENABLED } from 'common/envs/constants'
+import { CreateDashboardButton } from 'web/components/dashboard/create-dashboard-button'
+import { Col } from 'web/components/layout/col'
+import { Page } from 'web/components/layout/page'
+import { Row } from 'web/components/layout/row'
+import { useRedirectIfSignedOut } from 'web/hooks/use-redirect-if-signed-out'
+import { useUser } from 'web/hooks/use-user'
+import Custom404 from '../404'
+
+export default function DashboardPage() {
+ useRedirectIfSignedOut()
+ const user = useUser()
+
+ if (!DASHBOARD_ENABLED) {
+ return
+ }
+
+ return (
+
+
+
+
+ Dashboards
+ {user && }
+
+
+
+
+ )
+}