Added a create-dashboard and sharable view dashboard page
This commit is contained in:
parent
5682e5aab2
commit
760a112c3b
46
web/components/share-dashboard-modal.tsx
Normal file
46
web/components/share-dashboard-modal.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { LinkIcon } from '@heroicons/react/outline'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Title } from './title'
|
||||||
|
import { Button } from './button'
|
||||||
|
import { TweetButton } from './tweet-button'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
|
export function ShareDashboardModal(props: {
|
||||||
|
shareUrl: string
|
||||||
|
isOpen: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { isOpen, setOpen, shareUrl } = props
|
||||||
|
|
||||||
|
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} setOpen={setOpen} size="md">
|
||||||
|
<Col className="gap-4 rounded bg-white p-4">
|
||||||
|
<Title className="!mt-0 !mb-2" text="Share this dashboard" />
|
||||||
|
<Button
|
||||||
|
size="2xl"
|
||||||
|
color="gradient"
|
||||||
|
className={'mb-2 flex max-w-xs self-center'}
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(shareUrl)
|
||||||
|
toast.success('Link copied!', {
|
||||||
|
icon: linkIcon,
|
||||||
|
})
|
||||||
|
track('copy share link')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{linkIcon} Copy link
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Row className="z-0 justify-start gap-4 self-center">
|
||||||
|
<TweetButton className="self-start" tweetText={shareUrl} />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,11 +1,18 @@
|
||||||
import { deleteDoc, doc, updateDoc } from 'firebase/firestore'
|
import {
|
||||||
|
deleteDoc,
|
||||||
|
doc,
|
||||||
|
getDocs,
|
||||||
|
query,
|
||||||
|
updateDoc,
|
||||||
|
where,
|
||||||
|
} from 'firebase/firestore'
|
||||||
import { Dashboard } from 'common/dashboard'
|
import { Dashboard } from 'common/dashboard'
|
||||||
import { coll, getValue } from './utils'
|
import { coll, getValue } from './utils'
|
||||||
|
|
||||||
export const dashboards = coll<Dashboard>('dashboards')
|
export const dashboards = coll<Dashboard>('dashboards')
|
||||||
|
|
||||||
export function dashboardPath(dashboardSlug: string) {
|
export function dashboardPath(dashboardSlug: string) {
|
||||||
return `/dashboard/${dashboardSlug}}`
|
return `/dashboard/${dashboardSlug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDashboard(
|
export function updateDashboard(
|
||||||
|
@ -22,3 +29,10 @@ export function deleteDashboard(dashboard: Dashboard) {
|
||||||
export function getDashboard(dashboardId: string) {
|
export function getDashboard(dashboardId: string) {
|
||||||
return getValue<Dashboard>(doc(dashboards, dashboardId))
|
return getValue<Dashboard>(doc(dashboards, dashboardId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDashboardBySlug(slug: string) {
|
||||||
|
const q = query(dashboards, where('slug', '==', slug))
|
||||||
|
const docs = (await getDocs(q)).docs
|
||||||
|
console.log(docs.length === 0 ? null : docs[0].data())
|
||||||
|
return docs.length === 0 ? null : docs[0].data()
|
||||||
|
}
|
||||||
|
|
94
web/pages/create-dashboard.tsx
Normal file
94
web/pages/create-dashboard.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
|
import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
|
import { createDashboard } from 'web/lib/firebase/api'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Dashboard } from 'common/dashboard'
|
||||||
|
import { dashboardPath } from 'web/lib/firebase/dashboards'
|
||||||
|
|
||||||
|
export default function CreateDashboard() {
|
||||||
|
useTracking('view create dashboards page')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { editor, upload } = useTextEditor({
|
||||||
|
max: 1000,
|
||||||
|
defaultValue: '',
|
||||||
|
disabled: isSubmitting,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function saveDashboard(name: string) {
|
||||||
|
if (!editor) return
|
||||||
|
const newDashboard = {
|
||||||
|
name: name,
|
||||||
|
content: editor.getJSON(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createDashboard(newDashboard).catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
console.log(result.dashboard as Dashboard)
|
||||||
|
await router.push(dashboardPath((result.dashboard as Dashboard).slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="mx-auto w-full max-w-2xl">
|
||||||
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
|
<Title className="!mt-0" text="Create a dashboard" />
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Name<span className={'text-red-700'}>*</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. Elon Mania Dashboard"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
|
maxLength={100}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
<Spacer h={6} />
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Content<span className={'text-red-700'}>*</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor editor={editor} upload={upload} />
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
'btn btn-primary normal-case',
|
||||||
|
isSubmitting && 'loading disabled'
|
||||||
|
)}
|
||||||
|
disabled={isSubmitting || upload.isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
await saveDashboard(name)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Creating...' : 'Create a dashboard'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<Spacer h={6} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
107
web/pages/dashboard/[...slugs]/index.tsx
Normal file
107
web/pages/dashboard/[...slugs]/index.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
|
||||||
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
|
import { dashboardPath, getDashboardBySlug } from 'web/lib/firebase/dashboards'
|
||||||
|
import { Dashboard } from 'common/dashboard'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
|
import { Content } from 'web/components/editor'
|
||||||
|
import { UserLink } from 'web/components/user-page'
|
||||||
|
import { getUser, User } from 'web/lib/firebase/users'
|
||||||
|
import { ShareIcon } from '@heroicons/react/solid'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ShareDashboardModal } from 'web/components/share-dashboard-modal'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
const { slugs } = props.params
|
||||||
|
|
||||||
|
const dashboard = await getDashboardBySlug(slugs[0])
|
||||||
|
const creatorPromise = dashboard ? getUser(dashboard.creatorId) : null
|
||||||
|
const creator = await creatorPromise
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
dashboard: dashboard,
|
||||||
|
creator: creator,
|
||||||
|
},
|
||||||
|
|
||||||
|
revalidate: 60, // regenerate after a minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
return { paths: [], fallback: 'blocking' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardPage(props: {
|
||||||
|
dashboard: Dashboard
|
||||||
|
creator: User
|
||||||
|
}) {
|
||||||
|
props = usePropz(props, getStaticPropz) ?? {
|
||||||
|
dashboard: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareUrl = `https://${ENV_CONFIG.domain}${dashboardPath(
|
||||||
|
props?.dashboard.slug
|
||||||
|
)}`
|
||||||
|
const [isShareOpen, setShareOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="mx-auto w-full max-w-3xl ">
|
||||||
|
<Spacer h={1} />
|
||||||
|
<Title className="!mt-0" text={props.dashboard?.name ?? ''} />
|
||||||
|
<Row>
|
||||||
|
<Col className=" flex-1">
|
||||||
|
<div className={'items-right inline-flex'}>
|
||||||
|
<div className="mr-1 text-gray-500">Created by</div>
|
||||||
|
<UserLink
|
||||||
|
className="text-neutral"
|
||||||
|
name={props.creator.name}
|
||||||
|
username={props.creator.username}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
color="gray-white"
|
||||||
|
className={'flex'}
|
||||||
|
onClick={() => {
|
||||||
|
setShareOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShareIcon
|
||||||
|
className={clsx('mr-2 h-[24px] w-5')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Share
|
||||||
|
<ShareDashboardModal
|
||||||
|
isOpen={isShareOpen}
|
||||||
|
setOpen={setShareOpen}
|
||||||
|
shareUrl={shareUrl}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Spacer h={1} />
|
||||||
|
|
||||||
|
<Spacer h={1} />
|
||||||
|
<div className="rounded-lg bg-white px-6 py-4 sm:py-0">
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<Spacer h={6} />
|
||||||
|
<Content content={props.dashboard?.content ?? ''} />
|
||||||
|
<Spacer h={6} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user