diff --git a/prisma/migrations/20220607124511_email_subject_for_post/migration.sql b/prisma/migrations/20220607124511_email_subject_for_post/migration.sql new file mode 100644 index 00000000..11764cab --- /dev/null +++ b/prisma/migrations/20220607124511_email_subject_for_post/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `subscribersNotifiedAt` on the `pages` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "PageEmailStatus" AS ENUM ('PENDING', 'RUNNING', 'SUCCESS', 'FAILED'); + +-- AlterTable +ALTER TABLE "pages" DROP COLUMN "subscribersNotifiedAt", +ADD COLUMN "emailStatus" "PageEmailStatus", +ADD COLUMN "emailSubject" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d1e68d97..e4ffdd19 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -101,26 +101,34 @@ enum PageType { PAGE } +enum PageEmailStatus { + PENDING + RUNNING + SUCCESS + FAILED +} + model Page { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - contentUpdatedAt DateTime @default(now()) - publishedAt DateTime @default(now()) - published Boolean @default(false) - type PageType @default(POST) - title String - content String - excerpt String? - format String @default("markdown") - slug String - site Site @relation(name: "SitePages", fields: [siteId], references: [id], onDelete: Cascade) - siteId String - deletedAt DateTime? - subscribersNotifiedAt DateTime? - authors User[] + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + contentUpdatedAt DateTime @default(now()) + publishedAt DateTime @default(now()) + published Boolean @default(false) + type PageType @default(POST) + title String + content String + excerpt String? + format String @default("markdown") + slug String + site Site @relation(name: "SitePages", fields: [siteId], references: [id], onDelete: Cascade) + siteId String + deletedAt DateTime? + authors User[] // prerendered markdown to html, including extra data like excerpt - rendered Json? + rendered Json? + emailSubject String? + emailStatus PageEmailStatus? @@unique([siteId, slug]) @@map("pages") diff --git a/src/components/common/EmailPostModal.tsx b/src/components/common/EmailPostModal.tsx new file mode 100644 index 00000000..6fffc3d0 --- /dev/null +++ b/src/components/common/EmailPostModal.tsx @@ -0,0 +1,57 @@ +import { PageEmailStatus } from "@prisma/client" +import { useForm } from "react-hook-form" +import toast from "react-hot-toast" +import { useStore } from "~/lib/store" +import { trpc } from "~/lib/trpc" +import { Button } from "../ui/Button" +import { Input } from "../ui/Input" +import { Modal } from "../ui/Modal" + +export const EmailPostModal: React.FC<{ + pageId: string +}> = ({ pageId }) => { + const [open, setOpen] = useStore((store) => [ + store.emailPostModalOpened, + store.setEmailPostModalOpened, + ]) + + const { handleSubmit, register } = useForm({ + defaultValues: { + subject: "", + }, + }) + const scheduleEmailForPost = trpc.useMutation("site.scheduleEmailForPost") + + const onSubmit = handleSubmit(async (values) => { + await scheduleEmailForPost.mutateAsync({ + pageId, + emailSubject: values.subject, + }) + toast.success("Scheduled!") + setOpen(false) + }) + + return ( + + { +
+
+ +
+
+ +
+
+ } +
+ ) +} diff --git a/src/components/common/LoginModal.tsx b/src/components/common/LoginModal.tsx index 3be808a8..786740ec 100644 --- a/src/components/common/LoginModal.tsx +++ b/src/components/common/LoginModal.tsx @@ -1,6 +1,5 @@ import { Button } from "~/components/ui/Button" import { useStore } from "~/lib/store" -import { Dialog } from "@headlessui/react" import { trpc } from "~/lib/trpc" import { useForm } from "react-hook-form" import { Modal } from "../ui/Modal" diff --git a/src/components/ui/Editor.tsx b/src/components/ui/Editor.tsx index a77e2ca7..1bc0c386 100644 --- a/src/components/ui/Editor.tsx +++ b/src/components/ui/Editor.tsx @@ -126,8 +126,10 @@ export const useEditor = ({ return { view, - editorRef(node: HTMLDivElement) { - setNode(node) + editorRef(newNode: HTMLDivElement) { + if (!newNode) return + if (node && newNode.isEqualNode(node)) return + setNode(newNode) }, } } diff --git a/src/components/ui/EditorToolbar.tsx b/src/components/ui/EditorToolbar.tsx index ab46b182..c9aa37a9 100644 --- a/src/components/ui/EditorToolbar.tsx +++ b/src/components/ui/EditorToolbar.tsx @@ -32,7 +32,7 @@ export const EditorToolbar: FC = ({ const active = isPreviewModeCommand && previewVisible const disabled = !isPreviewModeCommand && previewVisible return ( - + + )} + + )} -
-
- ) => { - const value = inLocalTimezone(e.target.value).toISOString() - updateValue("publishedAt", value) - }} - help={`This ${ - isPost ? "post" : "page" - } will be accesisble from this time`} - /> -
-
- ) => - updateValue("slug", e.target.value) - } - help={ - <> - {values.slug && ( - <> - This {isPost ? "post" : "page"} will be accessible at{" "} - - {getSiteLink({ subdomain, noProtocol: true })}/ - {values.slug} - - - )} - - } - /> -
-
- ) => { - updateValue("excerpt", e.target.value) - }} - help="Leave it blank to use auto-generated excerpt" - /> -
-
- - - + + + {pageId && } + ) } diff --git a/src/router/page.ts b/src/router/page.ts index 6855109e..40cbcea0 100644 --- a/src/router/page.ts +++ b/src/router/page.ts @@ -15,8 +15,9 @@ export const pageRouter = createRouter() published: z.boolean().optional(), publishedAt: z.string().optional(), excerpt: z.string().optional(), - isPost: z.boolean(), + isPost: z.boolean().optional(), slug: z.string().optional(), + emailSubject: z.string().optional(), }), output: z.object({ id: z.string(), diff --git a/src/router/site.ts b/src/router/site.ts index 9a0b6eae..afe2bebc 100644 --- a/src/router/site.ts +++ b/src/router/site.ts @@ -10,30 +10,12 @@ import { unsubscribeFromSite, } from "~/models/site.model" import { - createOrUpdatePage, getPage, - deletePage, getPagesBySite, - notifySubscribersForNewPost, + scheduleEmailForPost, } from "~/models/page.model" export const siteRouter = createRouter() - .query("subscribersNotifiedAt", { - input: z.object({ - pageId: z.string(), - }), - output: z - .date() - .transform((v) => v.toISOString()) - .nullable(), - async resolve({ input, ctx }) { - const page = await getPage(ctx.gate, { page: input.pageId }) - if (!ctx.gate.isOwnerOrAdmin(page.siteId)) { - throw ctx.gate.permissionError() - } - return page.subscribersNotifiedAt - }, - }) .query("subscription", { input: z.object({ site: z.string(), @@ -123,6 +105,9 @@ export const siteRouter = createRouter() }), ) .optional(), + emailSubject: z.string().nullish(), + emailStatus: z.string().nullish(), + siteId: z.string(), }), async resolve({ input, ctx }) { const page = await getPage(ctx.gate, input) @@ -205,13 +190,15 @@ export const siteRouter = createRouter() await unsubscribeFromSite(ctx.gate, input) }, }) - .mutation("notifySubscribersForNewPost", { + .mutation("scheduleEmailForPost", { input: z.object({ pageId: z.string(), + emailSubject: z.string().optional(), }), async resolve({ ctx, input }) { - await notifySubscribersForNewPost(ctx.gate, { + await scheduleEmailForPost(ctx.gate, { pageId: input.pageId, + emailSubject: input.emailSubject, }) }, })