♻️ Refactor email templating code (#533)

This commit is contained in:
Luke Vella 2023-03-03 11:46:30 +00:00 committed by GitHub
parent 0a836aeec7
commit 309cb109aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3926 additions and 1455 deletions

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules
/.pnp
.pnp.js

View file

@ -13,6 +13,7 @@ const nextConfig = {
i18n: i18n,
productionBrowserSourceMaps: true,
output: "standalone",
transpilePackages: ["@rallly/emails", "@rallly/database"],
webpack(config) {
config.module.rules.push({
test: /\.svg$/,

View file

@ -1,5 +1,5 @@
{
"name": "app",
"name": "@rallly/web",
"version": "0.0.0",
"private": true,
"scripts": {
@ -10,6 +10,7 @@
"lint": "eslint .",
"lint:tsc": "tsc --noEmit",
"lint:i18n": "i18n-unused remove-unused",
"prettier": "prettier --write ./src",
"test": "PORT=3001 playwright test",
"test:codegen": "playwright codegen http://localhost:3000",
"docker:start": "./scripts/docker-start.sh"
@ -20,6 +21,8 @@
"@next/bundle-analyzer": "^12.3.4",
"@radix-ui/react-popover": "^1.0.3",
"@rallly/database": "*",
"@rallly/emails": "*",
"@rallly/tailwind-config": "*",
"@sentry/nextjs": "^7.33.0",
"@svgr/webpack": "^6.5.1",
"@tailwindcss/typography": "^0.5.9",
@ -33,7 +36,6 @@
"autoprefixer": "^10.4.13",
"clsx": "^1.1.1",
"dayjs": "^1.11.7",
"eta": "^2.0.0",
"framer-motion": "^6.5.1",
"i18next": "^22.4.9",
"iron-session": "^6.3.1",
@ -43,7 +45,6 @@
"next": "^13.2.1",
"next-i18next": "^13.0.3",
"next-seo": "^5.15.0",
"nodemailer": "^6.9.0",
"postcss": "^8.4.21",
"posthog-js": "^1.42.3",
"react": "^18.2.0",
@ -68,7 +69,6 @@
"@rallly/tsconfig": "*",
"@types/accept-language-parser": "^1.5.3",
"@types/lodash": "^4.14.178",
"@types/nodemailer": "^6.4.4",
"@types/react": "^18.0.28",
"@types/react-big-calendar": "^0.31.0",
"@types/react-dom": "^18.0.11",

View file

@ -7,8 +7,8 @@ export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
<div className="h-full bg-gray-100 p-3 sm:p-8">
<div className="flex h-full items-start justify-center">
<div className="w-[480px] max-w-full overflow-hidden rounded-lg border bg-white shadow-sm">
<div className="bg-pattern border-b border-t-4 border-t-primary-500 bg-slate-500/5 p-4 text-center sm:p-8">
<Logo className="inline-block h-7 text-primary-500" />
<div className="bg-pattern border-t-primary-500 border-b border-t-4 bg-slate-500/5 p-4 text-center sm:p-8">
<Logo className="text-primary-500 inline-block h-7" />
</div>
<div className="p-4 sm:p-6">{children}</div>
</div>

View file

@ -5,7 +5,7 @@ import Logo from "~/public/logo.svg";
export const HomeLink = (props: { className?: string }) => {
return (
<Link href="/" className={props.className}>
<Logo className="inline-block w-28 text-primary-500 transition-colors active:text-primary-600 lg:w-32" />
<Logo className="text-primary-500 active:text-primary-600 inline-block w-28 transition-colors lg:w-32" />
</Link>
);
};

View file

@ -63,7 +63,7 @@ export const MobileNavigation = (props: { className?: string }) => {
type="button"
className="group flex items-center rounded px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
>
<Menu className="mr-2 w-5 group-hover:text-primary-500" />
<Menu className="group-hover:text-primary-500 mr-2 w-5" />
<Logo />
</button>
}
@ -117,9 +117,9 @@ export const MobileNavigation = (props: { className?: string }) => {
)}
>
<div className="relative shrink-0">
<UserCircle className="w-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<UserCircle className="group-hover:text-primary-500 w-5 opacity-75 group-hover:opacity-100" />
</div>
<div className="max-w-[120px] truncate font-medium xs:block">
<div className="xs:block max-w-[120px] truncate font-medium">
{user.isGuest ? t("app:guest") : user.shortName}
</div>
</button>
@ -134,7 +134,7 @@ export const MobileNavigation = (props: { className?: string }) => {
type="button"
className="group flex items-center whitespace-nowrap rounded px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
>
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500" />
<Adjustments className="group-hover:text-primary-500 h-5 opacity-75" />
<span className="ml-2 hidden sm:block">
{t("app:preferences")}
</span>

View file

@ -1,4 +1,4 @@
import { VoteType } from "@prisma/client";
import { VoteType } from "@rallly/database";
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";

View file

@ -21,25 +21,25 @@ const OpenBeta = () => {
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2">
<a
href="https://github.com/lukevella/rallly/issues/new?assignees=&labels=bug&template=---bug-report.md&title="
className="rounded border p-3 hover:text-primary-500"
className="hover:text-primary-500 rounded border p-3"
>
🐞 Submit a bug report
</a>
<a
href="https://github.com/lukevella/rallly/discussions/new/choose"
className="rounded border p-3 hover:text-primary-500"
className="hover:text-primary-500 rounded border p-3"
>
📢 Open a discussion with the community
</a>
<a
href="https://discord.gg/uzg4ZcHbuM"
className="rounded border p-3 hover:text-primary-500"
className="hover:text-primary-500 rounded border p-3"
>
💬 Chat on Discord
</a>
<a
href="mailto:feedback@rallly.co"
className="rounded border p-3 hover:text-primary-500"
className="hover:text-primary-500 rounded border p-3"
>
Send an email
</a>

View file

@ -1,4 +1,4 @@
import { Participant, Vote, VoteType } from "@prisma/client";
import { Participant, Vote, VoteType } from "@rallly/database";
import { useTranslation } from "next-i18next";
import * as React from "react";

View file

@ -1,4 +1,4 @@
import { Participant, Vote, VoteType } from "@prisma/client";
import { Participant, Vote, VoteType } from "@rallly/database";
import { keyBy } from "lodash";
import { useTranslation } from "next-i18next";
import React from "react";

View file

@ -1,4 +1,4 @@
import { Participant, Vote, VoteType } from "@prisma/client";
import { Participant, Vote, VoteType } from "@rallly/database";
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import * as React from "react";

View file

@ -1,4 +1,4 @@
import { Participant, VoteType } from "@prisma/client";
import { Participant, VoteType } from "@rallly/database";
import clsx from "clsx";
import { AnimatePresence, m } from "framer-motion";
import { useTranslation } from "next-i18next";

View file

@ -1,4 +1,4 @@
import { VoteType } from "@prisma/client";
import { VoteType } from "@rallly/database";
import * as React from "react";
import { Controller, useFormContext } from "react-hook-form";

View file

@ -1,4 +1,4 @@
import { VoteType } from "@prisma/client";
import { VoteType } from "@rallly/database";
export interface ParticipantForm {
votes: Array<

View file

@ -7,8 +7,8 @@ import { usePoll } from "../poll-context";
export const UnverifiedPollNotice = () => {
const { t } = useTranslation("app");
const { poll } = usePoll();
const requestVerificationEmail = trpc.polls.verification.request.useMutation(
);
const requestVerificationEmail =
trpc.polls.verification.request.useMutation();
return (
<div className="space-y-3 rounded-md border border-amber-200 bg-amber-100 p-3 text-gray-700 shadow-sm">

View file

@ -1,4 +1,4 @@
import { VoteType } from "@prisma/client";
import { VoteType } from "@rallly/database";
import clsx from "clsx";
import * as React from "react";

View file

@ -1,4 +1,4 @@
import { VoteType } from "@prisma/client";
import { VoteType } from "@rallly/database";
import clsx from "clsx";
import * as React from "react";

View file

@ -18,7 +18,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={clsx(
"z-50 animate-popIn rounded-md border bg-white p-1 shadow-md outline-none",
"animate-popIn z-50 rounded-md border bg-white p-1 shadow-md outline-none",
{
"origin-top-left": align === "start",
"origin-top-right": align === "end",

View file

@ -1,9 +1,7 @@
import { Prisma } from "@prisma/client";
import { Prisma, prisma } from "@rallly/database";
import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "@/utils/prisma";
import { parseValue } from "../../utils/date-time-utils";
/**

View file

@ -1,6 +1,5 @@
import { mergeRouters, router } from "../trpc";
import { auth } from "./auth";
import { login } from "./login";
import { polls } from "./polls";
import { user } from "./user";
import { whoami } from "./whoami";
@ -11,7 +10,6 @@ export const appRouter = mergeRouters(
auth,
polls,
user,
login,
}),
);

View file

@ -1,11 +1,8 @@
import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import emailTemplate from "~/templates/email-verification";
import { absoluteUrl } from "../../utils/absolute-url";
import { sendEmailTemplate } from "../../utils/api-utils";
import {
createToken,
decryptToken,
@ -21,12 +18,10 @@ const sendVerificationEmail = async (
name: string,
code: string,
) => {
await sendEmailTemplate({
await sendEmail("VerificationCodeEmail", {
to: email,
subject: `Your 6-digit code is: ${code}`,
templateString: emailTemplate,
templateVars: {
homePageUrl: absoluteUrl(),
props: {
code,
name,
},

View file

@ -1,42 +0,0 @@
import { z } from "zod";
import loginTemplate from "~/templates/login";
import { absoluteUrl } from "../../utils/absolute-url";
import { sendEmailTemplate } from "../../utils/api-utils";
import { createToken } from "../../utils/auth";
import { publicProcedure, router } from "../trpc";
export const login = router({
login: publicProcedure
.input(
z.object({
email: z.string(),
path: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
const { email, path } = input;
const homePageUrl = absoluteUrl();
const user = ctx.session.user;
const token = await createToken({
email,
guestId: user.id,
path,
});
const loginUrl = `${homePageUrl}/login?code=${token}`;
await sendEmailTemplate({
templateString: loginTemplate,
to: email,
subject: "Rallly - Login",
templateVars: {
loginUrl,
homePageUrl,
supportEmail: process.env.SUPPORT_EMAIL,
},
});
}),
});

View file

@ -1,12 +1,9 @@
import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import newPollTemplate from "~/templates/new-poll";
import newVerfiedPollTemplate from "~/templates/new-poll-verified";
import { absoluteUrl } from "../../utils/absolute-url";
import { sendEmailTemplate } from "../../utils/api-utils";
import { createToken } from "../../utils/auth";
import { nanoid } from "../../utils/nanoid";
import { GetPollApiResponse } from "../../utils/trpc/types";
@ -123,6 +120,7 @@ export const polls = router({
authorName: input.user.name,
demo: input.demo,
verified: verified,
notifications: verified,
adminUrlId,
participantUrlId: await nanoid(),
user: {
@ -146,21 +144,17 @@ export const polls = router({
},
});
const homePageUrl = absoluteUrl();
const pollUrl = `${homePageUrl}/admin/${adminUrlId}`;
const pollUrl = absoluteUrl(`/admin/${adminUrlId}`);
try {
if (poll.verified) {
await sendEmailTemplate({
templateString: newVerfiedPollTemplate,
await sendEmail("NewPollEmail", {
to: input.user.email,
subject: `Your poll for ${poll.title} has been created`,
templateVars: {
props: {
title: poll.title,
name: input.user.name,
pollUrl,
homePageUrl,
supportEmail: process.env.SUPPORT_EMAIL,
adminLink: pollUrl,
},
});
} else {
@ -169,17 +163,14 @@ export const polls = router({
});
const verifyEmailUrl = `${pollUrl}?code=${verificationCode}`;
await sendEmailTemplate({
templateString: newPollTemplate,
await sendEmail("NewPollVerificationEmail", {
to: input.user.email,
subject: `Your poll for ${poll.title} has been created`,
templateVars: {
props: {
title: poll.title,
name: input.user.name,
pollUrl,
verifyEmailUrl,
homePageUrl,
supportEmail: process.env.SUPPORT_EMAIL,
adminLink: pollUrl,
verificationLink: verifyEmailUrl,
},
});
}

View file

@ -1,7 +1,6 @@
import { prisma } from "@rallly/database";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import { sendNotification } from "../../../utils/api-utils";
import { publicProcedure, router } from "../../trpc";

View file

@ -1,8 +1,6 @@
import { VoteType } from "@prisma/client";
import { prisma, VoteType } from "@rallly/database";
import dayjs from "dayjs";
import { prisma } from "@/utils/prisma";
import { nanoid } from "../../../utils/nanoid";
import { publicProcedure, router } from "../../trpc";

View file

@ -1,7 +1,6 @@
import { prisma } from "@rallly/database";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import { sendNotification } from "../../../utils/api-utils";
import { publicProcedure, router } from "../../trpc";

View file

@ -1,11 +1,9 @@
import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import newPollTemplate from "~/templates/new-poll";
import { absoluteUrl } from "../../../utils/absolute-url";
import { sendEmailTemplate } from "../../../utils/api-utils";
import {
createToken,
decryptToken,
@ -39,6 +37,7 @@ export const verification = router({
},
data: {
verified: true,
notifications: true,
},
include: { user: true },
});
@ -79,24 +78,20 @@ export const verification = router({
});
}
const homePageUrl = absoluteUrl();
const pollUrl = `${homePageUrl}/admin/${adminUrlId}`;
const pollUrl = absoluteUrl(`/admin/${adminUrlId}`);
const token = await createToken({
pollId,
});
const verifyEmailUrl = `${pollUrl}?code=${token}`;
await sendEmailTemplate({
templateString: newPollTemplate,
await sendEmail("GuestVerifyEmail", {
to: poll.user.email,
subject: "Please verify your email address",
templateVars: {
props: {
title: poll.title,
name: poll.user.name,
pollUrl,
verifyEmailUrl,
homePageUrl,
supportEmail: process.env.SUPPORT_EMAIL,
adminLink: pollUrl,
verificationLink: verifyEmailUrl,
},
});
}),

View file

@ -1,9 +1,8 @@
import { prisma } from "@rallly/database";
import { TRPCError } from "@trpc/server";
import { IronSessionData } from "iron-session";
import { z } from "zod";
import { prisma } from "@/utils/prisma";
import { publicProcedure, router } from "../trpc";
const requireUser = (user: IronSessionData["user"]) => {

View file

@ -1,4 +1,4 @@
import { prisma } from "@/utils/prisma";
import { prisma } from "@rallly/database";
import { createGuestUser, UserSession } from "../../utils/auth";
import { publicProcedure, router } from "../trpc";

View file

@ -1,11 +1,7 @@
import * as Eta from "eta";
import { prisma } from "@/utils/prisma";
import newCommentTemplate from "~/templates/new-comment";
import newParticipantTemplate from "~/templates/new-participant";
import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails";
import { absoluteUrl } from "./absolute-url";
import { sendEmail } from "./send-email";
type NotificationAction =
| {
@ -24,7 +20,19 @@ export const sendNotification = async (
try {
const poll = await prisma.poll.findUnique({
where: { id: pollId },
include: { user: true },
select: {
verified: true,
demo: true,
notifications: true,
adminUrlId: true,
title: true,
user: {
select: {
name: true,
email: true,
},
},
},
});
/**
* poll needs to:
@ -46,34 +54,28 @@ export const sendNotification = async (
switch (action.type) {
case "newParticipant":
await sendEmailTemplate({
templateString: newParticipantTemplate,
await sendEmail("NewParticipantEmail", {
to: poll.user.email,
subject: `${action.participantName} has shared their availability for ${poll.title}`,
templateVars: {
title: poll.title,
name: poll.authorName,
subject: `New participant on ${poll.title}`,
props: {
name: poll.user.name,
participantName: action.participantName,
pollUrl,
homePageUrl: absoluteUrl(),
supportEmail: process.env.SUPPORT_EMAIL,
unsubscribeUrl,
title: poll.title,
},
});
break;
case "newComment":
await sendEmailTemplate({
templateString: newCommentTemplate,
await sendEmail("NewCommentEmail", {
to: poll.user.email,
subject: `${action.authorName} has commented on ${poll.title}`,
templateVars: {
title: poll.title,
name: poll.authorName,
author: action.authorName,
subject: `New comment on ${poll.title}`,
props: {
name: poll.user.name,
authorName: action.authorName,
pollUrl,
homePageUrl: absoluteUrl(),
supportEmail: process.env.SUPPORT_EMAIL,
unsubscribeUrl,
title: poll.title,
},
});
break;
@ -83,29 +85,3 @@ export const sendNotification = async (
console.error(e);
}
};
interface SendEmailTemplateParams {
templateString: string;
to: string;
subject: string;
templateVars: Record<string, string | undefined>;
}
export const sendEmailTemplate = async ({
templateString,
templateVars,
to,
subject,
}: SendEmailTemplateParams) => {
const rendered = Eta.render(templateString, templateVars);
if (rendered) {
await sendEmail({
html: rendered,
to,
subject,
});
} else {
throw new Error(`Failed to render email template`);
}
};

View file

@ -1,3 +1,4 @@
import { prisma } from "@rallly/database";
import {
IronSession,
IronSessionOptions,
@ -11,8 +12,6 @@ import {
NextApiHandler,
} from "next";
import { prisma } from "@/utils/prisma";
import { createSSGHelperFromContext } from "../server/context";
import { randomid } from "./nanoid";

View file

@ -1,4 +1,4 @@
import { Option } from "@prisma/client";
import { Option } from "@rallly/database";
import dayjs from "dayjs";
import {

View file

@ -1,15 +0,0 @@
import { PrismaClient } from "@rallly/database";
import { softDeleteMiddleware } from "./middlewares/softDeleteMiddleware";
declare global {
// allow global `var` declarations
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma || new PrismaClient();
softDeleteMiddleware(prisma, "Poll");
if (process.env.NODE_ENV !== "production") global.prisma = prisma;

View file

@ -1,45 +0,0 @@
import nodemailer from "nodemailer";
interface SendEmailParameters {
to: string;
subject: string;
html: string;
}
let transport: nodemailer.Transporter;
const env = process.env["NODE" + "_ENV"] || "development";
const getTransport = async () => {
if (!transport) {
if (env === "test") {
transport = nodemailer.createTransport({ port: 4025 });
} else {
transport = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PWD,
},
});
}
}
return transport;
};
export const sendEmail = async (params: SendEmailParameters) => {
const transport = await getTransport();
try {
await transport.verify();
return await transport.sendMail({
to: params.to,
from: `Rallly ${process.env.SUPPORT_EMAIL}`,
subject: params.subject,
html: params.html,
});
} catch (e) {
console.error(e);
}
};

View file

@ -1,4 +1,4 @@
import { Option, User } from "@prisma/client";
import { Option, User } from "@rallly/database";
export type GetPollApiResponse = {
id: string;

View file

@ -1,49 +1,15 @@
const colors = require("tailwindcss/colors");
const defaultTheme = require("tailwindcss/defaultTheme");
const sharedConfig = require("@rallly/tailwind-config/tailwind.config");
module.exports = {
...sharedConfig,
content: ["./src/pages/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"],
theme: {
extend: {
boxShadow: {
huge: "0px 51px 78px rgb(17 7 53 / 5%), 0px 21.3066px 35.4944px rgb(17 7 53 / 4%), 0px 11.3915px 18.9418px rgb(17 7 53 / 3%), 0px 6.38599px 9.8801px rgb(17 7 53 / 3%), 0px 3.39155px 4.58665px rgb(17 7 53 / 2%), 0px 1.4113px 1.55262px rgb(17 7 53 / 1%), inset 0px 1px 0px rgb(41 56 78 / 5%)",
},
colors: {
primary: colors.indigo,
},
keyframes: {
wiggle: {
"0%, 100%": { transform: "rotate(-1deg)" },
"50%": { transform: "rotate(1deg)" },
},
popIn: {
"0%": {
transform: "scale(0.8) translateY(-10px)",
opacity: "0",
},
"100%": {
transform: "scale(1) translateY(0px)",
opacity: "1",
translateY: "0",
},
},
},
animation: {
wiggle: "wiggle 0.2s ease-in-out",
popIn: "popIn 0.1s ease-out",
},
screens: {
xs: "375px",
},
...sharedConfig.theme,
fontFamily: {
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
mono: ["var(--font-noto)", ...defaultTheme.fontFamily.mono],
},
transitionTimingFunction: {
"in-expo": "cubic-bezier(0.68, -0.6, 0.32, 1.6)",
"out-expo": "cubic-bezier(0.19, 1, 0.22, 1)",
},
},
},
plugins: [require("@tailwindcss/typography"), require("tailwindcss-animate")],
};

View file

@ -1,314 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Please verify your email address</title>
<style>
.hover-underline:hover {
text-decoration-line: underline !important;
}
.hover-no-underline:hover {
text-decoration-line: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-8 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-6 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body
style="
margin: 0;
width: 100%;
padding: 0;
word-break: break-word;
-webkit-font-smoothing: antialiased;
background-color: #fff;
"
>
<div style="display: none">
Use the 6-digit code provided to complete the verification process.&#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &zwnj; &#160;&#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &zwnj; &#160;&#847; &#847; &#847; &#847;
&#847;
</div>
<div
role="article"
aria-roledescription="email"
aria-label="Please verify your email address"
lang="en"
>
<table
style="
width: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI',
sans-serif;
"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td
align="center"
class="sm-py-8"
style="padding-top: 64px; padding-bottom: 64px"
>
<table
class="sm-w-full"
style="width: 480px"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td
style="
padding-left: 32px;
padding-right: 32px;
color: #334155;
"
>
<a href="<%= it.homePageUrl %>">
<img
src="<%= it.homePageUrl %>/logo.png"
width="150"
alt="Rallly"
style="
max-width: 100%;
vertical-align: middle;
line-height: 100%;
border: 0;
"
/>
</a>
</td>
</tr>
<tr>
<td
align="center"
class="sm-px-6"
style="
padding: 32px;
text-align: left;
font-size: 16px;
line-height: 24px;
color: #475569;
"
>
<table
style="width: 100%"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td>
<p
style="
margin: 0;
margin-bottom: 32px;
line-height: 24px;
"
>
Hey <strong id="name"><%= it.name %></strong>,
</p>
<div
style="
margin-bottom: 32px;
border-radius: 8px;
background-color: #f9fafb;
padding-top: 16px;
padding-bottom: 16px;
text-align: center;
"
>
<p
style="
margin: 0;
margin-bottom: 16px;
line-height: 24px;
"
>
Your 6-digit code is:
</p>
<p
style="
margin: 0;
margin-bottom: 16px;
text-align: center;
font-size: 30px;
font-weight: 700;
line-height: 32px;
letter-spacing: 8px;
color: #1e293b;
"
id="code"
>
<%= it.code %>
</p>
<p
style="
margin: 0;
text-align: center;
line-height: 24px;
"
>
This code is valid for 10 minutues.
</p>
</div>
<p style="margin: 0; line-height: 24px">
Use this code to complete the verification process.
</p>
</td>
</tr>
<tr>
<td>
<table
style="width: 100%"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td style="padding-top: 32px; padding-bottom: 32px">
<div
style="
height: 1px;
background-color: #e5e7eb;
line-height: 1px;
"
>
&zwnj;
</div>
</td>
</tr>
</table>
<p style="margin: 0; text-align: center">
Not sure why you received this email? Please
<a
href="mailto:<%= it.supportEmail %>"
class="hover-no-underline"
style="
color: #6366f1;
text-decoration-line: underline;
"
>let us know</a
>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td
class="sm-px-6"
style="
padding-left: 32px;
padding-right: 32px;
text-align: center;
font-size: 14px;
color: #4b5563;
"
>
<p style="cursor: default">
<a
href="<%= it.homePageUrl %>"
class="hover-underline"
style="color: #6366f1; text-decoration-line: none"
>Home</a
>
&bull;
<a
href="https://twitter.com/ralllyco"
class="hover-underline"
style="color: #6366f1; text-decoration-line: none"
>Twitter</a
>
&bull;
<a
href="https://github.com/lukevella/rallly"
class="hover-underline"
style="color: #6366f1; text-decoration-line: none"
>Github</a
>
&bull;
<a
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
class="hover-underline"
style="color: #6366f1; text-decoration-line: none"
>Donate</a
>
&bull;
<a
href="mailto:<%= it.supportEmail %>"
class="hover-underline"
style="color: #6366f1; text-decoration-line: none"
>Contact</a
>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,150 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Login with your email</title>
<style>
.hover-bg-indigo-400:hover {
background-color: #818cf8 !important;
}
.hover-underline:hover {
text-decoration: underline !important;
}
.hover-no-underline:hover {
text-decoration: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-32 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-24 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="margin: 0; width: 100%; padding: 0; word-break: break-word; -webkit-font-smoothing: antialiased; background-color: #f3f4f6;">
<div style="display: none;">
Please click the link below to verify your email address.&#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847;
</div>
<div role="article" aria-roledescription="email" aria-label="Login with your email" lang="en">
<table style="width: 100%; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" style="background-color: #f3f4f6;">
<table class="sm-w-full" style="width: 600px;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-py-32 sm-px-24" style="padding-left: 48px; padding-right: 48px; padding-top: 36px; padding-bottom: 36px; text-align: center;">
<a href="<%= it.homePageUrl %>">
<img src="<%= it.homePageUrl %>/logo.png" width="150" alt="Rallly" style="max-width: 100%; vertical-align: middle; line-height: 100%; border: 0;">
</a>
</td>
</tr>
<tr>
<td align="center" class="sm-px-24">
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-px-24" style="border-radius: 4px; background-color: #ffffff; padding: 36px; text-align: left; font-size: 16px; line-height: 24px; color: #1f2937;">
<p style="margin-bottom: 8px;">Hey there,</p>
<p style="margin-bottom: 8px;">
To login with your email please click the button below:
</p>
<p style="margin-bottom: 24px;"></p>
<div style="line-height: 100%;">
<a href="<%= it.loginUrl %>" class="hover-bg-indigo-400" style="display: inline-block; border-radius: 4px; background-color: #6366f1; padding-top: 16px; padding-bottom: 16px; padding-left: 24px; padding-right: 24px; text-align: center; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none;"> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%; mso-text-raise: 26pt;">&nbsp;</i><![endif]-->
<span style="mso-text-raise: 16px">Log me in &rarr;
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;">&nbsp;</i><![endif]-->
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;">
<div style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
&zwnj;
</div>
</td>
</tr>
</table>
<p>
Not sure why you received this email? Please
<a href="mailto:<%= it.supportEmail %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">let us know</a>.
</p>
</td>
</tr>
<tr>
<td style="height: 48px;"></td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #4b5563;">
<p style="margin-bottom: 4px; text-transform: uppercase;">RALLLY</p>
<p style="font-style: italic;">Collaborative Scheduling</p>
<p style="cursor: default;">
<a href="<%= it.homePageUrl %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Website</a>
&bull;
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
&bull;
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
&bull;
<a href="mailto:<%= it.supportEmail %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Contact</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,154 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Somone left a comment on your poll</title>
<style>
.hover-bg-indigo-400:hover {
background-color: #818cf8 !important;
}
.hover-underline:hover {
text-decoration: underline !important;
}
.hover-no-underline:hover {
text-decoration: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-32 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-24 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="margin: 0; width: 100%; padding: 0; word-break: break-word; -webkit-font-smoothing: antialiased; background-color: #f3f4f6;">
<div style="display: none;">
Go to your poll to see what they wrote&#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847;
</div>
<div role="article" aria-roledescription="email" aria-label="Somone left a comment on your poll" lang="en">
<table style="width: 100%; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" style="background-color: #f3f4f6;">
<table class="sm-w-full" style="width: 600px;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-py-32 sm-px-24" style="padding-left: 48px; padding-right: 48px; padding-top: 36px; padding-bottom: 36px; text-align: center;">
<a href="<%= it.homePageUrl %>>">
<img src="<%= it.homePageUrl %>/logo.png" width="150" alt="Rallly" style="max-width: 100%; vertical-align: middle; line-height: 100%; border: 0;">
</a>
</td>
</tr>
<tr>
<td align="center" class="sm-px-24">
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-px-24" style="border-radius: 4px; background-color: #ffffff; padding: 36px; text-align: left; font-size: 16px; line-height: 24px; color: #1f2937;">
<p style="margin-bottom: 8px;">Hi <%= it.name %>,</p>
<p style="margin-bottom: 8px;">
<strong><%= it.author %></strong> has left a comment on
your&nbsp;poll.
</p>
<p style="margin-bottom: 24px;"></p>
<div style="margin-bottom: 24px; line-height: 100%;">
<a href="<%= it.pollUrl %>" class="hover-bg-indigo-400" style="display: inline-block; border-radius: 4px; background-color: #6366f1; padding-top: 16px; padding-bottom: 16px; padding-left: 24px; padding-right: 24px; text-align: center; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none;"> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%; mso-text-raise: 26pt;">&nbsp;</i><![endif]-->
<span style="mso-text-raise: 16px">Go to poll &rarr;
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;">&nbsp;</i><![endif]-->
</a>
</div>
<p>
<a href="<%= it.unsubscribeUrl %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">Stop receiving notifications for this poll.</a>
</p>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;">
<div style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
&zwnj;
</div>
</td>
</tr>
</table>
<p>
Not sure why you received this email? Please
<a href="mailto:<%= it.supportEmail %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">let us know</a>.
</p>
</td>
</tr>
<tr>
<td style="height: 48px;"></td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #4b5563;">
<p style="margin-bottom: 4px; text-transform: uppercase;">RALLLY</p>
<p style="font-style: italic;">Collaborative Scheduling</p>
<p style="cursor: default;">
<a href="<%= it.homePageUrl %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Website</a>
&bull;
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
&bull;
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
&bull;
<a href="mailto:<%= it.supportEmail %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Contact</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,154 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Your poll has a new participant</title>
<style>
.hover-bg-indigo-400:hover {
background-color: #818cf8 !important;
}
.hover-underline:hover {
text-decoration: underline !important;
}
.hover-no-underline:hover {
text-decoration: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-32 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-24 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="margin: 0; width: 100%; padding: 0; word-break: break-word; -webkit-font-smoothing: antialiased; background-color: #f3f4f6;">
<div style="display: none;">
Go to your poll to see how they voted&#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847;
</div>
<div role="article" aria-roledescription="email" aria-label="Your poll has a new participant" lang="en">
<table style="width: 100%; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" style="background-color: #f3f4f6;">
<table class="sm-w-full" style="width: 600px;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-py-32 sm-px-24" style="padding-left: 48px; padding-right: 48px; padding-top: 36px; padding-bottom: 36px; text-align: center;">
<a href="<%= it.homePageUrl %>">
<img src="<%= it.homePageUrl %>/logo.png" width="150" alt="Rallly" style="max-width: 100%; vertical-align: middle; line-height: 100%; border: 0;">
</a>
</td>
</tr>
<tr>
<td align="center" class="sm-px-24">
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-px-24" style="border-radius: 4px; background-color: #ffffff; padding: 36px; text-align: left; font-size: 16px; line-height: 24px; color: #1f2937;">
<p style="margin-bottom: 8px;">Hi <%= it.name %>,</p>
<p style="margin-bottom: 8px;">
<strong><%= it.participantName %></strong> has voted on
your&nbsp;poll.
</p>
<p style="margin-bottom: 24px;"></p>
<div style="margin-bottom: 24px; line-height: 100%;">
<a href="<%= it.pollUrl %>" class="hover-bg-indigo-400" style="display: inline-block; border-radius: 4px; background-color: #6366f1; padding-top: 16px; padding-bottom: 16px; padding-left: 24px; padding-right: 24px; text-align: center; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none;"> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%; mso-text-raise: 26pt;">&nbsp;</i><![endif]-->
<span style="mso-text-raise: 16px">Go to poll &rarr;
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;">&nbsp;</i><![endif]-->
</a>
</div>
<p>
<a href="<%= it.unsubscribeUrl %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">Stop receiving notifications for this poll.</a>
</p>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;">
<div style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
&zwnj;
</div>
</td>
</tr>
</table>
<p>
Not sure why you received this email? Please
<a href="mailto:<%= it.supportEmail %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">let us know</a>.
</p>
</td>
</tr>
<tr>
<td style="height: 48px;"></td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #4b5563;">
<p style="margin-bottom: 4px; text-transform: uppercase;">RALLLY</p>
<p style="font-style: italic;">Collaborative Scheduling</p>
<p style="cursor: default;">
<a href="<%= it.homePageUrl %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Website</a>
&bull;
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
&bull;
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
&bull;
<a href="mailto:<%= it.supportEmail %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Contact</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,159 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Your poll has been created</title>
<style>
.hover-bg-indigo-400:hover {
background-color: #818cf8 !important;
}
.hover-underline:hover {
text-decoration: underline !important;
}
.hover-no-underline:hover {
text-decoration: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-32 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-24 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="margin: 0; width: 100%; padding: 0; word-break: break-word; -webkit-font-smoothing: antialiased; background-color: #f3f4f6;">
<div style="display: none;">
Click the button below to access your poll!&#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847;
</div>
<div role="article" aria-roledescription="email" aria-label="Your poll has been created" lang="en">
<table style="width: 100%; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" style="background-color: #f3f4f6;">
<table class="sm-w-full" style="width: 600px;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-py-32 sm-px-24" style="padding-left: 48px; padding-right: 48px; padding-top: 36px; padding-bottom: 36px; text-align: center;">
<a href="<%= it.homePageUrl %>">
<img src="<%= it.homePageUrl %>/logo.png" width="150" alt="Rallly" style="max-width: 100%; vertical-align: middle; line-height: 100%; border: 0;">
</a>
</td>
</tr>
<tr>
<td align="center" class="sm-px-24">
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-px-24" style="border-radius: 4px; background-color: #ffffff; padding: 36px; text-align: left; font-size: 16px; line-height: 24px; color: #1f2937;">
<p style="margin-bottom: 8px;">Hi <%= it.name %>,</p>
<p style="margin-bottom: 8px;">
Your poll <strong>"<%= it.title %>"</strong> has been
created.
</p>
<p style="margin-bottom: 24px;"></p>
<div style="line-height: 100%;">
<a id="pollUrl" href="<%= it.pollUrl %>" class="hover-bg-indigo-400" style="display: inline-block; border-radius: 4px; background-color: #6366f1; padding-top: 16px; padding-bottom: 16px; padding-left: 24px; padding-right: 24px; text-align: center; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none;"> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%; mso-text-raise: 26pt;">&nbsp;</i><![endif]-->
<span style="mso-text-raise: 16px">Go to poll &rarr;
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;">&nbsp;</i><![endif]-->
</a>
</div>
<p style="margin-bottom: 8px;">
You can use the <em>admin link</em> below to manage your poll.
</p>
<p style="font-weight: 500;">
<a id="pollUrl" href="<%= it.pollUrl %>" style="display: inline-block; background-color: #eef2ff; padding: 8px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 20px; color: #6366f1; text-decoration: none;">
<%= it.pollUrl %>
</a>
</p>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;">
<div style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
&zwnj;
</div>
</td>
</tr>
</table>
<p>
Not sure why you received this email? Please
<a href="mailto:<%= it.supportEmail %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">let us know</a>.
</p>
</td>
</tr>
<tr>
<td style="height: 48px;"></td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #4b5563;">
<p style="margin-bottom: 4px; text-transform: uppercase;">RALLLY</p>
<p style="font-style: italic;">Collaborative Scheduling</p>
<p style="cursor: default;">
<a href="<%= it.homePageUrl %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Website</a>
&bull;
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
&bull;
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
&bull;
<a href="mailto:<%= it.supportEmail %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Contact</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,160 +0,0 @@
const template = `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<style>
td,
th,
div,
p,
a,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Segoe UI", sans-serif;
mso-line-height-rule: exactly;
}
</style>
<![endif]-->
<title>Your poll has been created</title>
<style>
.hover-bg-indigo-400:hover {
background-color: #818cf8 !important;
}
.hover-underline:hover {
text-decoration: underline !important;
}
.hover-no-underline:hover {
text-decoration: none !important;
}
@media (max-width: 600px) {
.sm-w-full {
width: 100% !important;
}
.sm-py-32 {
padding-top: 32px !important;
padding-bottom: 32px !important;
}
.sm-px-24 {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="margin: 0; width: 100%; padding: 0; word-break: break-word; -webkit-font-smoothing: antialiased; background-color: #f3f4f6;">
<div style="display: none;">
Please click the link below to verify your email address!&#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847;
&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
&#160;&#847; &#847; &#847; &#847; &#847;
</div>
<div role="article" aria-roledescription="email" aria-label="Your poll has been created" lang="en">
<table style="width: 100%; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" style="background-color: #f3f4f6;">
<table class="sm-w-full" style="width: 600px;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-py-32 sm-px-24" style="padding-left: 48px; padding-right: 48px; padding-top: 36px; padding-bottom: 36px; text-align: center;">
<a href="<%= it.homePageUrl %>">
<img src="<%= it.homePageUrl %>/logo.png" width="150" alt="Rallly" style="max-width: 100%; vertical-align: middle; line-height: 100%; border: 0;">
</a>
</td>
</tr>
<tr>
<td align="center" class="sm-px-24">
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="sm-px-24" style="border-radius: 4px; background-color: #ffffff; padding: 36px; text-align: left; font-size: 16px; line-height: 24px; color: #1f2937;">
<p style="margin-bottom: 8px;">Hi <%= it.name %>,</p>
<p style="margin-bottom: 8px;">
Your poll <strong>"<%= it.title %>"</strong> has been
created. Please verify your email address to claim
ownership of this&nbsp;poll:
</p>
<p style="margin-bottom: 24px;"></p>
<div style="margin-bottom: 24px; line-height: 100%;">
<a id="verifyEmailUrl" href="<%= it.verifyEmailUrl %>" class="hover-bg-indigo-400" style="display: inline-block; border-radius: 4px; background-color: #6366f1; padding-top: 16px; padding-bottom: 16px; padding-left: 24px; padding-right: 24px; text-align: center; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none;"> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%; mso-text-raise: 26pt;">&nbsp;</i><![endif]-->
<span style="mso-text-raise: 16px">Verify your email &rarr;
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;">&nbsp;</i><![endif]-->
</a>
</div>
<p style="margin-bottom: 8px;">
You can use the <em>admin link</em> below to manage your poll.
</p>
<p style="font-weight: 500;">
<a id="pollUrl" href="<%= it.pollUrl %>" style="display: inline-block; background-color: #eef2ff; padding: 8px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 20px; color: #6366f1; text-decoration: none;">
<%= it.pollUrl %>
</a>
</p>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;">
<div style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
&zwnj;
</div>
</td>
</tr>
</table>
<p>
Not sure why you received this email? Please
<a href="mailto:<%= it.supportEmail %>" class="hover-no-underline" style="color: #6366f1; text-decoration: underline;">let us know</a>.
</p>
</td>
</tr>
<tr>
<td style="height: 48px;"></td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #4b5563;">
<p style="margin-bottom: 4px; text-transform: uppercase;">RALLLY</p>
<p style="font-style: italic;">Collaborative Scheduling</p>
<p style="cursor: default;">
<a href="<%= it.homePageUrl %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Website</a>
&bull;
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
&bull;
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
&bull;
<a href="mailto:<%= it.supportEmail %>" class="hover-underline" style="color: #6366f1; text-decoration: none;">Contact</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`;
export default template;

View file

@ -1,9 +1,8 @@
import { expect, Page, test } from "@playwright/test";
import { prisma } from "@rallly/database";
import { load } from "cheerio";
import smtpTester from "smtp-tester";
import { prisma } from "@/utils/prisma";
const testUserEmail = "test@example.com";
test.describe.serial(() => {

View file

@ -1,9 +1,7 @@
import { expect, test } from "@playwright/test";
import { Prisma } from "@prisma/client";
import { Prisma, prisma } from "@rallly/database";
import dayjs from "dayjs";
import { prisma } from "@/utils/prisma";
/**
* House keeping policy:
* * Demo polls are hard deleted after one day

View file

@ -1,37 +1,24 @@
{
"compilerOptions": {
"allowJs": false,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"lib": ["dom", "es2017"],
"module": "ESNext",
"moduleResolution": "node",
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ESNext",
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"suppressImplicitAnyIndexErrors": true,
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "**/.*/", "**/*.js"]
"exclude": ["node_modules", "**/*.js"]
}

View file

@ -14,6 +14,7 @@ services:
rallly:
build:
context: .
dockerfile: ./apps/web/Dockerfile
restart: always
depends_on:
rallly_db:

View file

@ -3,8 +3,10 @@
"private": true,
"version": "2.1.1",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"db:deploy": "turbo db:deploy",
"db:generate": "turbo db:generate",
"test": "turbo test",
"lint": "turbo lint",
"lint:tsc": "turbo lint:tsc",
@ -20,5 +22,8 @@
"prettier": "^2.8.4",
"turbo": "^1.8.3"
},
"engines": {
"node": ">=16.0.0"
},
"packageManager": "yarn@1.22.19"
}

View file

@ -1 +1,17 @@
import { PrismaClient } from "@rallly/database";
import { softDeleteMiddleware } from "./middleware/soft-delete-middleware";
export * from "@prisma/client";
declare global {
// allow global `var` declarations
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma || new PrismaClient();
softDeleteMiddleware(prisma, "Poll");
if (process.env.NODE_ENV !== "production") global.prisma = prisma;

View file

@ -3,7 +3,7 @@
"private": true,
"version": "0.0.0",
"scripts": {
"generate": "prisma generate",
"db:generate": "prisma generate",
"db:push": "prisma db push --skip-generate",
"db:deploy": "prisma migrate deploy"
},

View file

@ -1,4 +0,0 @@
{
"extends": "@rallly/tsconfig/base.json",
"includes": ["**/*.ts"]
}

2
packages/emails/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.react-email
/out

View file

@ -0,0 +1,24 @@
{
"name": "@rallly/emails",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "email dev --port 3333 --dir ./src/templates"
},
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"dependencies": {
"@react-email/components": "0.0.2",
"@react-email/render": "0.0.6",
"@react-email/tailwind": "0.0.6",
"clsx": "^1.2.1",
"nodemailer": "^6.9.1",
"react-email": "1.7.15"
},
"devDependencies": {
"@rallly/tailwind-config": "*",
"@rallly/tsconfig": "*",
"@rallly/utils": "*",
"@types/nodemailer": "^6.4.7"
}
}

27
packages/emails/readme.md Normal file
View file

@ -0,0 +1,27 @@
# React Email Starter
A live preview right in your browser so you don't need to keep sending real emails during development.
## Getting Started
First, install the dependencies:
```sh
npm install
# or
yarn
```
Then, run the development server:
```sh
npm run dev
# or
yarn dev
```
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
## License
MIT License

View file

@ -0,0 +1 @@
export * from "./send-email";

View file

@ -0,0 +1,64 @@
import { render } from "@react-email/render";
import { createTransport, Transporter } from "nodemailer";
import React from "react";
import * as templates from "./templates";
type Templates = typeof templates;
type TemplateName = keyof typeof templates;
type TemplateProps<T extends TemplateName> = React.ComponentProps<
TemplateComponent<T>
>;
type TemplateComponent<T extends TemplateName> = Templates[T];
const env = process.env["NODE" + "_ENV"] || "development";
let transport: Transporter;
const getTransport = () => {
if (env === "test") {
transport = createTransport({ port: 4025 });
} else {
transport = createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : undefined,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PWD,
},
});
}
return transport;
};
type SendEmailOptions<T extends TemplateName> = {
to: string;
subject: string;
props: TemplateProps<T>;
onError?: () => void;
};
export const sendEmail = async <T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) => {
const transport = getTransport();
const Template = templates[templateName] as TemplateComponent<T>;
try {
return await transport.sendMail({
from: process.env.SUPPORT_EMAIL,
to: options.to,
subject: options.subject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
html: render(<Template {...(options.props as any)} />),
});
} catch (e) {
console.error("Error sending email", templateName);
options.onError?.();
}
};

View file

@ -0,0 +1,6 @@
export * from "./templates/guest-verify-email";
export * from "./templates/new-comment";
export * from "./templates/new-participant";
export * from "./templates/new-poll";
export * from "./templates/new-poll-verification";
export * from "./templates/verification-code";

View file

@ -0,0 +1,162 @@
import tailwindConfig from "@rallly/tailwind-config";
import { absoluteUrl } from "@rallly/utils";
import {
Body,
Container,
Head,
Html,
Img,
Link,
Preview,
Section,
} from "@react-email/components";
import { Tailwind } from "@react-email/tailwind";
export const EmailLayout = (props: {
children: React.ReactNode;
preview: string;
}) => {
return (
<Html>
<Head />
<Preview>{props.preview}</Preview>
<Tailwind
config={{
theme: {
extend: {
...tailwindConfig.theme.extend,
spacing: {
screen: "100vw",
full: "100%",
px: "1px",
0: "0",
0.5: "2px",
1: "4px",
1.5: "6px",
2: "8px",
2.5: "10px",
3: "12px",
3.5: "14px",
4: "16px",
4.5: "18px",
5: "20px",
5.5: "22px",
6: "24px",
6.5: "26px",
7: "28px",
7.5: "30px",
8: "32px",
8.5: "34px",
9: "36px",
9.5: "38px",
10: "40px",
11: "44px",
12: "48px",
14: "56px",
16: "64px",
20: "80px",
24: "96px",
28: "112px",
32: "128px",
36: "144px",
40: "160px",
44: "176px",
48: "192px",
52: "208px",
56: "224px",
60: "240px",
64: "256px",
72: "288px",
80: "320px",
96: "384px",
97.5: "390px",
120: "480px",
150: "600px",
160: "640px",
175: "700px",
"1/2": "50%",
"1/3": "33.333333%",
"2/3": "66.666667%",
"1/4": "25%",
"2/4": "50%",
"3/4": "75%",
"1/5": "20%",
"2/5": "40%",
"3/5": "60%",
"4/5": "80%",
"1/6": "16.666667%",
"2/6": "33.333333%",
"3/6": "50%",
"4/6": "66.666667%",
"5/6": "83.333333%",
"1/12": "8.333333%",
"2/12": "16.666667%",
"3/12": "25%",
"4/12": "33.333333%",
"5/12": "41.666667%",
"6/12": "50%",
"7/12": "58.333333%",
"8/12": "66.666667%",
"9/12": "75%",
"10/12": "83.333333%",
"11/12": "91.666667%",
},
borderRadius: {
none: "0px",
sm: "2px",
DEFAULT: "4px",
md: "6px",
lg: "8px",
xl: "12px",
"2xl": "16px",
"3xl": "24px",
},
},
},
}}
>
<Body className="bg-gray-50 p-4">
<Container className="mx-auto bg-white p-6">
<Section className="mb-4">
<Img src={absoluteUrl("/logo.png")} alt="Rallly" width={128} />
</Section>
<Section>{props.children}</Section>
<Section className="mt-4 text-sm text-slate-500">
<Link className="font-sans text-slate-500" href={absoluteUrl()}>
Home
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://twitter.com/ralllyco"
>
Twitter
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://github.com/lukevella/rallly"
>
Github
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
>
Donate
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href={`mailto:${process.env.SUPPORT_EMAIL}`}
>
Contact
</Link>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};

View file

@ -0,0 +1,34 @@
import { Container } from "@react-email/container";
import { Link, Text } from "./styled-components";
export interface NewPollBaseEmailProps {
title: string;
name: string;
adminLink: string;
}
export const NewPollBaseEmail = ({
name,
title,
adminLink,
children,
}: React.PropsWithChildren<NewPollBaseEmailProps>) => {
return (
<Container>
<Text>Hi {name},</Text>
<Text>
Your poll <strong>&quot;{title}&quot;</strong> has been created.
</Text>
<Text>
To manage your poll use the <em>admin link</em> below.
</Text>
<Text>
<Link href={adminLink}>
<span className="font-mono">{adminLink}</span> &rarr;
</Link>
</Text>
{children}
</Container>
);
};

View file

@ -0,0 +1,63 @@
import {
Button as UnstyledButton,
ButtonProps,
Heading as UnstyledHeading,
Link as UnstyledLink,
LinkProps,
Section as UnstyledSection,
SectionProps,
Text as UnstyledText,
TextProps,
} from "@react-email/components";
import clsx from "clsx";
export const Text = (props: TextProps) => {
return (
<UnstyledText
{...props}
className={clsx(
"my-4 font-sans text-base text-slate-800",
props.className,
)}
/>
);
};
export const Button = (props: ButtonProps) => {
return (
<UnstyledButton
{...props}
className={clsx(
"bg-primary-500 rounded px-3 py-2 font-sans text-white",
props.className,
)}
/>
);
};
export const Link = (props: LinkProps) => {
return (
<UnstyledLink
{...props}
className={clsx("text-primary-500 font-sans text-base", props.className)}
/>
);
};
export const Heading = (
props: React.ComponentProps<typeof UnstyledHeading>,
) => {
return (
<UnstyledHeading
{...props}
as={props.as || "h3"}
className={clsx("my-4 font-sans text-slate-800", props.className)}
/>
);
};
export const Section = (props: SectionProps) => {
return (
<UnstyledSection {...props} className={clsx("my-4", props.className)} />
);
};

View file

@ -0,0 +1,41 @@
import { Button, Container } from "@react-email/components";
import { EmailLayout } from "./components/email-layout";
import { Section, Text } from "./components/styled-components";
type GuestVerifyEmailProps = {
title: string;
name: string;
verificationLink: string;
adminLink: string;
};
export const GuestVerifyEmail = ({
title = "Untitled Poll",
name = "Guest",
verificationLink = "https://rallly.co",
}: GuestVerifyEmailProps) => {
return (
<EmailLayout preview="Click the button below to verify your email">
<Container>
<Text>Hi {name},</Text>
<Text>
To receive notifications for <strong>&quot;{title}&quot;</strong> you
will need to verify your email address.
</Text>
<Text>To verify your email please click the button below.</Text>
<Section>
<Button
className="bg-primary-500 rounded px-3 py-2 font-sans text-white"
href={verificationLink}
id="verifyEmailUrl"
>
Verify your email &rarr;
</Button>
</Section>
</Container>
</EmailLayout>
);
};
export default GuestVerifyEmail;

View file

@ -0,0 +1,37 @@
import { EmailLayout } from "./components/email-layout";
import { Button, Link, Section, Text } from "./components/styled-components";
export interface NewCommentEmailProps {
name: string;
title: string;
authorName: string;
pollUrl: string;
unsubscribeUrl: string;
}
export const NewCommentEmail = ({
name = "Guest",
title = "Untitled Poll",
authorName = "Someone",
pollUrl = "https://rallly.co",
unsubscribeUrl = "https://rallly.co",
}: NewCommentEmailProps) => {
return (
<EmailLayout preview={`${authorName} has commented on ${title}`}>
<Text>Hi {name},</Text>
<Text>
<strong>{authorName}</strong> has commented on <strong>{title}</strong>.
</Text>
<Section>
<Button href={pollUrl}>Go to poll &rarr;</Button>
</Section>
<Text>
<Link href={unsubscribeUrl}>
Stop receiving notifications for this poll.
</Link>
</Text>
</EmailLayout>
);
};
export default NewCommentEmail;

View file

@ -0,0 +1,40 @@
import { EmailLayout } from "./components/email-layout";
import { Button, Link, Section, Text } from "./components/styled-components";
export interface NewParticipantEmailProps {
name: string;
title: string;
participantName: string;
pollUrl: string;
unsubscribeUrl: string;
}
export const NewParticipantEmail = ({
name = "Guest",
title = "Untitled Poll",
participantName = "Someone",
pollUrl = "https://rallly.co",
unsubscribeUrl = "https://rallly.co",
}: NewParticipantEmailProps) => {
return (
<EmailLayout
preview={`${participantName} has shared their availability for ${title}`}
>
<Text>Hi {name},</Text>
<Text>
<strong>{participantName}</strong> has shared their availability for{" "}
<strong>{title}</strong>.
</Text>
<Section>
<Button href={pollUrl}>Go to poll &rarr;</Button>
</Section>
<Text>
<Link href={unsubscribeUrl}>
Stop receiving notifications for this poll.
</Link>
</Text>
</EmailLayout>
);
};
export default NewParticipantEmail;

View file

@ -0,0 +1,37 @@
import { EmailLayout } from "./components/email-layout";
import {
NewPollBaseEmail,
NewPollBaseEmailProps,
} from "./components/new-poll-base";
import { Button, Heading, Section, Text } from "./components/styled-components";
export interface NewPollVerificationEmailProps extends NewPollBaseEmailProps {
verificationLink: string;
}
export const NewPollVerificationEmail = ({
title = "Untitled Poll",
name = "Guest",
verificationLink = "https://rallly.co",
adminLink = "https://rallly.co/admin/abcdefg123",
}: NewPollVerificationEmailProps) => {
return (
<EmailLayout preview="Please verify your email address to turn on notifications">
<NewPollBaseEmail name={name} title={title} adminLink={adminLink}>
<Section className="mt-8 bg-gray-100 px-4 text-center">
<Heading as="h3">
Want to get notified when participants vote?
</Heading>
<Text>Verify your email address to turn on notifications.</Text>
<Section>
<Button id="verifyEmailUrl" href={verificationLink}>
Verify your email &rarr;
</Button>
</Section>
</Section>
</NewPollBaseEmail>
</EmailLayout>
);
};
export default NewPollVerificationEmail;

View file

@ -0,0 +1,19 @@
import { EmailLayout } from "./components/email-layout";
import {
NewPollBaseEmail,
NewPollBaseEmailProps,
} from "./components/new-poll-base";
export const NewPollEmail = ({
title = "Untitled Poll",
name = "Guest",
adminLink = "https://rallly.co/admin/abcdefg123",
}: NewPollBaseEmailProps) => {
return (
<EmailLayout preview="Please verify your email address to turn on notifications">
<NewPollBaseEmail name={name} title={title} adminLink={adminLink} />
</EmailLayout>
);
};
export default NewPollEmail;

View file

@ -0,0 +1,32 @@
import { Heading } from "@react-email/heading";
import { EmailLayout } from "./components/email-layout";
import { Text } from "./components/styled-components";
interface VerificationCodeEmailProps {
name: string;
code: string;
}
export const VerificationCodeEmail = ({
name = "Guest",
code = "123456",
}: VerificationCodeEmailProps) => {
return (
<EmailLayout preview="Here is your 6-digit code">
<Text>Hi {name},</Text>
<Text>Your 6-digit code is:</Text>
<Heading className="font-sans tracking-widest" id="code">
{code}
</Heading>
<Text>
<span className="text-slate-500">
This code is valid for 10 minutes
</span>
</Text>
<Text>Use this code to complete the verification process.</Text>
</EmailLayout>
);
};
export default VerificationCodeEmail;

View file

@ -0,0 +1,5 @@
{
"extends": "@rallly/tsconfig/react-library.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".react-email"]
}

1989
packages/emails/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
{
"name": "@rallly/tailwind-config",
"version": "0.0.0",
"private": true,
"main": "tailwind.config.js",
"types": "tailwind.config.d.ts",
"dependencies": {
"tailwindcss": "^3.2.7"
}
}

View file

@ -0,0 +1 @@
declare module "@rallly/tailwind-config";

View file

@ -0,0 +1,42 @@
const colors = require("tailwindcss/colors");
module.exports = {
theme: {
extend: {
boxShadow: {
huge: "0px 51px 78px rgb(17 7 53 / 5%), 0px 21.3066px 35.4944px rgb(17 7 53 / 4%), 0px 11.3915px 18.9418px rgb(17 7 53 / 3%), 0px 6.38599px 9.8801px rgb(17 7 53 / 3%), 0px 3.39155px 4.58665px rgb(17 7 53 / 2%), 0px 1.4113px 1.55262px rgb(17 7 53 / 1%), inset 0px 1px 0px rgb(41 56 78 / 5%)",
},
colors: {
primary: colors.indigo,
},
keyframes: {
wiggle: {
"0%, 100%": { transform: "rotate(-1deg)" },
"50%": { transform: "rotate(1deg)" },
},
popIn: {
"0%": {
transform: "scale(0.8) translateY(-10px)",
opacity: "0",
},
"100%": {
transform: "scale(1) translateY(0px)",
opacity: "1",
translateY: "0",
},
},
},
animation: {
wiggle: "wiggle 0.2s ease-in-out",
popIn: "popIn 0.1s ease-out",
},
screens: {
xs: "375px",
},
transitionTimingFunction: {
"in-expo": "cubic-bezier(0.68, -0.6, 0.32, 1.6)",
"out-expo": "cubic-bezier(0.19, 1, 0.22, 1)",
},
},
},
};

View file

@ -16,5 +16,6 @@
"skipLibCheck": true,
"strict": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2015"],
"module": "ESNext",
"target": "es6"
}
}

1
packages/utils/index.ts Normal file
View file

@ -0,0 +1 @@
export * from "./src/absolute-url";

View file

@ -0,0 +1,7 @@
{
"name": "@rallly/utils",
"version": "0.0.0",
"private": true,
"main": "index.ts",
"types": "index.ts"
}

View file

@ -0,0 +1,15 @@
const port = process.env.PORT || 3000;
const getVercelUrl = () => {
return process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: null;
};
export function absoluteUrl(path = "") {
const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ??
getVercelUrl() ??
`http://localhost:${port}`;
return `${baseUrl}${path}`;
}

View file

@ -0,0 +1,4 @@
{
"extends": "@rallly/tsconfig/react-library.json",
"include": ["**/*.ts", "**/*.tsx"]
}

View file

@ -1,5 +1,6 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
@ -7,8 +8,6 @@
"env": [
"ANALYZE",
"API_SECRET",
"CI",
"DATABASE_URL",
"LANDING_PAGE",
"MAINTENANCE_MODE",
"NEXT_PUBLIC_BASE_URL",
@ -37,8 +36,8 @@
"outputs": [],
"env": ["CI"]
},
"generate": {
"dependsOn": ["^generate"]
"db:generate": {
"dependsOn": ["^db:generate"]
},
"db:push": {
"cache": false
@ -51,6 +50,10 @@
},
"lint:tsc": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}

1154
yarn.lock

File diff suppressed because it is too large Load diff