mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-04 03:32:12 +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.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ const nextConfig = {
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
transpilePackages: ["@rallly/emails", "@rallly/database"],
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "app",
|
"name": "@rallly/web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
"lint:i18n": "i18n-unused remove-unused",
|
"lint:i18n": "i18n-unused remove-unused",
|
||||||
|
"prettier": "prettier --write ./src",
|
||||||
"test": "PORT=3001 playwright test",
|
"test": "PORT=3001 playwright test",
|
||||||
"test:codegen": "playwright codegen http://localhost:3000",
|
"test:codegen": "playwright codegen http://localhost:3000",
|
||||||
"docker:start": "./scripts/docker-start.sh"
|
"docker:start": "./scripts/docker-start.sh"
|
||||||
|
@ -20,6 +21,8 @@
|
||||||
"@next/bundle-analyzer": "^12.3.4",
|
"@next/bundle-analyzer": "^12.3.4",
|
||||||
"@radix-ui/react-popover": "^1.0.3",
|
"@radix-ui/react-popover": "^1.0.3",
|
||||||
"@rallly/database": "*",
|
"@rallly/database": "*",
|
||||||
|
"@rallly/emails": "*",
|
||||||
|
"@rallly/tailwind-config": "*",
|
||||||
"@sentry/nextjs": "^7.33.0",
|
"@sentry/nextjs": "^7.33.0",
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
@ -33,7 +36,6 @@
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"eta": "^2.0.0",
|
|
||||||
"framer-motion": "^6.5.1",
|
"framer-motion": "^6.5.1",
|
||||||
"i18next": "^22.4.9",
|
"i18next": "^22.4.9",
|
||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
|
@ -43,7 +45,6 @@
|
||||||
"next": "^13.2.1",
|
"next": "^13.2.1",
|
||||||
"next-i18next": "^13.0.3",
|
"next-i18next": "^13.0.3",
|
||||||
"next-seo": "^5.15.0",
|
"next-seo": "^5.15.0",
|
||||||
"nodemailer": "^6.9.0",
|
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"posthog-js": "^1.42.3",
|
"posthog-js": "^1.42.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -68,7 +69,6 @@
|
||||||
"@rallly/tsconfig": "*",
|
"@rallly/tsconfig": "*",
|
||||||
"@types/accept-language-parser": "^1.5.3",
|
"@types/accept-language-parser": "^1.5.3",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/nodemailer": "^6.4.4",
|
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-big-calendar": "^0.31.0",
|
"@types/react-big-calendar": "^0.31.0",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@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="h-full bg-gray-100 p-3 sm:p-8">
|
||||||
<div className="flex h-full items-start justify-center">
|
<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="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">
|
<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="inline-block h-7 text-primary-500" />
|
<Logo className="text-primary-500 inline-block h-7" />
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 sm:p-6">{children}</div>
|
<div className="p-4 sm:p-6">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Logo from "~/public/logo.svg";
|
||||||
export const HomeLink = (props: { className?: string }) => {
|
export const HomeLink = (props: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<Link href="/" className={props.className}>
|
<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>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const MobileNavigation = (props: { className?: string }) => {
|
||||||
type="button"
|
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"
|
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 />
|
<Logo />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -117,9 +117,9 @@ export const MobileNavigation = (props: { className?: string }) => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative shrink-0">
|
<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>
|
||||||
<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}
|
{user.isGuest ? t("app:guest") : user.shortName}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -134,7 +134,7 @@ export const MobileNavigation = (props: { className?: string }) => {
|
||||||
type="button"
|
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"
|
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">
|
<span className="ml-2 hidden sm:block">
|
||||||
{t("app:preferences")}
|
{t("app:preferences")}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VoteType } from "@prisma/client";
|
import { VoteType } from "@rallly/database";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
|
@ -21,25 +21,25 @@ const OpenBeta = () => {
|
||||||
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/lukevella/rallly/issues/new?assignees=&labels=bug&template=---bug-report.md&title="
|
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
|
🐞 Submit a bug report
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/lukevella/rallly/discussions/new/choose"
|
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
|
📢 Open a discussion with the community
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/uzg4ZcHbuM"
|
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
|
💬 Chat on Discord
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="mailto:feedback@rallly.co"
|
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
|
✉️ Send an email
|
||||||
</a>
|
</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 { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
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 { keyBy } from "lodash";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
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 clsx from "clsx";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
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 clsx from "clsx";
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import { useTranslation } from "next-i18next";
|
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 * as React from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VoteType } from "@prisma/client";
|
import { VoteType } from "@rallly/database";
|
||||||
|
|
||||||
export interface ParticipantForm {
|
export interface ParticipantForm {
|
||||||
votes: Array<
|
votes: Array<
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { usePoll } from "../poll-context";
|
||||||
export const UnverifiedPollNotice = () => {
|
export const UnverifiedPollNotice = () => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { poll } = usePoll();
|
const { poll } = usePoll();
|
||||||
const requestVerificationEmail = trpc.polls.verification.request.useMutation(
|
const requestVerificationEmail =
|
||||||
);
|
trpc.polls.verification.request.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 rounded-md border border-amber-200 bg-amber-100 p-3 text-gray-700 shadow-sm">
|
<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 clsx from "clsx";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VoteType } from "@prisma/client";
|
import { VoteType } from "@rallly/database";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ const PopoverContent = React.forwardRef<
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={clsx(
|
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-left": align === "start",
|
||||||
"origin-top-right": align === "end",
|
"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 dayjs from "dayjs";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { parseValue } from "../../utils/date-time-utils";
|
import { parseValue } from "../../utils/date-time-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { mergeRouters, router } from "../trpc";
|
import { mergeRouters, router } from "../trpc";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { login } from "./login";
|
|
||||||
import { polls } from "./polls";
|
import { polls } from "./polls";
|
||||||
import { user } from "./user";
|
import { user } from "./user";
|
||||||
import { whoami } from "./whoami";
|
import { whoami } from "./whoami";
|
||||||
|
@ -11,7 +10,6 @@ export const appRouter = mergeRouters(
|
||||||
auth,
|
auth,
|
||||||
polls,
|
polls,
|
||||||
user,
|
user,
|
||||||
login,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
import { sendEmail } from "@rallly/emails";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 {
|
import {
|
||||||
createToken,
|
createToken,
|
||||||
decryptToken,
|
decryptToken,
|
||||||
|
@ -21,12 +18,10 @@ const sendVerificationEmail = async (
|
||||||
name: string,
|
name: string,
|
||||||
code: string,
|
code: string,
|
||||||
) => {
|
) => {
|
||||||
await sendEmailTemplate({
|
await sendEmail("VerificationCodeEmail", {
|
||||||
to: email,
|
to: email,
|
||||||
subject: `Your 6-digit code is: ${code}`,
|
subject: `Your 6-digit code is: ${code}`,
|
||||||
templateString: emailTemplate,
|
props: {
|
||||||
templateVars: {
|
|
||||||
homePageUrl: absoluteUrl(),
|
|
||||||
code,
|
code,
|
||||||
name,
|
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 { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 { absoluteUrl } from "../../utils/absolute-url";
|
||||||
import { sendEmailTemplate } from "../../utils/api-utils";
|
|
||||||
import { createToken } from "../../utils/auth";
|
import { createToken } from "../../utils/auth";
|
||||||
import { nanoid } from "../../utils/nanoid";
|
import { nanoid } from "../../utils/nanoid";
|
||||||
import { GetPollApiResponse } from "../../utils/trpc/types";
|
import { GetPollApiResponse } from "../../utils/trpc/types";
|
||||||
|
@ -123,6 +120,7 @@ export const polls = router({
|
||||||
authorName: input.user.name,
|
authorName: input.user.name,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
verified: verified,
|
verified: verified,
|
||||||
|
notifications: verified,
|
||||||
adminUrlId,
|
adminUrlId,
|
||||||
participantUrlId: await nanoid(),
|
participantUrlId: await nanoid(),
|
||||||
user: {
|
user: {
|
||||||
|
@ -146,21 +144,17 @@ export const polls = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const homePageUrl = absoluteUrl();
|
const pollUrl = absoluteUrl(`/admin/${adminUrlId}`);
|
||||||
const pollUrl = `${homePageUrl}/admin/${adminUrlId}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (poll.verified) {
|
if (poll.verified) {
|
||||||
await sendEmailTemplate({
|
await sendEmail("NewPollEmail", {
|
||||||
templateString: newVerfiedPollTemplate,
|
|
||||||
to: input.user.email,
|
to: input.user.email,
|
||||||
subject: `Your poll for ${poll.title} has been created`,
|
subject: `Your poll for ${poll.title} has been created`,
|
||||||
templateVars: {
|
props: {
|
||||||
title: poll.title,
|
title: poll.title,
|
||||||
name: input.user.name,
|
name: input.user.name,
|
||||||
pollUrl,
|
adminLink: pollUrl,
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -169,17 +163,14 @@ export const polls = router({
|
||||||
});
|
});
|
||||||
const verifyEmailUrl = `${pollUrl}?code=${verificationCode}`;
|
const verifyEmailUrl = `${pollUrl}?code=${verificationCode}`;
|
||||||
|
|
||||||
await sendEmailTemplate({
|
await sendEmail("NewPollVerificationEmail", {
|
||||||
templateString: newPollTemplate,
|
|
||||||
to: input.user.email,
|
to: input.user.email,
|
||||||
subject: `Your poll for ${poll.title} has been created`,
|
subject: `Your poll for ${poll.title} has been created`,
|
||||||
templateVars: {
|
props: {
|
||||||
title: poll.title,
|
title: poll.title,
|
||||||
name: input.user.name,
|
name: input.user.name,
|
||||||
pollUrl,
|
adminLink: pollUrl,
|
||||||
verifyEmailUrl,
|
verificationLink: verifyEmailUrl,
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { sendNotification } from "../../../utils/api-utils";
|
import { sendNotification } from "../../../utils/api-utils";
|
||||||
import { publicProcedure, router } from "../../trpc";
|
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 dayjs from "dayjs";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { nanoid } from "../../../utils/nanoid";
|
import { nanoid } from "../../../utils/nanoid";
|
||||||
import { publicProcedure, router } from "../../trpc";
|
import { publicProcedure, router } from "../../trpc";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { sendNotification } from "../../../utils/api-utils";
|
import { sendNotification } from "../../../utils/api-utils";
|
||||||
import { publicProcedure, router } from "../../trpc";
|
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 { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
import newPollTemplate from "~/templates/new-poll";
|
|
||||||
|
|
||||||
import { absoluteUrl } from "../../../utils/absolute-url";
|
import { absoluteUrl } from "../../../utils/absolute-url";
|
||||||
import { sendEmailTemplate } from "../../../utils/api-utils";
|
|
||||||
import {
|
import {
|
||||||
createToken,
|
createToken,
|
||||||
decryptToken,
|
decryptToken,
|
||||||
|
@ -39,6 +37,7 @@ export const verification = router({
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
verified: true,
|
verified: true,
|
||||||
|
notifications: true,
|
||||||
},
|
},
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
});
|
});
|
||||||
|
@ -79,24 +78,20 @@ export const verification = router({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const homePageUrl = absoluteUrl();
|
const pollUrl = absoluteUrl(`/admin/${adminUrlId}`);
|
||||||
const pollUrl = `${homePageUrl}/admin/${adminUrlId}`;
|
|
||||||
const token = await createToken({
|
const token = await createToken({
|
||||||
pollId,
|
pollId,
|
||||||
});
|
});
|
||||||
const verifyEmailUrl = `${pollUrl}?code=${token}`;
|
const verifyEmailUrl = `${pollUrl}?code=${token}`;
|
||||||
|
|
||||||
await sendEmailTemplate({
|
await sendEmail("GuestVerifyEmail", {
|
||||||
templateString: newPollTemplate,
|
|
||||||
to: poll.user.email,
|
to: poll.user.email,
|
||||||
subject: "Please verify your email address",
|
subject: "Please verify your email address",
|
||||||
templateVars: {
|
props: {
|
||||||
title: poll.title,
|
title: poll.title,
|
||||||
name: poll.user.name,
|
name: poll.user.name,
|
||||||
pollUrl,
|
adminLink: pollUrl,
|
||||||
verifyEmailUrl,
|
verificationLink: verifyEmailUrl,
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { IronSessionData } from "iron-session";
|
import { IronSessionData } from "iron-session";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { publicProcedure, router } from "../trpc";
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
|
||||||
const requireUser = (user: IronSessionData["user"]) => {
|
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 { createGuestUser, UserSession } from "../../utils/auth";
|
||||||
import { publicProcedure, router } from "../trpc";
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import * as Eta from "eta";
|
import { prisma } from "@rallly/database";
|
||||||
|
import { sendEmail } from "@rallly/emails";
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
import newCommentTemplate from "~/templates/new-comment";
|
|
||||||
import newParticipantTemplate from "~/templates/new-participant";
|
|
||||||
|
|
||||||
import { absoluteUrl } from "./absolute-url";
|
import { absoluteUrl } from "./absolute-url";
|
||||||
import { sendEmail } from "./send-email";
|
|
||||||
|
|
||||||
type NotificationAction =
|
type NotificationAction =
|
||||||
| {
|
| {
|
||||||
|
@ -24,7 +20,19 @@ export const sendNotification = async (
|
||||||
try {
|
try {
|
||||||
const poll = await prisma.poll.findUnique({
|
const poll = await prisma.poll.findUnique({
|
||||||
where: { id: pollId },
|
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:
|
* poll needs to:
|
||||||
|
@ -46,34 +54,28 @@ export const sendNotification = async (
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "newParticipant":
|
case "newParticipant":
|
||||||
await sendEmailTemplate({
|
await sendEmail("NewParticipantEmail", {
|
||||||
templateString: newParticipantTemplate,
|
|
||||||
to: poll.user.email,
|
to: poll.user.email,
|
||||||
subject: `${action.participantName} has shared their availability for ${poll.title}`,
|
subject: `New participant on ${poll.title}`,
|
||||||
templateVars: {
|
props: {
|
||||||
title: poll.title,
|
name: poll.user.name,
|
||||||
name: poll.authorName,
|
|
||||||
participantName: action.participantName,
|
participantName: action.participantName,
|
||||||
pollUrl,
|
pollUrl,
|
||||||
homePageUrl: absoluteUrl(),
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
|
title: poll.title,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "newComment":
|
case "newComment":
|
||||||
await sendEmailTemplate({
|
await sendEmail("NewCommentEmail", {
|
||||||
templateString: newCommentTemplate,
|
|
||||||
to: poll.user.email,
|
to: poll.user.email,
|
||||||
subject: `${action.authorName} has commented on ${poll.title}`,
|
subject: `New comment on ${poll.title}`,
|
||||||
templateVars: {
|
props: {
|
||||||
title: poll.title,
|
name: poll.user.name,
|
||||||
name: poll.authorName,
|
authorName: action.authorName,
|
||||||
author: action.authorName,
|
|
||||||
pollUrl,
|
pollUrl,
|
||||||
homePageUrl: absoluteUrl(),
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
|
title: poll.title,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -83,29 +85,3 @@ export const sendNotification = async (
|
||||||
console.error(e);
|
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 {
|
import {
|
||||||
IronSession,
|
IronSession,
|
||||||
IronSessionOptions,
|
IronSessionOptions,
|
||||||
|
@ -11,8 +12,6 @@ import {
|
||||||
NextApiHandler,
|
NextApiHandler,
|
||||||
} from "next";
|
} from "next";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
import { createSSGHelperFromContext } from "../server/context";
|
import { createSSGHelperFromContext } from "../server/context";
|
||||||
import { randomid } from "./nanoid";
|
import { randomid } from "./nanoid";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Option } from "@prisma/client";
|
import { Option } from "@rallly/database";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import {
|
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 = {
|
export type GetPollApiResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -1,49 +1,15 @@
|
||||||
const colors = require("tailwindcss/colors");
|
|
||||||
const defaultTheme = require("tailwindcss/defaultTheme");
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
const sharedConfig = require("@rallly/tailwind-config/tailwind.config");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
...sharedConfig,
|
||||||
content: ["./src/pages/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"],
|
content: ["./src/pages/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
...sharedConfig.theme,
|
||||||
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",
|
|
||||||
},
|
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
|
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
|
||||||
mono: ["var(--font-noto)", ...defaultTheme.fontFamily.mono],
|
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")],
|
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 { expect, Page, test } from "@playwright/test";
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
import { load } from "cheerio";
|
import { load } from "cheerio";
|
||||||
import smtpTester from "smtp-tester";
|
import smtpTester from "smtp-tester";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
const testUserEmail = "test@example.com";
|
const testUserEmail = "test@example.com";
|
||||||
|
|
||||||
test.describe.serial(() => {
|
test.describe.serial(() => {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma, prisma } from "@rallly/database";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import { prisma } from "@/utils/prisma";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* House keeping policy:
|
* House keeping policy:
|
||||||
* * Demo polls are hard deleted after one day
|
* * Demo polls are hard deleted after one day
|
||||||
|
|
|
@ -1,37 +1,24 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"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": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"~/*": ["./*"]
|
"~/*": ["./*"]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
{
|
"allowJs": true,
|
||||||
"name": "next"
|
"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"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules", "**/.*/", "**/*.js"]
|
"exclude": ["node_modules", "**/*.js"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ services:
|
||||||
rallly:
|
rallly:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
dockerfile: ./apps/web/Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
rallly_db:
|
rallly_db:
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "turbo dev",
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
"db:deploy": "turbo db:deploy",
|
"db:deploy": "turbo db:deploy",
|
||||||
|
"db:generate": "turbo db:generate",
|
||||||
"test": "turbo test",
|
"test": "turbo test",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"lint:tsc": "turbo lint:tsc",
|
"lint:tsc": "turbo lint:tsc",
|
||||||
|
@ -20,5 +22,8 @@
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"turbo": "^1.8.3"
|
"turbo": "^1.8.3"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,17 @@
|
||||||
|
import { PrismaClient } from "@rallly/database";
|
||||||
|
|
||||||
|
import { softDeleteMiddleware } from "./middleware/soft-delete-middleware";
|
||||||
|
|
||||||
export * from "@prisma/client";
|
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,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push --skip-generate",
|
"db:push": "prisma db push --skip-generate",
|
||||||
"db:deploy": "prisma migrate deploy"
|
"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,
|
"skipLibCheck": true,
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"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",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": [".env"],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
|
@ -7,8 +8,6 @@
|
||||||
"env": [
|
"env": [
|
||||||
"ANALYZE",
|
"ANALYZE",
|
||||||
"API_SECRET",
|
"API_SECRET",
|
||||||
"CI",
|
|
||||||
"DATABASE_URL",
|
|
||||||
"LANDING_PAGE",
|
"LANDING_PAGE",
|
||||||
"MAINTENANCE_MODE",
|
"MAINTENANCE_MODE",
|
||||||
"NEXT_PUBLIC_BASE_URL",
|
"NEXT_PUBLIC_BASE_URL",
|
||||||
|
@ -37,8 +36,8 @@
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"env": ["CI"]
|
"env": ["CI"]
|
||||||
},
|
},
|
||||||
"generate": {
|
"db:generate": {
|
||||||
"dependsOn": ["^generate"]
|
"dependsOn": ["^db:generate"]
|
||||||
},
|
},
|
||||||
"db:push": {
|
"db:push": {
|
||||||
"cache": false
|
"cache": false
|
||||||
|
@ -51,6 +50,10 @@
|
||||||
},
|
},
|
||||||
"lint:tsc": {
|
"lint:tsc": {
|
||||||
"outputs": []
|
"outputs": []
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue