diff --git a/apps/web/components/layout/header.component.tsx b/apps/web/components/layout/header.component.tsx index 69b63fa..8660ada 100644 --- a/apps/web/components/layout/header.component.tsx +++ b/apps/web/components/layout/header.component.tsx @@ -27,6 +27,7 @@ export default function HeaderComponent() { if (billingDetails?.has_active_subscription) { return [ { name: "Pages", href: ROUTES.PAGES }, + { name: "Zapier", href: ROUTES.ZAPIER }, { name: "Billing", href: ROUTES.BILLING }, { name: "Support", href: ROUTES.SUPPORT, external: true }, ]; @@ -41,7 +42,7 @@ export default function HeaderComponent() { return [ { name: "Pricing", href: ROUTES.PRICING }, - { name: "Zapier Integration", href: ROUTES.ZAPIER }, + { name: "Automate using Zapier", href: ROUTES.ZAPIER }, { name: "Knowledge base", href: ROUTES.DOCS, external: true }, { name: "Support", href: ROUTES.SUPPORT, external: true }, ]; diff --git a/apps/web/components/marketing/features.tsx b/apps/web/components/marketing/features.tsx index e5652b8..a73fadb 100644 --- a/apps/web/components/marketing/features.tsx +++ b/apps/web/components/marketing/features.tsx @@ -9,6 +9,7 @@ import { MailIcon, StarIcon, } from "@heroicons/react/solid"; +import Head from "next/head"; import Image from "next/image"; import { useMemo } from "react"; import appScreenshot from "../../public/images/hero/app-screenshot.png"; @@ -76,6 +77,12 @@ export default function Features() { return (
+ + +

@@ -124,6 +131,16 @@ export default function Features() { ))}

+ +
+ +
); } diff --git a/apps/web/inngest/email/send-welcome-email.ts b/apps/web/inngest/email/send-welcome-email.ts new file mode 100644 index 0000000..9ce40c1 --- /dev/null +++ b/apps/web/inngest/email/send-welcome-email.ts @@ -0,0 +1,25 @@ +import inngestClient from "../../utils/inngest"; +import postmarkClient from "../../utils/postmark"; + +export const sendWelcomeEmail = inngestClient.createFunction( + { name: "Email: Welcome" }, + { event: "email/user.welcome" }, + async ({ event }) => { + const { email, payload } = event.data; + + console.log("Job started", { + email, + payload, + }); + + const result = await postmarkClient.sendEmailWithTemplate({ + MessageStream: "outbound", + From: "hello@changes.page", + To: email, + TemplateAlias: "welcome-user", + TemplateModel: payload, + }); + + return { body: "Job completed", result }; + } +); diff --git a/apps/web/jsx-types.d.ts b/apps/web/jsx-types.d.ts new file mode 100644 index 0000000..9a6144d --- /dev/null +++ b/apps/web/jsx-types.d.ts @@ -0,0 +1,5 @@ +declare namespace JSX { + interface IntrinsicElements { + "zapier-workflow": any; + } +} diff --git a/apps/web/next.config.js b/apps/web/next.config.js index bdfd0aa..adbb4bc 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -2,10 +2,10 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({}); const ContentSecurityPolicy = ` script-src 'self' 'unsafe-eval' 'unsafe-inline' *; - style-src 'self' data: 'unsafe-inline' maxcdn.bootstrapcdn.com cdn.jsdelivr.net; + style-src 'self' data: 'unsafe-inline' maxcdn.bootstrapcdn.com cdn.jsdelivr.net cdn.zapier.com fonts.googleapis.com; img-src 'self' * data: blob:; - font-src 'self' data: maxcdn.bootstrapcdn.com cdn.jsdelivr.net; - connect-src 'self' wss: *.supabase.co *.changes.page manageprompt.com; + font-src 'self' data: maxcdn.bootstrapcdn.com cdn.jsdelivr.net fonts.gstatic.com; + connect-src 'self' wss: *.supabase.co *.changes.page manageprompt.com zapier.com *.zapier.com www.google.com; worker-src 'self' blob:; report-to default `; diff --git a/apps/web/pages/_document.js b/apps/web/pages/_document.js index 750bd81..835eacc 100644 --- a/apps/web/pages/_document.js +++ b/apps/web/pages/_document.js @@ -61,6 +61,10 @@ export default class MyDocument extends Document { media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> +
diff --git a/apps/web/pages/api/inngest.ts b/apps/web/pages/api/inngest.ts index 70bdf87..a7b92e7 100644 --- a/apps/web/pages/api/inngest.ts +++ b/apps/web/pages/api/inngest.ts @@ -2,6 +2,7 @@ import { serve } from "inngest/next"; import { handleSubscriptionChange } from "../../inngest/billing/handle-subscription"; import { reportUsageForStripeInvoice } from "../../inngest/billing/report-pages-usage-invoice"; import { sendConfirmEmailNotification } from "../../inngest/email/send-confirm-email-notification"; +import { sendWelcomeEmail } from "../../inngest/email/send-welcome-email"; import { deleteImagesJob } from "../../inngest/jobs/delete-images"; import { sendPostNotification } from "./../../inngest/email/send-post-notification"; @@ -13,6 +14,7 @@ export default serve("changes-page", [ // Emails sendConfirmEmailNotification, sendPostNotification, + sendWelcomeEmail, // Background Jobs deleteImagesJob, ]); diff --git a/apps/web/pages/api/users/webhook.ts b/apps/web/pages/api/users/webhook.ts new file mode 100644 index 0000000..e5078eb --- /dev/null +++ b/apps/web/pages/api/users/webhook.ts @@ -0,0 +1,57 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import inngestClient from "../../../utils/inngest"; + +const databaseWebhook = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === "POST") { + try { + if (req?.headers["x-webhook-key"] !== process.env.SUPABASE_WEBHOOK_KEY) { + return res + .status(400) + .json({ error: { statusCode: 500, message: "Invalid webhook key" } }); + } + + const { type, record, old_record } = req.body; + const user: { + id: string; + email?: string; + raw_user_meta_data?: { + name?: string; + full_name?: string; + }; + } = record || old_record; + + const { id, email } = user; + console.log("Trigger databaseWebhook [Users]: Record:", type, id); + + if (type === "INSERT") { + await inngestClient.send({ + name: "email/user.welcome", + data: { + email, + payload: { + first_name: + user.raw_user_meta_data?.full_name ?? + user.raw_user_meta_data?.name ?? + "there", + }, + }, + user: { + id, + }, + }); + } + + return res.status(200).json({ ok: true }); + } catch (err) { + console.log("Trigger databaseWebhook [Users]: Error:", err); + res + .status(500) + .json({ error: { statusCode: 500, message: err.message } }); + } + } else { + res.setHeader("Allow", "POST,PUT"); + res.status(405).end("Method Not Allowed"); + } +}; + +export default databaseWebhook; diff --git a/apps/web/pages/integrations/zapier.tsx b/apps/web/pages/integrations/zapier.tsx index 1588873..6bb94f6 100644 --- a/apps/web/pages/integrations/zapier.tsx +++ b/apps/web/pages/integrations/zapier.tsx @@ -1,128 +1,41 @@ -import { InboxIcon, SparklesIcon } from "@heroicons/react/outline"; -import Image from "next/image"; +import { GetServerSidePropsContext } from "next"; +import Head from "next/head"; import FooterComponent from "../../components/layout/footer.component"; import HeaderComponent from "../../components/layout/header.component"; import Page from "../../components/layout/page.component"; -import zapierGitHub from "../../public/images/zapier/github.png"; -import zapierTweet from "../../public/images/zapier/tweet.png"; +import { getSupabaseServerClient } from "../../utils/supabase/supabase-admin"; -export default function Example() { +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { user } = await getSupabaseServerClient(ctx); + + return { + props: { email: user?.email }, + }; +} + +export default function Zapier({ email }: { email?: string }) { return (
+ + + - -
+ +
-
-
-

- Connect with Zapier and{" "} - - automate your workflow - - , here are some examples: -

-
-
- -
-
-
-
- - -
-
-

- Tweet when you create a post -

-

- Share your new posts on Twitter so that customers are in - the loop with latest changes in your product. -

- -
-
-
-
-
- Post to Tweet -
-
-
-
-
-
-
-
- - -
-
-

- Sync GitHub releases -

-

- Automatically create a new post in your changes page every - time you publish a new release in GitHub. -

- -
-
-
-
- Copy releases from GitHub -
-
-
-
- -
-
-

- Zapier lets you connect changes.page with thousands of the most - popular apps, so you can automate your work and have more time - for what matters most—no code required. -

+
+
diff --git a/apps/web/utils/useDatabase.ts b/apps/web/utils/useDatabase.ts index 61fc68d..705f1d1 100644 --- a/apps/web/utils/useDatabase.ts +++ b/apps/web/utils/useDatabase.ts @@ -20,9 +20,13 @@ export const getUserById = async (user_id: string): Promise => { return { ...user, - has_active_subscription: user.pro_gifted ? true : ["trialing", "active"].includes( - (user?.stripe_subscription as unknown as Stripe.Subscription)?.status - ), + has_active_subscription: + user.pro_gifted === true + ? true + : ["trialing", "active"].includes( + (user?.stripe_subscription as unknown as Stripe.Subscription) + ?.status + ), } as unknown as IUser; };