diff --git a/package.json b/package.json index 03b6c846..75e229d1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "chakra-react-select": "^3.0.1", "chakra-ui-markdown-renderer": "^4.0.0", "focus-visible": "^5.2.0", + "formidable": "^2.0.1", "framer-motion": "^4", "google-spreadsheet": "^3.2.0", "jsforce": "^1.11.0", @@ -35,6 +36,7 @@ "redaxios": "^0.4.1" }, "devDependencies": { + "@types/formidable": "^2.0.4", "@types/google-spreadsheet": "^3.1.5", "@types/jsforce": "^1.9.37", "@types/mailchimp__mailchimp_marketing": "^3.0.3", diff --git a/src/components/forms/ProjectGrantsForm.tsx b/src/components/forms/ProjectGrantsForm.tsx index b809e2bd..804ccbb8 100644 --- a/src/components/forms/ProjectGrantsForm.tsx +++ b/src/components/forms/ProjectGrantsForm.tsx @@ -47,7 +47,7 @@ import { TOAST_OPTIONS } from '../../constants'; -import { ProjectGrantsFormData, ProposalFile, ReferralSource } from '../../types'; +import { ProjectGrantsFormData, ReferralSource } from '../../types'; import { RemoveIcon } from '../UI/icons'; const MotionBox = motion(Box); @@ -81,26 +81,7 @@ export const ProjectGrantsForm: FC = () => { setSelectedFile(file); - const reader = new FileReader(); - // we have to encode the file content as base64 to be able to upload it to SF - reader.readAsDataURL(file); - - reader.onabort = () => console.log('File reading was aborted.'); - reader.onerror = () => console.error('File reading has failed.'); - reader.onload = () => { - const base64 = reader.result as string; - - const uploadedFile: ProposalFile = { - name: file.name, - type: file.type, - size: file.size, - // `data:*/*;base64,` needs to be removed to retrieve the base64 encoded string only - content: base64.split('base64,')[1] as string, - path: file.path - }; - - setValue('uploadProposal', uploadedFile, { shouldValidate: true }); - }; + setValue('uploadProposal', file, { shouldValidate: true }); toast({ ...TOAST_OPTIONS, @@ -862,7 +843,6 @@ export const ProjectGrantsForm: FC = () => { name='uploadProposal' control={control} rules={{ validate: file => (file ? file.size < MAX_PROPOSAL_FILE_SIZE : true) }} - defaultValue={null} render={({ field: { onChange } }) => ( diff --git a/src/components/forms/api.ts b/src/components/forms/api.ts index c6b0ebd7..2443496d 100644 --- a/src/components/forms/api.ts +++ b/src/components/forms/api.ts @@ -44,19 +44,27 @@ export const api = { }, projectGrants: { submit: (data: ProjectGrantsFormData) => { + const curatedData: { [key: string]: any } = { + ...data, + // Company is a required field in SF, we're using the Name as default value if no company provided + company: data.company === 'N/A' ? `${data.firstName} ${data.lastName}` : data.company, + website: getWebsite(data.website), + github: getGitHub(data.github), + projectCategory: data.projectCategory.value, + country: data.country.value, + timezone: data.timezone.value, + howDidYouHearAboutESP: data.howDidYouHearAboutESP.value + }; + + const formData = new FormData(); + + for (const name in data) { + formData.append(name, curatedData[name]); + } + const projectGrantsRequestOptions: RequestInit = { - ...methodOptions, - body: JSON.stringify({ - ...data, - // Company is a required field in SF, we're using the Name as default value if no company provided - company: data.company === 'N/A' ? `${data.firstName} ${data.lastName}` : data.company, - website: getWebsite(data.website), - github: getGitHub(data.github), - projectCategory: data.projectCategory.value, - country: data.country.value, - timezone: data.timezone.value, - howDidYouHearAboutESP: data.howDidYouHearAboutESP.value - }) + method: 'POST', + body: formData }; return fetch(API_PROJECT_GRANTS, projectGrantsRequestOptions); diff --git a/src/pages/api/project-grants.ts b/src/pages/api/project-grants.ts index 3132ff2d..cbfd398c 100644 --- a/src/pages/api/project-grants.ts +++ b/src/pages/api/project-grants.ts @@ -1,68 +1,72 @@ +import fs from 'fs'; import jsforce from 'jsforce'; import { NextApiRequest, NextApiResponse } from 'next'; +import formidable, { File } from 'formidable'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { body } = req; - const { - firstName: FirstName, - lastName: LastName, - email: Email, - company: Company, - projectName: Project_Name__c, - website: Website, - github: Github_Link__c, - twitter: Twitter__c, - teamProfile: Team_Profile__c, - projectDescription: Project_Description__c, - projectCategory: Category__c, - requestedAmount: Requested_Amount__c, - city: npsp__CompanyCity__c, - country: npsp__CompanyCountry__c, - timezone: Time_Zone__c, - howDidYouHearAboutESP: Referral_Source__c, - referralSourceIfOther: Referral_Source_if_Other__c, - referrals: Referrals__c, - uploadProposal - } = body; - const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } = - process.env; - - const conn = new jsforce.Connection({ - // you can change loginUrl to connect to sandbox or prerelease env. - loginUrl: SF_PROD_LOGIN_URL - }); + const form = formidable({}); - conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => { + form.parse(req, (err, fields, files) => { if (err) { - return console.error(err); + res.status(400).json({ status: 'fail' }); + return; } - let createdLeadID: string; - - // Single record creation - conn.sobject('Lead').create( - { - FirstName: FirstName.trim(), - LastName: LastName.trim(), - Email: Email.trim(), - Company: Company.trim(), - Project_Name__c: Project_Name__c.trim(), - Website: Website.trim(), - Github_Link__c: Github_Link__c.trim(), - Twitter__c: Twitter__c.trim(), - Team_Profile__c: Team_Profile__c.trim(), - Project_Description__c: Project_Description__c.trim(), - Category__c: Category__c.trim(), - Requested_Amount__c: Requested_Amount__c.trim(), - npsp__CompanyCity__c: npsp__CompanyCity__c.trim(), - npsp__CompanyCountry__c: npsp__CompanyCountry__c.trim(), - Time_Zone__c: Time_Zone__c.trim(), - Referral_Source__c: Referral_Source__c.trim(), - Referral_Source_if_Other__c: Referral_Source_if_Other__c.trim(), - Referrals__c: Referrals__c.trim(), - RecordTypeId: process.env.SF_RECORD_TYPE_PROJECT_GRANTS - }, - (err, ret) => { + const fieldsSanitized = Object.keys(fields).reduce((prev, key) => { + let value = fields[key]; + if (typeof value === 'string') { + value = value.trim(); + } + + return { + ...prev, + [key]: value + }; + }, {}); + + const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } = + process.env; + + const conn = new jsforce.Connection({ + // you can change loginUrl to connect to sandbox or prerelease env. + loginUrl: SF_PROD_LOGIN_URL + }); + + const application = { + FirstName: fieldsSanitized.firstName, + LastName: fieldsSanitized.lastName, + Email: fieldsSanitized.email, + Company: fieldsSanitized.company, + Project_Name__c: fieldsSanitized.projectName, + Website: fieldsSanitized.website, + Github_Link__c: fieldsSanitized.github, + Twitter__c: fieldsSanitized.twitter, + Team_Profile__c: fieldsSanitized.teamProfile, + Project_Description__c: fieldsSanitized.projectDescription, + Category__c: fieldsSanitized.projectCategory, + Requested_Amount__c: fieldsSanitized.requestedAmount, + npsp__CompanyCity__c: fieldsSanitized.city, + npsp__CompanyCountry__c: fieldsSanitized.country, + Time_Zone__c: fieldsSanitized.timezone, + Referral_Source__c: fieldsSanitized.howDidYouHearAboutESP, + Referral_Source_if_Other__c: fieldsSanitized.referralSourceIfOther, + Referrals__c: fieldsSanitized.referrals, + RecordTypeId: process.env.SF_RECORD_TYPE_PROJECT_GRANTS + }; + + res.status(200).json({ status: 'ok' }); + + conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => { + if (err) { + return console.error(err); + } + + let createdLeadID: string; + + res.status(200).json({ status: 'ok' }); + + // Single record creation + conn.sobject('Lead').create(application, (err, ret) => { if (err || !ret.success) { console.error(err); res.status(400).json({ status: 'fail' }); @@ -72,13 +76,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) createdLeadID = ret.id; console.log({ createdLeadID }); + const uploadProposal = files.uploadProposal as File; + console.log({ uploadProposal }); + if (uploadProposal) { + let uploadProposalContent; + try { + // turn file into base64 encoding + uploadProposalContent = fs.readFileSync(uploadProposal.filepath, { + encoding: 'base64' + }); + } catch (error) { + console.error(error); + res.status(500).json({ status: 'fail' }); + return; + } + // Document upload conn.sobject('ContentVersion').create( { - Title: `[PROPOSAL] ${Project_Name__c} - ${createdLeadID}`, - PathOnClient: uploadProposal.path, - VersionData: uploadProposal.content // base64 encoded file content + Title: `[PROPOSAL] ${application.Project_Name__c} - ${createdLeadID}`, + PathOnClient: uploadProposal.originalFilename, + VersionData: uploadProposalContent // base64 encoded file content }, async (err, uploadedFile) => { if (err || !uploadedFile.success) { @@ -108,15 +127,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.status(200).json({ status: 'ok' }); } - } - ); + }); + }); }); } export const config = { api: { - bodyParser: { - sizeLimit: '4mb' - } + bodyParser: false } }; diff --git a/src/types.ts b/src/types.ts index d1c773f9..7b46be50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,14 +21,6 @@ export interface GranteeFinanceAPIMap { [preference: string]: string; } -export type ProposalFile = { - name: string; - type: string; - size: number; - content: string; - path: string; -} | null; - export type NewsletterFormData = { email: string; }; @@ -52,7 +44,7 @@ export type ProjectGrantsFormData = { howDidYouHearAboutESP: ReferralSource; // SF API: Referral_Source__c referralSourceIfOther: string; // SF API: Referral_Source_if_Other__c referrals: string; // SF API: Referrals__c - uploadProposal: ProposalFile; + uploadProposal: File; }; export type SmallGrantsFormData = { diff --git a/yarn.lock b/yarn.lock index 45bc4918..54edbfe6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1014,6 +1014,13 @@ dependencies: "@types/ms" "*" +"@types/formidable@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-2.0.4.tgz#fdda9c4952ac7c7866485fea8e15e2cbcebfcc44" + integrity sha512-6HYcnmBCeby/nNGgX9kq1DxUpK2UcB3yoHCr3GzFjjqkpivOdcBSbsXP9NbxLcPEi11Fl/L41rbFCIsteF9sbg== + dependencies: + "@types/node" "*" + "@types/google-spreadsheet@^3.1.5": version "3.1.5" resolved "https://registry.yarnpkg.com/@types/google-spreadsheet/-/google-spreadsheet-3.1.5.tgz#2bdc6f9f5372551e0506cb6ef3f562adcf44fc2e" @@ -1313,7 +1320,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@*, asap@~2.0.3: +asap@*, asap@^2.0.0, asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -2005,6 +2012,14 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diff@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" @@ -2597,6 +2612,16 @@ formidable@^1.1.1: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + framer-motion@^4: version "4.1.17" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.17.tgz#4029469252a62ea599902e5a92b537120cc89721" @@ -2873,6 +2898,11 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hey-listen@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" @@ -3968,7 +3998,7 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -once@^1.3.0: +once@1.4.0, once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -4253,6 +4283,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@^6.5.1: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"