Renamed Dashboards to Posts

This commit is contained in:
Pico2x 2022-08-29 15:46:36 +01:00
parent 9c3b3920ef
commit 2aff501c66
12 changed files with 94 additions and 104 deletions

View File

@ -1,6 +1,6 @@
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
export type Dashboard = { export type Post = {
id: string id: string
name: string name: string
content: JSONContent content: JSONContent
@ -9,4 +9,4 @@ export type Dashboard = {
slug: string slug: string
} }
export const MAX_DASHBOARD_NAME_LENGTH = 480 export const MAX_POST_NAME_LENGTH = 480

View File

@ -170,7 +170,7 @@ service cloud.firestore {
} }
} }
match /dashboards/{dashboardId} { match /posts/{postId} {
allow read; allow read;
allow update: if request.auth.uid == resource.data.creatorId allow update: if request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data) && request.resource.data.diff(resource.data)

View File

@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
import { getUser } from './utils' import { getUser } from './utils'
import { slugify } from '../../common/util/slugify' import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { Dashboard, MAX_DASHBOARD_NAME_LENGTH } from '../../common/dashboard' import { Post, MAX_POST_NAME_LENGTH } from '../../common/post'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { z } from 'zod' import { z } from 'zod'
@ -31,27 +31,27 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
) )
) )
const dashboardSchema = z.object({ const postSchema = z.object({
name: z.string().min(1).max(MAX_DASHBOARD_NAME_LENGTH), name: z.string().min(1).max(MAX_POST_NAME_LENGTH),
content: contentSchema, content: contentSchema,
}) })
export const createdashboard = newEndpoint({}, async (req, auth) => { export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore() const firestore = admin.firestore()
const { name, content } = validate(dashboardSchema, req.body) const { name, content } = validate(postSchema, req.body)
const creator = await getUser(auth.uid) const creator = await getUser(auth.uid)
if (!creator) if (!creator)
throw new APIError(400, 'No user exists with the authenticated user ID.') throw new APIError(400, 'No user exists with the authenticated user ID.')
console.log('creating dashboard owned by', creator.username, 'named', name) console.log('creating post owned by', creator.username, 'named', name)
const slug = await getSlug(name) const slug = await getSlug(name)
const dashboardRef = firestore.collection('dashboards').doc() const postRef = firestore.collection('posts').doc()
const dashboard: Dashboard = { const post: Post = {
id: dashboardRef.id, id: postRef.id,
creatorId: creator.id, creatorId: creator.id,
slug, slug,
name, name,
@ -59,27 +59,25 @@ export const createdashboard = newEndpoint({}, async (req, auth) => {
content: content, content: content,
} }
await dashboardRef.create(dashboard) await postRef.create(post)
return { status: 'success', dashboard: dashboard } return { status: 'success', post: post }
}) })
export const getSlug = async (name: string) => { export const getSlug = async (name: string) => {
const proposedSlug = slugify(name) const proposedSlug = slugify(name)
const preexistingDashboard = await getDashboardFromSlug(proposedSlug) const preexistingPost = await getPostFromSlug(proposedSlug)
return preexistingDashboard return preexistingPost ? proposedSlug + '-' + randomString() : proposedSlug
? proposedSlug + '-' + randomString()
: proposedSlug
} }
export async function getDashboardFromSlug(slug: string) { export async function getPostFromSlug(slug: string) {
const firestore = admin.firestore() const firestore = admin.firestore()
const snap = await firestore const snap = await firestore
.collection('dashboards') .collection('posts')
.where('slug', '==', slug) .where('slug', '==', slug)
.get() .get()
return snap.empty ? undefined : (snap.docs[0].data() as Dashboard) return snap.empty ? undefined : (snap.docs[0].data() as Post)
} }

View File

@ -71,7 +71,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
import { getcurrentuser } from './get-current-user' import { getcurrentuser } from './get-current-user'
import { acceptchallenge } from './accept-challenge' import { acceptchallenge } from './accept-challenge'
import { getcustomtoken } from './get-custom-token' import { getcustomtoken } from './get-custom-token'
import { createdashboard } from './create-dashboard' import { createpost } from './create-post'
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
return onRequest(opts, handler as any) return onRequest(opts, handler as any)
@ -97,7 +97,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
const getCurrentUserFunction = toCloudFunction(getcurrentuser) const getCurrentUserFunction = toCloudFunction(getcurrentuser)
const acceptChallenge = toCloudFunction(acceptchallenge) const acceptChallenge = toCloudFunction(acceptchallenge)
const getCustomTokenFunction = toCloudFunction(getcustomtoken) const getCustomTokenFunction = toCloudFunction(getcustomtoken)
const createDashboardFunction = toCloudFunction(createdashboard) const createPostFunction = toCloudFunction(createpost)
export { export {
healthFunction as health, healthFunction as health,
@ -121,5 +121,5 @@ export {
getCurrentUserFunction as getcurrentuser, getCurrentUserFunction as getcurrentuser,
acceptChallenge as acceptchallenge, acceptChallenge as acceptchallenge,
getCustomTokenFunction as getcustomtoken, getCustomTokenFunction as getcustomtoken,
createDashboardFunction as createdashboard, createPostFunction as createpost,
} }

View File

@ -27,7 +27,7 @@ import { unsubscribe } from './unsubscribe'
import { stripewebhook, createcheckoutsession } from './stripe' import { stripewebhook, createcheckoutsession } from './stripe'
import { getcurrentuser } from './get-current-user' import { getcurrentuser } from './get-current-user'
import { getcustomtoken } from './get-custom-token' import { getcustomtoken } from './get-custom-token'
import { createdashboard } from './create-dashboard' import { createpost } from './create-post'
type Middleware = (req: Request, res: Response, next: NextFunction) => void type Middleware = (req: Request, res: Response, next: NextFunction) => void
const app = express() const app = express()
@ -68,7 +68,7 @@ addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
addJsonEndpointRoute('/getcurrentuser', getcurrentuser) addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
addEndpointRoute('/getcustomtoken', getcustomtoken) addEndpointRoute('/getcustomtoken', getcustomtoken)
addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
addEndpointRoute('/createdashboard', createdashboard) addEndpointRoute('/createpost', createpost)
app.listen(PORT) app.listen(PORT)
console.log(`Serving functions on port ${PORT}.`) console.log(`Serving functions on port ${PORT}.`)

View File

@ -4,7 +4,7 @@ import { chunk } from 'lodash'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user' import { PrivateUser, User } from '../../common/user'
import { Group } from '../../common/group' import { Group } from '../../common/group'
import { Dashboard } from 'common/dashboard' import { Post } from 'common/post'
export const log = (...args: unknown[]) => { export const log = (...args: unknown[]) => {
console.log(`[${new Date().toISOString()}]`, ...args) console.log(`[${new Date().toISOString()}]`, ...args)
@ -81,8 +81,8 @@ export const getGroup = (groupId: string) => {
return getDoc<Group>('groups', groupId) return getDoc<Group>('groups', groupId)
} }
export const getDashboard = (dashboardId: string) => { export const getPost = (postId: string) => {
return getDoc<Dashboard>('dashboards', dashboardId) return getDoc<Post>('posts', postId)
} }
export const getUser = (userId: string) => { export const getUser = (userId: string) => {

View File

@ -9,7 +9,7 @@ import { Button } from './button'
import { TweetButton } from './tweet-button' import { TweetButton } from './tweet-button'
import { Row } from './layout/row' import { Row } from './layout/row'
export function ShareDashboardModal(props: { export function SharePostModal(props: {
shareUrl: string shareUrl: string
isOpen: boolean isOpen: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
@ -21,7 +21,7 @@ export function ShareDashboardModal(props: {
return ( return (
<Modal open={isOpen} setOpen={setOpen} size="md"> <Modal open={isOpen} setOpen={setOpen} size="md">
<Col className="gap-4 rounded bg-white p-4"> <Col className="gap-4 rounded bg-white p-4">
<Title className="!mt-0 !mb-2" text="Share this dashboard" /> <Title className="!mt-0 !mb-2" text="Share this post" />
<Button <Button
size="2xl" size="2xl"
color="gradient" color="gradient"
@ -31,7 +31,7 @@ export function ShareDashboardModal(props: {
toast.success('Link copied!', { toast.success('Link copied!', {
icon: linkIcon, icon: linkIcon,
}) })
track('copy share dashboard link') track('copy share post link')
}} }}
> >
{linkIcon} Copy link {linkIcon} Copy link

View File

@ -89,6 +89,6 @@ export function getCurrentUser(params: any) {
return call(getFunctionUrl('getcurrentuser'), 'GET', params) return call(getFunctionUrl('getcurrentuser'), 'GET', params)
} }
export function createDashboard(params: any) { export function createPost(params: any) {
return call(getFunctionUrl('createdashboard'), 'POST', params) return call(getFunctionUrl('createpost'), 'POST', params)
} }

View File

@ -1,37 +0,0 @@
import {
deleteDoc,
doc,
getDocs,
query,
updateDoc,
where,
} from 'firebase/firestore'
import { Dashboard } from 'common/dashboard'
import { coll, getValue } from './utils'
export const dashboards = coll<Dashboard>('dashboards')
export function dashboardPath(dashboardSlug: string) {
return `/dashboard/${dashboardSlug}`
}
export function updateDashboard(
dashboard: Dashboard,
updates: Partial<Dashboard>
) {
return updateDoc(doc(dashboards, dashboard.id), updates)
}
export function deleteDashboard(dashboard: Dashboard) {
return deleteDoc(doc(dashboards, dashboard.id))
}
export function getDashboard(dashboardId: string) {
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
return docs.length === 0 ? null : docs[0].data()
}

34
web/lib/firebase/posts.ts Normal file
View File

@ -0,0 +1,34 @@
import {
deleteDoc,
doc,
getDocs,
query,
updateDoc,
where,
} from 'firebase/firestore'
import { Post } from 'common/post'
import { coll, getValue } from './utils'
export const posts = coll<Post>('posts')
export function postPath(postSlug: string) {
return `/post/${postSlug}`
}
export function updatePost(post: Post, updates: Partial<Post>) {
return updateDoc(doc(posts, post.id), updates)
}
export function deletePost(post: Post) {
return deleteDoc(doc(posts, post.id))
}
export function getPost(postId: string) {
return getValue<Post>(doc(posts, postId))
}
export async function getPostBySlug(slug: string) {
const q = query(posts, where('slug', '==', slug))
const docs = (await getDocs(q)).docs
return docs.length === 0 ? null : docs[0].data()
}

View File

@ -5,13 +5,13 @@ import { Title } from 'web/components/title'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
import { TextEditor, useTextEditor } from 'web/components/editor' import { TextEditor, useTextEditor } from 'web/components/editor'
import { createDashboard } from 'web/lib/firebase/api' import { createPost } from 'web/lib/firebase/api'
import clsx from 'clsx' import clsx from 'clsx'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Dashboard, MAX_DASHBOARD_NAME_LENGTH } from 'common/dashboard' import { Post, MAX_POST_NAME_LENGTH } from 'common/post'
import { dashboardPath } from 'web/lib/firebase/dashboards' import { postPath } from 'web/lib/firebase/posts'
export default function CreateDashboard() { export default function CreatePost() {
const [name, setName] = useState('') const [name, setName] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -23,20 +23,20 @@ export default function CreateDashboard() {
const isValid = editor && name.length > 0 && editor.isEmpty === false const isValid = editor && name.length > 0 && editor.isEmpty === false
async function saveDashboard(name: string) { async function savePost(name: string) {
if (!editor) return if (!editor) return
const newDashboard = { const newPost = {
name: name, name: name,
content: editor.getJSON(), content: editor.getJSON(),
} }
const result = await createDashboard(newDashboard).catch((e) => { const result = await createPost(newPost).catch((e) => {
console.log(e) console.log(e)
setError('There was an error creating the dashboard, please try again') setError('There was an error creating the post, please try again')
return e return e
}) })
if (result.dashboard) { if (result.post) {
await router.push(dashboardPath((result.dashboard as Dashboard).slug)) await router.push(postPath((result.post as Post).slug))
} }
} }
@ -44,7 +44,7 @@ export default function CreateDashboard() {
<Page> <Page>
<div className="mx-auto w-full max-w-2xl"> <div className="mx-auto w-full max-w-2xl">
<div className="rounded-lg px-6 py-4 sm:py-0"> <div className="rounded-lg px-6 py-4 sm:py-0">
<Title className="!mt-0" text="Create a dashboard" /> <Title className="!mt-0" text="Create a post" />
<form> <form>
<div className="form-control w-full"> <div className="form-control w-full">
<label className="label"> <label className="label">
@ -53,10 +53,10 @@ export default function CreateDashboard() {
</span> </span>
</label> </label>
<Textarea <Textarea
placeholder="e.g. Elon Mania Dashboard" placeholder="e.g. Elon Mania Post"
className="input input-bordered resize-none" className="input input-bordered resize-none"
autoFocus autoFocus
maxLength={MAX_DASHBOARD_NAME_LENGTH} maxLength={MAX_POST_NAME_LENGTH}
value={name} value={name}
onChange={(e) => setName(e.target.value || '')} onChange={(e) => setName(e.target.value || '')}
/> />
@ -78,11 +78,11 @@ export default function CreateDashboard() {
disabled={isSubmitting || !isValid || upload.isLoading} disabled={isSubmitting || !isValid || upload.isLoading}
onClick={async () => { onClick={async () => {
setIsSubmitting(true) setIsSubmitting(true)
await saveDashboard(name) await savePost(name)
setIsSubmitting(false) setIsSubmitting(false)
}} }}
> >
{isSubmitting ? 'Creating...' : 'Create a dashboard'} {isSubmitting ? 'Creating...' : 'Create a post'}
</button> </button>
{error !== '' && <div className="text-red-700">{error}</div>} {error !== '' && <div className="text-red-700">{error}</div>}
</div> </div>

View File

@ -1,8 +1,8 @@
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { fromPropz, usePropz } from 'web/hooks/use-propz' import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { dashboardPath, getDashboardBySlug } from 'web/lib/firebase/dashboards' import { postPath, getPostBySlug } from 'web/lib/firebase/posts'
import { Dashboard } from 'common/dashboard' import { Post } from 'common/post'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { Spacer } from 'web/components/layout/spacer' import { Spacer } from 'web/components/layout/spacer'
import { Content } from 'web/components/editor' import { Content } from 'web/components/editor'
@ -12,7 +12,7 @@ import { ShareIcon } from '@heroicons/react/solid'
import clsx from 'clsx' import clsx from 'clsx'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { useState } from 'react' import { useState } from 'react'
import { ShareDashboardModal } from 'web/components/share-dashboard-modal' import { SharePostModal } from 'web/components/share-post-modal'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { ENV_CONFIG } from 'common/envs/constants' import { ENV_CONFIG } from 'common/envs/constants'
@ -22,13 +22,13 @@ export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) { export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
const dashboard = await getDashboardBySlug(slugs[0]) const post = await getPostBySlug(slugs[0])
const creatorPromise = dashboard ? getUser(dashboard.creatorId) : null const creatorPromise = post ? getUser(post.creatorId) : null
const creator = await creatorPromise const creator = await creatorPromise
return { return {
props: { props: {
dashboard: dashboard, post: post,
creator: creator, creator: creator,
}, },
@ -40,28 +40,23 @@ export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' } return { paths: [], fallback: 'blocking' }
} }
export default function DashboardPage(props: { export default function PostPage(props: { post: Post; creator: User }) {
dashboard: Dashboard
creator: User
}) {
props = usePropz(props, getStaticPropz) ?? { props = usePropz(props, getStaticPropz) ?? {
dashboard: null, post: null,
} }
const [isShareOpen, setShareOpen] = useState(false) const [isShareOpen, setShareOpen] = useState(false)
if (props.dashboard === null) { if (props.post === null) {
return <Custom404 /> return <Custom404 />
} }
const shareUrl = `https://${ENV_CONFIG.domain}${dashboardPath( const shareUrl = `https://${ENV_CONFIG.domain}${postPath(props?.post.slug)}`
props?.dashboard.slug
)}`
return ( return (
<Page> <Page>
<div className="mx-auto w-full max-w-3xl "> <div className="mx-auto w-full max-w-3xl ">
<Spacer h={1} /> <Spacer h={1} />
<Title className="!mt-0" text={props.dashboard.name} /> <Title className="!mt-0" text={props.post.name} />
<Row> <Row>
<Col className="flex-1"> <Col className="flex-1">
<div className={'inline-flex'}> <div className={'inline-flex'}>
@ -87,7 +82,7 @@ export default function DashboardPage(props: {
aria-hidden="true" aria-hidden="true"
/> />
Share Share
<ShareDashboardModal <SharePostModal
isOpen={isShareOpen} isOpen={isShareOpen}
setOpen={setShareOpen} setOpen={setShareOpen}
shareUrl={shareUrl} shareUrl={shareUrl}
@ -99,7 +94,7 @@ export default function DashboardPage(props: {
<Spacer h={2} /> <Spacer h={2} />
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <div className="rounded-lg bg-white px-6 py-4 sm:py-0">
<div className="form-control w-full py-2"> <div className="form-control w-full py-2">
<Content content={props.dashboard.content} /> <Content content={props.post.content} />
</div> </div>
</div> </div>
</div> </div>