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'
export type Dashboard = {
export type Post = {
id: string
name: string
content: JSONContent
@ -9,4 +9,4 @@ export type Dashboard = {
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 update: if request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data)

View File

@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
import { getUser } from './utils'
import { slugify } from '../../common/util/slugify'
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 { JSONContent } from '@tiptap/core'
import { z } from 'zod'
@ -31,27 +31,27 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
)
)
const dashboardSchema = z.object({
name: z.string().min(1).max(MAX_DASHBOARD_NAME_LENGTH),
const postSchema = z.object({
name: z.string().min(1).max(MAX_POST_NAME_LENGTH),
content: contentSchema,
})
export const createdashboard = newEndpoint({}, async (req, auth) => {
export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { name, content } = validate(dashboardSchema, req.body)
const { name, content } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
if (!creator)
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 dashboardRef = firestore.collection('dashboards').doc()
const postRef = firestore.collection('posts').doc()
const dashboard: Dashboard = {
id: dashboardRef.id,
const post: Post = {
id: postRef.id,
creatorId: creator.id,
slug,
name,
@ -59,27 +59,25 @@ export const createdashboard = newEndpoint({}, async (req, auth) => {
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) => {
const proposedSlug = slugify(name)
const preexistingDashboard = await getDashboardFromSlug(proposedSlug)
const preexistingPost = await getPostFromSlug(proposedSlug)
return preexistingDashboard
? proposedSlug + '-' + randomString()
: proposedSlug
return preexistingPost ? proposedSlug + '-' + randomString() : proposedSlug
}
export async function getDashboardFromSlug(slug: string) {
export async function getPostFromSlug(slug: string) {
const firestore = admin.firestore()
const snap = await firestore
.collection('dashboards')
.collection('posts')
.where('slug', '==', slug)
.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 { acceptchallenge } from './accept-challenge'
import { getcustomtoken } from './get-custom-token'
import { createdashboard } from './create-dashboard'
import { createpost } from './create-post'
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
return onRequest(opts, handler as any)
@ -97,7 +97,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
const acceptChallenge = toCloudFunction(acceptchallenge)
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
const createDashboardFunction = toCloudFunction(createdashboard)
const createPostFunction = toCloudFunction(createpost)
export {
healthFunction as health,
@ -121,5 +121,5 @@ export {
getCurrentUserFunction as getcurrentuser,
acceptChallenge as acceptchallenge,
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 { getcurrentuser } from './get-current-user'
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
const app = express()
@ -68,7 +68,7 @@ addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
addEndpointRoute('/getcustomtoken', getcustomtoken)
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
addEndpointRoute('/createdashboard', createdashboard)
addEndpointRoute('/createpost', createpost)
app.listen(PORT)
console.log(`Serving functions on port ${PORT}.`)

View File

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

View File

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

View File

@ -89,6 +89,6 @@ export function getCurrentUser(params: any) {
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
}
export function createDashboard(params: any) {
return call(getFunctionUrl('createdashboard'), 'POST', params)
export function createPost(params: any) {
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 { 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 { useRouter } from 'next/router'
import { Dashboard, MAX_DASHBOARD_NAME_LENGTH } from 'common/dashboard'
import { dashboardPath } from 'web/lib/firebase/dashboards'
import { Post, MAX_POST_NAME_LENGTH } from 'common/post'
import { postPath } from 'web/lib/firebase/posts'
export default function CreateDashboard() {
export default function CreatePost() {
const [name, setName] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
@ -23,20 +23,20 @@ export default function CreateDashboard() {
const isValid = editor && name.length > 0 && editor.isEmpty === false
async function saveDashboard(name: string) {
async function savePost(name: string) {
if (!editor) return
const newDashboard = {
const newPost = {
name: name,
content: editor.getJSON(),
}
const result = await createDashboard(newDashboard).catch((e) => {
const result = await createPost(newPost).catch((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
})
if (result.dashboard) {
await router.push(dashboardPath((result.dashboard as Dashboard).slug))
if (result.post) {
await router.push(postPath((result.post as Post).slug))
}
}
@ -44,7 +44,7 @@ export default function CreateDashboard() {
<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" />
<Title className="!mt-0" text="Create a post" />
<form>
<div className="form-control w-full">
<label className="label">
@ -53,10 +53,10 @@ export default function CreateDashboard() {
</span>
</label>
<Textarea
placeholder="e.g. Elon Mania Dashboard"
placeholder="e.g. Elon Mania Post"
className="input input-bordered resize-none"
autoFocus
maxLength={MAX_DASHBOARD_NAME_LENGTH}
maxLength={MAX_POST_NAME_LENGTH}
value={name}
onChange={(e) => setName(e.target.value || '')}
/>
@ -78,11 +78,11 @@ export default function CreateDashboard() {
disabled={isSubmitting || !isValid || upload.isLoading}
onClick={async () => {
setIsSubmitting(true)
await saveDashboard(name)
await savePost(name)
setIsSubmitting(false)
}}
>
{isSubmitting ? 'Creating...' : 'Create a dashboard'}
{isSubmitting ? 'Creating...' : 'Create a post'}
</button>
{error !== '' && <div className="text-red-700">{error}</div>}
</div>

View File

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