From 703d551aac7647b46039a05fd06662bec0d758a9 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Thu, 19 Oct 2023 18:01:00 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20code=20for=20ge?= =?UTF-8?q?nerating=20absolute=20url=20=20(#904)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/landing/src/pages/_app.tsx | 3 ++- apps/landing/src/pages/blog/[slug].tsx | 2 +- .../landing/src/utils}/absolute-url.ts | 5 ---- apps/web/declarations/environment.d.ts | 8 ------- apps/web/src/components/invite-dialog.tsx | 2 +- apps/web/src/pages/api/stripe/checkout.ts | 3 ++- apps/web/src/pages/api/stripe/portal.ts | 3 ++- apps/web/src/pages/api/trpc/[trpc].ts | 3 +++ apps/web/src/pages/invite/[urlId].tsx | 2 +- apps/web/src/utils/absolute-url.ts | 24 +++++++++++++++++-- apps/web/src/utils/emails.ts | 6 +++++ packages/backend/next/trpc/server.ts | 7 ++++++ packages/backend/trpc/routers/polls.ts | 9 ++++--- .../backend/trpc/routers/polls/comments.ts | 5 ++-- .../trpc/routers/polls/participants.ts | 7 +++--- packages/emails/src/send-email.tsx | 16 ++++++++++--- .../templates/components/email-context.tsx | 19 +++++++++++++++ .../src/templates/components/email-layout.tsx | 7 +++--- .../components/notification-email.tsx | 5 ++-- .../components/styled-components.tsx | 6 ++--- .../emails/src/templates/components/utils.ts | 7 ------ packages/emails/src/templates/login.tsx | 5 ++-- .../new-participant-confirmation.tsx | 5 ++-- packages/emails/src/templates/new-poll.tsx | 9 ++++--- packages/utils/index.ts | 1 - 25 files changed, 108 insertions(+), 61 deletions(-) rename {packages/utils/src => apps/landing/src/utils}/absolute-url.ts (82%) create mode 100644 packages/emails/src/templates/components/email-context.tsx delete mode 100644 packages/emails/src/templates/components/utils.ts diff --git a/apps/landing/src/pages/_app.tsx b/apps/landing/src/pages/_app.tsx index 6eace7278..39929591d 100644 --- a/apps/landing/src/pages/_app.tsx +++ b/apps/landing/src/pages/_app.tsx @@ -2,7 +2,6 @@ import "tailwindcss/tailwind.css"; import "../style.css"; import { trpc, UserSession } from "@rallly/backend/next/trpc/client"; -import { absoluteUrl } from "@rallly/utils"; import { inject } from "@vercel/analytics"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; @@ -16,6 +15,8 @@ import { appWithTranslation } from "next-i18next"; import { DefaultSeo, SoftwareAppJsonLd } from "next-seo"; import React from "react"; +import { absoluteUrl } from "@/utils/absolute-url"; + import * as nextI18nNextConfig from "../../next-i18next.config.js"; import { NextPageWithLayout } from "../types"; diff --git a/apps/landing/src/pages/blog/[slug].tsx b/apps/landing/src/pages/blog/[slug].tsx index 91c7a9981..fc6db6656 100644 --- a/apps/landing/src/pages/blog/[slug].tsx +++ b/apps/landing/src/pages/blog/[slug].tsx @@ -1,5 +1,4 @@ import { ArrowLeftIcon } from "@rallly/icons"; -import { absoluteUrl } from "@rallly/utils"; import { GetStaticPropsContext } from "next"; import ErrorPage from "next/error"; import Head from "next/head"; @@ -14,6 +13,7 @@ import { getBlogLayout } from "@/components/layouts/blog-layout"; import { getAllPosts, getPostBySlug } from "@/lib/api"; import markdownToHtml from "@/lib/markdownToHtml"; import { NextPageWithLayout, Post } from "@/types"; +import { absoluteUrl } from "@/utils/absolute-url"; import { getStaticTranslations } from "@/utils/page-translations"; type Props = { diff --git a/packages/utils/src/absolute-url.ts b/apps/landing/src/utils/absolute-url.ts similarity index 82% rename from packages/utils/src/absolute-url.ts rename to apps/landing/src/utils/absolute-url.ts index 6dd353603..c0b8b0153 100644 --- a/packages/utils/src/absolute-url.ts +++ b/apps/landing/src/utils/absolute-url.ts @@ -28,8 +28,3 @@ export function absoluteUrl(subpath = "", query?: Record) { return joinPath(baseUrl, subpath) + queryString; } - -export function shortUrl(subpath = "") { - const baseUrl = process.env.NEXT_PUBLIC_SHORT_BASE_URL ?? absoluteUrl(); - return joinPath(baseUrl, subpath); -} diff --git a/apps/web/declarations/environment.d.ts b/apps/web/declarations/environment.d.ts index 02a71d140..a91ab98d9 100644 --- a/apps/web/declarations/environment.d.ts +++ b/apps/web/declarations/environment.d.ts @@ -9,10 +9,6 @@ declare global { * "development" or "production" */ NODE_ENV: "development" | "production"; - /** - * Set to "true" to take users straight to app instead of landing page - */ - DISABLE_LANDING_PAGE?: string; /** * Must be 32 characters long */ @@ -84,10 +80,6 @@ declare global { * The app version just for reference */ NEXT_PUBLIC_APP_VERSION?: string; - /** - * "true" to enable finalization of polls - */ - NEXT_PUBLIC_ENABLE_FINALIZATION?: string; } } } diff --git a/apps/web/src/components/invite-dialog.tsx b/apps/web/src/components/invite-dialog.tsx index e3eb9d3ed..cb98c2bd2 100644 --- a/apps/web/src/components/invite-dialog.tsx +++ b/apps/web/src/components/invite-dialog.tsx @@ -8,7 +8,6 @@ import { DialogTitle, DialogTrigger, } from "@rallly/ui/dialog"; -import { shortUrl } from "@rallly/utils"; import Link from "next/link"; import React from "react"; import { useCopyToClipboard } from "react-use"; @@ -16,6 +15,7 @@ import { useCopyToClipboard } from "react-use"; import { useParticipants } from "@/components/participants-provider"; import { Trans } from "@/components/trans"; import { usePoll } from "@/contexts/poll"; +import { shortUrl } from "@/utils/absolute-url"; import { isSelfHosted } from "@/utils/constants"; export const InviteDialog = () => { diff --git a/apps/web/src/pages/api/stripe/checkout.ts b/apps/web/src/pages/api/stripe/checkout.ts index 9e30c38d9..3369bf71d 100644 --- a/apps/web/src/pages/api/stripe/checkout.ts +++ b/apps/web/src/pages/api/stripe/checkout.ts @@ -1,10 +1,11 @@ import { getSession } from "@rallly/backend/next/session"; import { stripe } from "@rallly/backend/stripe"; import { prisma } from "@rallly/database"; -import { absoluteUrl } from "@rallly/utils"; import { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; +import { absoluteUrl } from "@/utils/absolute-url"; + export const config = { edge: true, }; diff --git a/apps/web/src/pages/api/stripe/portal.ts b/apps/web/src/pages/api/stripe/portal.ts index a53db38ff..c440402c0 100644 --- a/apps/web/src/pages/api/stripe/portal.ts +++ b/apps/web/src/pages/api/stripe/portal.ts @@ -1,10 +1,11 @@ import { getSession } from "@rallly/backend/next/session"; import { stripe } from "@rallly/backend/stripe"; import { prisma } from "@rallly/database"; -import { absoluteUrl } from "@rallly/utils"; import { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; +import { absoluteUrl } from "@/utils/absolute-url"; + const inputSchema = z.object({ session_id: z.string().optional(), return_path: z.string().optional(), diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts index 1d59bae6d..186df892d 100644 --- a/apps/web/src/pages/api/trpc/[trpc].ts +++ b/apps/web/src/pages/api/trpc/[trpc].ts @@ -1,6 +1,7 @@ import { trpcNextApiHandler } from "@rallly/backend/next/trpc/server"; import { NextApiRequest, NextApiResponse } from "next"; +import { absoluteUrl, shortUrl } from "@/utils/absolute-url"; import { getServerSession, isEmailBlocked } from "@/utils/auth"; import { isSelfHosted } from "@/utils/constants"; import { emailClient } from "@/utils/emails"; @@ -31,5 +32,7 @@ export default async function handler( emailClient, isSelfHosted, isEmailBlocked, + absoluteUrl, + shortUrl, })(req, res); } diff --git a/apps/web/src/pages/invite/[urlId].tsx b/apps/web/src/pages/invite/[urlId].tsx index 444bdbe1f..12cb2c87a 100644 --- a/apps/web/src/pages/invite/[urlId].tsx +++ b/apps/web/src/pages/invite/[urlId].tsx @@ -2,7 +2,6 @@ import { trpc } from "@rallly/backend"; import { prisma } from "@rallly/database"; import { ArrowUpLeftIcon } from "@rallly/icons"; import { Button } from "@rallly/ui/button"; -import { absoluteUrl } from "@rallly/utils"; import { GetStaticProps } from "next"; import Head from "next/head"; import Link from "next/link"; @@ -19,6 +18,7 @@ import { UserProvider, useUser } from "@/components/user-provider"; import { VisibilityProvider } from "@/components/visibility"; import { PermissionsContext } from "@/contexts/permissions"; import { usePoll } from "@/contexts/poll"; +import { absoluteUrl } from "@/utils/absolute-url"; import { ConnectedDayjsProvider } from "@/utils/dayjs"; import { getStaticTranslations } from "@/utils/with-page-translations"; diff --git a/apps/web/src/utils/absolute-url.ts b/apps/web/src/utils/absolute-url.ts index fba32a0a5..6dd353603 100644 --- a/apps/web/src/utils/absolute-url.ts +++ b/apps/web/src/utils/absolute-url.ts @@ -6,10 +6,30 @@ const getVercelUrl = () => { : null; }; -export function absoluteUrl(path = "") { +function joinPath(baseUrl: string, subpath = "") { + if (subpath) { + const url = new URL(subpath, baseUrl); + return url.href; + } + + return baseUrl; +} + +export function absoluteUrl(subpath = "", query?: Record) { + const queryString = query + ? `?${Object.entries(query) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join("&")}` + : ""; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? getVercelUrl() ?? `http://localhost:${port}`; - return `${baseUrl}${path}`; + + return joinPath(baseUrl, subpath) + queryString; +} + +export function shortUrl(subpath = "") { + const baseUrl = process.env.NEXT_PUBLIC_SHORT_BASE_URL ?? absoluteUrl(); + return joinPath(baseUrl, subpath); } diff --git a/apps/web/src/utils/emails.ts b/apps/web/src/utils/emails.ts index 2cbbd8e37..504043f7a 100644 --- a/apps/web/src/utils/emails.ts +++ b/apps/web/src/utils/emails.ts @@ -1,5 +1,7 @@ import { EmailClient, SupportedEmailProviders } from "@rallly/emails"; +import { absoluteUrl } from "@/utils/absolute-url"; + const env = process.env["NODE" + "_ENV"]; export const emailClient = new EmailClient({ @@ -16,4 +18,8 @@ export const emailClient = new EmailClient({ (process.env.SUPPORT_EMAIL as string), }, }, + context: { + logoUrl: absoluteUrl("/logo.png"), + baseUrl: absoluteUrl(""), + }, }); diff --git a/packages/backend/next/trpc/server.ts b/packages/backend/next/trpc/server.ts index 5a26cdb0c..ad5c52035 100644 --- a/packages/backend/next/trpc/server.ts +++ b/packages/backend/next/trpc/server.ts @@ -8,6 +8,13 @@ export interface TRPCContext { emailClient: EmailClient; isSelfHosted: boolean; isEmailBlocked?: (email: string) => boolean; + /** + * Takes a relative path and returns an absolute URL to the app + * @param path + * @returns absolute URL + */ + absoluteUrl: (path?: string) => string; + shortUrl: (path?: string) => string; } export const trpcNextApiHandler = (context: TRPCContext) => { diff --git a/packages/backend/trpc/routers/polls.ts b/packages/backend/trpc/routers/polls.ts index 9ae6646ac..885fe6946 100644 --- a/packages/backend/trpc/routers/polls.ts +++ b/packages/backend/trpc/routers/polls.ts @@ -1,5 +1,4 @@ import { prisma } from "@rallly/database"; -import { absoluteUrl, shortUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; @@ -121,9 +120,9 @@ export const polls = router({ }, }); - const pollLink = absoluteUrl(`/poll/${pollId}`); + const pollLink = ctx.absoluteUrl(`/poll/${pollId}`); - const participantLink = shortUrl(`/invite/${pollId}`); + const participantLink = ctx.shortUrl(`/invite/${pollId}`); if (ctx.user.isGuest === false) { const user = await prisma.user.findUnique({ @@ -658,7 +657,7 @@ export const polls = router({ to: poll.user.email, props: { name: poll.user.name, - pollUrl: absoluteUrl(`/poll/${poll.id}`), + pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`), location: poll.location, title: poll.title, attendees: poll.participants @@ -682,7 +681,7 @@ export const polls = router({ to: p.email, props: { name: p.name, - pollUrl: absoluteUrl(`/poll/${poll.id}`), + pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`), location: poll.location, title: poll.title, hostName: poll.user?.name ?? "", diff --git a/packages/backend/trpc/routers/polls/comments.ts b/packages/backend/trpc/routers/polls/comments.ts index c8daa3516..77d76d5bc 100644 --- a/packages/backend/trpc/routers/polls/comments.ts +++ b/packages/backend/trpc/routers/polls/comments.ts @@ -1,5 +1,4 @@ import { prisma } from "@rallly/database"; -import { absoluteUrl } from "@rallly/utils"; import { z } from "zod"; import { createToken } from "../../../session"; @@ -87,8 +86,8 @@ export const comments = router({ props: { name: watcher.user.name, authorName, - pollUrl: absoluteUrl(`/poll/${poll.id}`), - disableNotificationsUrl: absoluteUrl( + pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`), + disableNotificationsUrl: ctx.absoluteUrl( `/auth/disable-notifications?token=${token}`, ), title: poll.title, diff --git a/packages/backend/trpc/routers/polls/participants.ts b/packages/backend/trpc/routers/polls/participants.ts index 08b053b6e..555987c69 100644 --- a/packages/backend/trpc/routers/polls/participants.ts +++ b/packages/backend/trpc/routers/polls/participants.ts @@ -1,5 +1,4 @@ import { prisma } from "@rallly/database"; -import { absoluteUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -112,7 +111,7 @@ export const participants = router({ props: { name, title: poll.title, - editSubmissionUrl: absoluteUrl( + editSubmissionUrl: ctx.absoluteUrl( `/invite/${poll.id}?token=${token}`, ), }, @@ -149,8 +148,8 @@ export const participants = router({ props: { name: watcher.user.name, participantName: participant.name, - pollUrl: absoluteUrl(`/poll/${poll.id}`), - disableNotificationsUrl: absoluteUrl( + pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`), + disableNotificationsUrl: ctx.absoluteUrl( `/auth/disable-notifications?token=${token}`, ), title: poll.title, diff --git a/packages/emails/src/send-email.tsx b/packages/emails/src/send-email.tsx index 8e6472fcb..077aac91d 100644 --- a/packages/emails/src/send-email.tsx +++ b/packages/emails/src/send-email.tsx @@ -7,6 +7,7 @@ import previewEmail from "preview-email"; import React from "react"; import * as templates from "./templates"; +import { EmailContext } from "./templates/components/email-context"; type Templates = typeof templates; @@ -15,7 +16,6 @@ type TemplateName = keyof typeof templates; type TemplateProps = React.ComponentProps< TemplateComponent >; - type TemplateComponent = Templates[T]; type SendEmailOptions = { @@ -59,6 +59,10 @@ type EmailClientConfig = { address: string; }; }; + /** + * Context to pass to each email + */ + context: EmailContext; }; export class EmailClient { @@ -79,8 +83,14 @@ export class EmailClient { } const Template = templates[templateName] as TemplateComponent; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = render(