Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ A simple **S**ingle **P**age **A**pp for getting funded. Built on

Development and support provided by [Tincre](https://tincre.com).

## Features

- Add your SAFE note, a business email, and [SendGrid](https://sendgrid.com) API key, get on-demand
email delivery of your note for authenticated users plus a notification to your business email.

- Simple content changes maximize efficient customization for your business and your capital raise.

- [Tailwindcss](https://tailwindcss.com) styling to beautifully modify your css right next to your
logic.

- [Next.js](https://nextjs.org) to optimize your engineering on the web.

- [next-auth](https://next-auth.js.org) for build-in security over your walled garden + SEC requirements for **not** marketing private placements (SAFE) to non-accredited investors.

- next-seo for easy and efficient Search Engine Optimization.

- jest/React Testing Library to declaratively test components and page renders.

- Sensible defaults!

## Development Setup

Clone this repo. Then,
Expand Down
45 changes: 34 additions & 11 deletions components/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@ import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { CheckIcon } from "@heroicons/react/outline";

export default function Modal({ title, buttonText, setIsClicked, children }) {
export default function Modal({
title,
buttonText,
setIsClicked,
safeType,
children,
}) {
const [open, setOpen] = useState(true);
const emailMe = async (e, safeType) => {
try {
e.preventDefault();
fetch("/api/email-me", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: safeType,
});
} catch (error) {
console.error(`Something went wrong during email attempt. ${error}`);
}
};

return (
<Transition.Root show={open} as={Fragment}>
Expand Down Expand Up @@ -63,16 +83,19 @@ export default function Modal({ title, buttonText, setIsClicked, children }) {
</div>
</div>
<div className="mt-5 sm:mt-6">
<button
type="button"
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm"
onClick={() => {
setOpen(false);
setIsClicked(true);
}}
>
{buttonText || "Email me this SAFE note"}
</button>
<form>
<button
type="button"
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm"
onClick={(e) => {
setOpen(false);
setIsClicked(true);
emailMe(e, safeType);
}}
>
{buttonText || "Email me this SAFE note"}
</button>
</form>
</div>
</div>
</Transition.Child>
Expand Down
2 changes: 1 addition & 1 deletion components/Sections/SafeTerms.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function SafeTerms({
</div>
</>
) : (
<Modal setIsClicked={setIsClicked}>
<Modal setIsClicked={setIsClicked} safeType={safeType}>
<SafeNote safeType={safeType} />
</Modal>
)}
Expand Down
49 changes: 47 additions & 2 deletions lib/node-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import jwt from "jsonwebtoken";
import sgMail from '@sendgrid/mail';

sgMail.setApiKey(process.env.EMAIL_API_KEY);

/**
* Encode (and sign) a given object as JWT.
*
Expand Down Expand Up @@ -49,5 +53,46 @@ function getCsp(isProd) {
csp += `connect-src 'self' https://vitals.vercel-insights.com`;
return csp;
}

export { getCsp, logApiError, encodeJwt };
/**
* A simple resolver for the safe note file name
*
* @param safeType string "Cap", "Discount", or "MFN"
* @returns string || null
*/
function resolveSafeTypeToFilename(safeType) {
const lower = safeType.toLowerCase();
if (lower.includes('cap')) {
return 'PostmoneySafe-ValuationCapOnlyv1.1-cb4934724e32a5864fd43ca894e51c8c081ecef136409c2e94f00d92168230ec.pdf';
}
if (lower.includes('discount')) {
return 'PostmoneySafe-DiscountOnlyv1.1-8b5fdc03d26538ff24b3336d7a3e8f2b6756eca527a78b1c64fe0dc73904ce3b.pdf';
}
if (lower.includes('mfn')) {
return 'PostmoneySafe-MFNOnlyv1.2-53dee6e87d480c724786fbe5339fa27871989b493bb3a0f73f8c54a953f040bb.pdf';
}
return null;
}
/*
* A function to email a notification to business development.
*
* @param string toEmail
* @param string notificationMessage
*
* @returns Boolean true if the message was sent, false otherwise.
*/
async function sendEmailNotification(toEmail, notificationMessage) {
try {
const msg = {
to: toEmail,
from: process.env.FROM_EMAIL,
subject: `Notification from Phund`,
text: notificationMessage,
};
await sgMail.send(msg);
return true;
} catch (error) {
console.error(error);
return false;
}
}
export { getCsp, logApiError, encodeJwt, resolveSafeTypeToFilename, sendEmailNotification};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@headlessui/react": "^1.4.3",
"@heroicons/react": "^1.0.5",
"@next-auth/prisma-adapter": "^1.0.1",
"@sendgrid/mail": "^7.6.0",
"@tailwindcss/typography": "^0.5.1",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
Expand Down
55 changes: 55 additions & 0 deletions pages/api/email-me.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable import/no-anonymous-default-export */
import sgMail from "@sendgrid/mail";
import fs from "fs";
import {getSession} from "next-auth/react";
import path from "path";
import {resolveSafeTypeToFilename, sendEmailNotification} from '../../lib/node-utils';

sgMail.setApiKey(process.env.EMAIL_API_KEY);
const businessUserEmail = 'jason@tincre.com'
const notificationMessage = 'The following user emailed themselves our SAFE note. You should probably follow up tiger. ;-)\n\n';
export default async (req, res) => {
if (req.method === "POST") {
try {
const session = await getSession({req});
if (!session) {
return res.status(403).json({error : "Not authorized"});
}
const safeType = process.env.SAFE_TYPE;
const filename = resolveSafeTypeToFilename(safeType);
const dirRelativeToPublicFolder = "safes";

const dir = path.resolve("./public", dirRelativeToPublicFolder);
const pathToAttachment = `${dir}/${filename}`;
const attachment = fs.readFileSync(pathToAttachment).toString("base64");
const message = "Hi!\n\nWe're thrilled that you're interested in investing to help us continue our growth. We will reach out shortly.\n\nMeanwhile, please find your requested SAFE note attached.";
const msg = {
to : session?.user?.email,
from : process.env.FROM_EMAIL,
subject : `Your requested SAFE note.`,
text : message,
attachments : [
{
content : attachment,
filename : filename,
type : "application/pdf",
disposition : "attachment",
},
],
};
await sgMail.send(msg);
const notificatWasSent = await sendEmailNotification(businessUserEmail, notificationMessage + ` - Investor email: ${session?.user?.email}`);
if (notificatWasSent) {
res.status(200).json({message: `Email has been sent and the business notified.`});

}
else {
res.status(200).json({message: `Email has been sent`});
}
} catch (error) {
console.error(error);
res.status(500).json({error : "Error sending email"});
}
}
res.end();
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,29 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323"
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==

"@sendgrid/client@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.6.0.tgz#f90cb8759c96e1d90224f29ad98f8fdc2be287f3"
integrity sha512-cpBVZKLlMTO+vpE18krTixubYmZa98oTbLkqBDuTiA3zRkW+urrxg7pDR24TkI35Mid0Zru8jDHwnOiqrXu0TA==
dependencies:
"@sendgrid/helpers" "^7.6.0"
axios "^0.21.4"

"@sendgrid/helpers@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-7.6.0.tgz#b381bfab391bcd66c771811b22bb6bb2d5c1dfc6"
integrity sha512-0uWD+HSXLl4Z/X3cN+UMQC20RE7xwAACgppnfjDyvKG0KvJcUgDGz7HDdQkiMUdcVWfmyk6zKSg7XKfKzBjTwA==
dependencies:
deepmerge "^4.2.2"

"@sendgrid/mail@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-7.6.0.tgz#e74ee30110527feab5d3b83d68af0cd94537f6d2"
integrity sha512-0KdaSZzflJD/vUAZjB3ALBIuaVGoLq22hrb2fvQXZHRepU/yhRNlEOqrr05MfKBnKskzq1blnD1J0fHxiwaolw==
dependencies:
"@sendgrid/client" "^7.6.0"
"@sendgrid/helpers" "^7.6.0"

"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
Expand Down Expand Up @@ -1102,6 +1125,13 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==

axios@^0.21.4:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.14.0"

axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
Expand Down Expand Up @@ -2037,6 +2067,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==

follow-redirects@^1.14.0:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==

form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
Expand Down