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 && } + + + + + ) +}