mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-02 10:41:54 +02:00
♻️ Refactor email templating code (#533)
This commit is contained in:
parent
0a836aeec7
commit
309cb109aa
79 changed files with 3926 additions and 1455 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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$/,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VoteType } from "@prisma/client";
|
||||
import { VoteType } from "@rallly/database";
|
||||
|
||||
export interface ParticipantForm {
|
||||
votes: Array<
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VoteType } from "@prisma/client";
|
||||
import { VoteType } from "@rallly/database";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VoteType } from "@prisma/client";
|
||||
import { VoteType } from "@rallly/database";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -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"]) => {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Option } from "@prisma/client";
|
||||
import { Option } from "@rallly/database";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { Option, User } from "@prisma/client";
|
||||
import { Option, User } from "@rallly/database";
|
||||
|
||||
export type GetPollApiResponse = {
|
||||
id: string;
|
||||
|
|
|
@ -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")],
|
||||
};
|
||||
|
|
|
@ -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.͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ‌  ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ‌  ͏ ͏ ͏ ͏
|
||||
͏
|
||||
</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;
|
||||
"
|
||||
>
|
||||
‌
|
||||
</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
|
||||
>
|
||||
•
|
||||
<a
|
||||
href="https://twitter.com/ralllyco"
|
||||
class="hover-underline"
|
||||
style="color: #6366f1; text-decoration-line: none"
|
||||
>Twitter</a
|
||||
>
|
||||
•
|
||||
<a
|
||||
href="https://github.com/lukevella/rallly"
|
||||
class="hover-underline"
|
||||
style="color: #6366f1; text-decoration-line: none"
|
||||
>Github</a
|
||||
>
|
||||
•
|
||||
<a
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
|
||||
class="hover-underline"
|
||||
style="color: #6366f1; text-decoration-line: none"
|
||||
>Donate</a
|
||||
>
|
||||
•
|
||||
<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;
|
|
@ -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.͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</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;"> </i><![endif]-->
|
||||
<span style="mso-text-raise: 16px">Log me in →
|
||||
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;"> </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;">
|
||||
‌
|
||||
</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>
|
||||
•
|
||||
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
|
||||
•
|
||||
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
|
||||
•
|
||||
<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;
|
|
@ -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͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</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 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;"> </i><![endif]-->
|
||||
<span style="mso-text-raise: 16px">Go to poll →
|
||||
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;"> </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;">
|
||||
‌
|
||||
</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>
|
||||
•
|
||||
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
|
||||
•
|
||||
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
|
||||
•
|
||||
<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;
|
|
@ -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͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</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 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;"> </i><![endif]-->
|
||||
<span style="mso-text-raise: 16px">Go to poll →
|
||||
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;"> </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;">
|
||||
‌
|
||||
</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>
|
||||
•
|
||||
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
|
||||
•
|
||||
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
|
||||
•
|
||||
<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;
|
|
@ -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!͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</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;"> </i><![endif]-->
|
||||
<span style="mso-text-raise: 16px">Go to poll →
|
||||
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;"> </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;">
|
||||
‌
|
||||
</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>
|
||||
•
|
||||
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
|
||||
•
|
||||
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
|
||||
•
|
||||
<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;
|
|
@ -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!͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</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 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;"> </i><![endif]-->
|
||||
<span style="mso-text-raise: 16px">Verify your email →
|
||||
</span> <!--[if mso]><i style="letter-spacing: 27px; mso-font-width: -100%;"> </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;">
|
||||
‌
|
||||
</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>
|
||||
•
|
||||
<a href="https://twitter.com/ralllyco" class="hover-underline" style="color: #6366f1; text-decoration: none;">Twitter</a>
|
||||
•
|
||||
<a href="https://github.com/lukevella/rallly" class="hover-underline" style="color: #6366f1; text-decoration: none;">Github</a>
|
||||
•
|
||||
<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;
|
|
@ -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(() => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ services:
|
|||
rallly:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/web/Dockerfile
|
||||
restart: always
|
||||
depends_on:
|
||||
rallly_db:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/base.json",
|
||||
"includes": ["**/*.ts"]
|
||||
}
|
2
packages/emails/.gitignore
vendored
Normal file
2
packages/emails/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.react-email
|
||||
/out
|
24
packages/emails/package.json
Normal file
24
packages/emails/package.json
Normal 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
27
packages/emails/readme.md
Normal 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
|
1
packages/emails/src/index.tsx
Normal file
1
packages/emails/src/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./send-email";
|
64
packages/emails/src/send-email.tsx
Normal file
64
packages/emails/src/send-email.tsx
Normal 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?.();
|
||||
}
|
||||
};
|
6
packages/emails/src/templates.ts
Normal file
6
packages/emails/src/templates.ts
Normal 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";
|
162
packages/emails/src/templates/components/email-layout.tsx
Normal file
162
packages/emails/src/templates/components/email-layout.tsx
Normal 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>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://twitter.com/ralllyco"
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://github.com/lukevella/rallly"
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
|
||||
>
|
||||
Donate
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href={`mailto:${process.env.SUPPORT_EMAIL}`}
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
34
packages/emails/src/templates/components/new-poll-base.tsx
Normal file
34
packages/emails/src/templates/components/new-poll-base.tsx
Normal 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>"{title}"</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> →
|
||||
</Link>
|
||||
</Text>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
};
|
|
@ -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)} />
|
||||
);
|
||||
};
|
41
packages/emails/src/templates/guest-verify-email.tsx
Normal file
41
packages/emails/src/templates/guest-verify-email.tsx
Normal 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>"{title}"</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 →
|
||||
</Button>
|
||||
</Section>
|
||||
</Container>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestVerifyEmail;
|
37
packages/emails/src/templates/new-comment.tsx
Normal file
37
packages/emails/src/templates/new-comment.tsx
Normal 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 →</Button>
|
||||
</Section>
|
||||
<Text>
|
||||
<Link href={unsubscribeUrl}>
|
||||
Stop receiving notifications for this poll.
|
||||
</Link>
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCommentEmail;
|
40
packages/emails/src/templates/new-participant.tsx
Normal file
40
packages/emails/src/templates/new-participant.tsx
Normal 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 →</Button>
|
||||
</Section>
|
||||
<Text>
|
||||
<Link href={unsubscribeUrl}>
|
||||
Stop receiving notifications for this poll.
|
||||
</Link>
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewParticipantEmail;
|
37
packages/emails/src/templates/new-poll-verification.tsx
Normal file
37
packages/emails/src/templates/new-poll-verification.tsx
Normal 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 →
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
</NewPollBaseEmail>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPollVerificationEmail;
|
19
packages/emails/src/templates/new-poll.tsx
Normal file
19
packages/emails/src/templates/new-poll.tsx
Normal 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;
|
32
packages/emails/src/templates/verification-code.tsx
Normal file
32
packages/emails/src/templates/verification-code.tsx
Normal 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;
|
5
packages/emails/tsconfig.json
Normal file
5
packages/emails/tsconfig.json
Normal 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
1989
packages/emails/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
10
packages/tailwind-config/package.json
Normal file
10
packages/tailwind-config/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
packages/tailwind-config/tailwind.config.d.ts
vendored
Normal file
1
packages/tailwind-config/tailwind.config.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "@rallly/tailwind-config";
|
42
packages/tailwind-config/tailwind.config.js
Normal file
42
packages/tailwind-config/tailwind.config.js
Normal 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)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -16,5 +16,6 @@
|
|||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
11
packages/tsconfig/react-library.json
Normal file
11
packages/tsconfig/react-library.json
Normal 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
1
packages/utils/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./src/absolute-url";
|
7
packages/utils/package.json
Normal file
7
packages/utils/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@rallly/utils",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"types": "index.ts"
|
||||
}
|
15
packages/utils/src/absolute-url.ts
Normal file
15
packages/utils/src/absolute-url.ts
Normal 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}`;
|
||||
}
|
4
packages/utils/tsconfig.json
Normal file
4
packages/utils/tsconfig.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/react-library.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
11
turbo.json
11
turbo.json
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue