diff --git a/public/images/pectra-pgr-hero.jpeg b/public/images/pectra-pgr-hero.jpeg new file mode 100644 index 00000000..8ce8ca2a Binary files /dev/null and b/public/images/pectra-pgr-hero.jpeg differ diff --git a/src/components/Banners.tsx b/src/components/Banners.tsx index 327f6f0a..ef6b97ad 100644 --- a/src/components/Banners.tsx +++ b/src/components/Banners.tsx @@ -4,7 +4,7 @@ import { Box, Link } from '@chakra-ui/react'; import { Banner } from './UI'; -import { ACADEMIC_GRANTS_URL } from '../constants'; +import { ACADEMIC_GRANTS_URL, PECTRA_PGR_URL } from '../constants'; export const Banners: FC = () => { const router = useRouter(); @@ -16,6 +16,9 @@ export const Banners: FC = () => { Applications are open for the{' '} Academic Grants Round + {' '} and{' '} + + Pectra Proactive Grant Round {' '} . See the details and apply. diff --git a/src/components/forms/AcademicGrantsForm.tsx b/src/components/forms/AcademicGrantsForm.tsx index ac492093..e145a5ea 100644 --- a/src/components/forms/AcademicGrantsForm.tsx +++ b/src/components/forms/AcademicGrantsForm.tsx @@ -375,7 +375,7 @@ export const AcademicGrantsForm: FC = () => { Estimated grant amount, i.e. USD 50,000. Proposals should include a detailed budget - breakdown for requested amount + breakdown for requested amount. diff --git a/src/components/forms/Forms.tsx b/src/components/forms/Forms.tsx index 82410fad..73b69fa7 100644 --- a/src/components/forms/Forms.tsx +++ b/src/components/forms/Forms.tsx @@ -12,7 +12,8 @@ import { EPFApplicationForm, PSESponsorshipsForm, PSEApplicationForm, - AcademicGrantsForm + AcademicGrantsForm, + PectraPGRForm, } from './'; import { @@ -22,6 +23,7 @@ import { EPF_APPLICATION_APPLY_URL, GRANTEE_FINANCE_URL, OFFICE_HOURS_APPLY_URL, + PECTRA_PGR_APPLY_URL, PROJECT_GRANTS_APPLY_URL, PSE_APPLICATION_APPLY_URL, PSE_SPONSORSHIPS_APPLY_URL, @@ -83,6 +85,11 @@ export const Forms: FC = () => { )} + {router.pathname === PECTRA_PGR_APPLY_URL && ( + + + + )} ); }; diff --git a/src/components/forms/PectraPGRForm.tsx b/src/components/forms/PectraPGRForm.tsx new file mode 100644 index 00000000..274c6987 --- /dev/null +++ b/src/components/forms/PectraPGRForm.tsx @@ -0,0 +1,507 @@ +import { + Box, + Center, + Fade, + Flex, + FormControl, + FormLabel, + Input, + Link, + Radio, + RadioGroup, + Stack, + useToast +} from '@chakra-ui/react' +import { Select } from 'chakra-react-select' +import { FC } from 'react' +import { Controller, FormProvider, useForm } from 'react-hook-form' +import { useRouter } from 'next/router' +import { zodResolver } from '@hookform/resolvers/zod' + +import { DropdownIndicator, PageText } from '../UI' +import { SubmitButton } from '../SubmitButton' +import { Captcha, Field, TextAreaField, TextField, UploadFile } from '.' + +import { api } from './api' + +import { chakraStyles } from './selectStyles' + +import { + COUNTRY_OPTIONS, + FIAT_CURRENCY_OPTIONS, + HOW_DID_YOU_HEAR_ABOUT_GRANTS_WAVE, + INDIVIDUAL, + OTHER, + PECTRA_PGR_PROJECT_CATEGORY_OPTIONS, + TEAM, + TIMEZONE_OPTIONS, +} from './constants' + +import { PECTRA_PGR_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants' + +import { PectraPGRSchema, PectraPGRData } from './schemas/PectraPGR' + +export const PectraPGRForm: FC = () => { + const router = useRouter() + const toast = useToast() + + const methods = useForm({ + mode: 'onBlur', + shouldFocusError: true, + defaultValues: { + individualOrTeam: INDIVIDUAL, + repeatApplicant: false + }, + resolver: zodResolver(PectraPGRSchema) + }) + + const { + handleSubmit, + register, + trigger, + control, + formState: { errors, isSubmitting }, + reset, + watch + } = methods + + const individualOrTeam = watch('individualOrTeam') + const referralSource = watch('referralSource') + + const handleDrop = () => { + toast({ + ...TOAST_OPTIONS, + title: 'Proposal uploaded!', + status: 'success' + }); + } + + const onSubmit = async (data: PectraPGRData) => { + return api.pectraPGR.submit(data).then(res => { + if (res.ok) { + reset() + router.push(PECTRA_PGR_THANK_YOU_PAGE_URL) + } else { + toast({ + ...TOAST_OPTIONS, + title: 'Something went wrong while submitting, please try again.', + status: 'error' + }) + + throw new Error('Network response was not OK') + } + }) + .catch(err => console.error('There has been a problem with your operation: ', err.message)) + } + + return ( + + + + + + + + + + + {!errors?.firstName && !errors?.lastName && ( + + Who is the point of contact for the application? + + )} + + + + + ( + + + + + {INDIVIDUAL} + + + + {TEAM} + + + + + )} + /> + + + + + + + + ( + + { + const value = (option as (typeof COUNTRY_OPTIONS)[number]).value; + onChange(value); + }} + components={{ DropdownIndicator }} + placeholder='Select' + closeMenuOnSelect={true} + selectedOptionColor='brand.option' + chakraStyles={chakraStyles} + /> + + )} + /> + + ( + + { + onChange((option as (typeof TIMEZONE_OPTIONS)[number]).value); + trigger('timezone'); + }} + components={{ DropdownIndicator }} + placeholder='Select' + closeMenuOnSelect={true} + selectedOptionColor='brand.option' + chakraStyles={chakraStyles} + /> + + )} + /> + + + + + + + + + + + Attach a PDF proposal for this scope of work. A proposal template is available{' '} + + here + + > + } + isRequired + onDrop={handleDrop} + mb={8} + /> + + ( + + { + onChange( + (option as (typeof PECTRA_PGR_PROJECT_CATEGORY_OPTIONS)[number]).value + ); + }} + components={{ DropdownIndicator }} + placeholder='Select' + closeMenuOnSelect={true} + selectedOptionColor='brand.option' + chakraStyles={chakraStyles} + /> + + )} + /> + + + + Requested amount + + + + Estimated grant amount, i.e. USD 50,000. Proposals should include a detailed budget + breakdown for requested amount. + + + + ( + + option.value === value)} + options={FIAT_CURRENCY_OPTIONS} + onChange={option => { + onChange((option as (typeof FIAT_CURRENCY_OPTIONS)[number]).value); + }} + components={{ DropdownIndicator }} + placeholder='Select' + closeMenuOnSelect={true} + selectedOptionColor='brand.option' + chakraStyles={chakraStyles} + /> + + )} + /> + + + + + + ( + + + onChange((option as (typeof HOW_DID_YOU_HEAR_ABOUT_GRANTS_WAVE)[number]).value) + } + components={{ DropdownIndicator }} + placeholder='Select' + closeMenuOnSelect={true} + selectedOptionColor='brand.option' + chakraStyles={chakraStyles} + /> + + )} + /> + + + + + + + + + + + + Twitter handle(s) + + + @ + + + + Ex: @mytwitterhandle + + + + + {errors?.twitter && ( + + + {errors?.twitter.message} + + + )} + + + + + + + ( + + onChange(value === 'Yes')} + value={value ? 'Yes' : 'No'} + fontSize='input' + colorScheme='white' + mt={4} + > + + + Yes + + + + No + + + + + )} + /> + + + + + + + + + + + + + + ) +} diff --git a/src/components/forms/api.ts b/src/components/forms/api.ts index 57364c06..a90e35e5 100644 --- a/src/components/forms/api.ts +++ b/src/components/forms/api.ts @@ -17,6 +17,7 @@ import { API_GRANTEE_FINANCE, API_NEWSLETTER_SIGNUP_URL, API_OFFICE_HOURS, + API_PECTRA_PGR, API_PROJECT_GRANTS, API_PSE_SPONSORSHIPS, API_SMALL_GRANTS_EVENT, @@ -29,6 +30,7 @@ import { import type { EPFData } from './schemas/EPFApplication'; import type { PSEData } from './schemas/PSEGrants'; import type { AcademicGrantsData } from './schemas/AcademicGrants'; +import type { PectraPGRData } from './schemas/PectraPGR'; const methodOptions = { method: 'POST', @@ -212,6 +214,23 @@ export const api = { return fetch(API_ACADEMIC_GRANTS, dataRequestOptions); } }, + pectraPGR: { + submit: (data: PectraPGRData) => { + + const curatedData: { [key: string]: any } = { + ...data, + company: data.individualOrTeam === 'Individual' && data.company === '' ? `${data.firstName} ${data.lastName}` : data.company, + } + const formData = createFormData(curatedData); + + const dataRequestOptions: RequestInit = { + method: 'POST', + body: formData + }; + + return fetch(API_PECTRA_PGR, dataRequestOptions); + } + }, newsletter: { submit: (data: NewsletterFormData) => { const newsletterRequestOptions: RequestInit = { diff --git a/src/components/forms/constants.ts b/src/components/forms/constants.ts index 595864b7..29709f8c 100644 --- a/src/components/forms/constants.ts +++ b/src/components/forms/constants.ts @@ -43,6 +43,25 @@ export const ACADEMIC_GRANTS_PROJECT_CATEGORY_OPTIONS = [ { value: 'Other', label: 'Other' } ]; +export const PECTRA_PGR_PROJECT_CATEGORY_OPTIONS = [ + { value: 'Consensus layer', label: 'Consensus layer' }, + { value: 'Core Protocol Development', label: 'Core Protocol Development' }, + { value: 'Cryptography and zero knowledge proofs', label: 'Cryptography and zero knowledge proofs' }, + { value: 'Cybersecurity', label: 'Cybersecurity' }, + { value: 'Economics', label: 'Economics' }, + { value: 'Formal Verification', label: 'Formal Verification' }, + { value: 'General research', label: 'General research' }, + { value: 'Governance', label: 'Governance' }, + { value: 'Government', label: 'Government' }, + { value: 'Healthcare', label: 'Healthcare' }, + { value: 'Layer 2', label: 'Layer 2' }, + { value: 'Maximal Extractable Value (MEV)', label: 'Maximal Extractable Value (MEV)' }, + { value: 'P2P networking', label: 'P2P networking' }, + { value: 'Privacy', label: 'Privacy' }, + { value: 'Society and Regulatory', label: 'Society and Regulatory' }, + { value: 'Other', label: 'Other' } +] + export const DATA_COLLECTION_PROJECT_CATEGORY_OPTIONS = [ { value: 'Community and education', label: 'Community and education' }, { value: 'Data Analysis', label: 'Data Analysis' }, @@ -1662,3 +1681,4 @@ export const API_DATA_COLLECTION_GRANTS = '/api/data-collection-grants'; export const API_EPF_APPLICATION = '/api/epf-application'; export const API_NEWSLETTER_SIGNUP_URL = '/api/newsletter-signup'; export const API_PSE_APPLICATION = '/api/pse-grants'; +export const API_PECTRA_PGR = '/api/pectra-pgr'; diff --git a/src/components/forms/index.ts b/src/components/forms/index.ts index f4f2ec2f..e8b44876 100644 --- a/src/components/forms/index.ts +++ b/src/components/forms/index.ts @@ -11,4 +11,5 @@ export * from './EPFApplicationForm'; export * from './PSEApplicationForm'; export * from './PSESponsorshipsForm'; export * from './AcademicGrantsForm'; +export * from './PectraPGRForm'; export * from './fields'; diff --git a/src/components/forms/schemas/PectraPGR.ts b/src/components/forms/schemas/PectraPGR.ts new file mode 100644 index 00000000..4fed38c6 --- /dev/null +++ b/src/components/forms/schemas/PectraPGR.ts @@ -0,0 +1,77 @@ +import * as z from 'zod'; + +import { stringFieldSchema } from './utils' +import { containURL } from '../../../utils' +import { MAX_PROPOSAL_FILE_SIZE } from '../../../constants' + +const MAX_TEXT_LENGTH = 255 +const MAX_TEXT_AREA_LENGTH = 2000 +const MIN_TEXT_AREA_LENGTH = 500 +const TEXT_AREA_LONG_LENGTH = 32768 + +const ACCEPTED_FILE_TYPES = ['application/pdf'] + +export const PectraPGRSchema = z.object({ + firstName: stringFieldSchema('First name', { min: 1, max: 40 }).refine( + value => !containURL(value), + 'First name cannot contain a URL' + ), + lastName: stringFieldSchema('Last name', { min: 1, max: 80 }).refine( + value => !containURL(value), + 'Last name cannot contain a URL' + ), + email: z.string().email({ message: 'Invalid email address' }), + individualOrTeam: stringFieldSchema( + 'Please select whether you are applying as an individual or a team.', + { min: 1, max: MAX_TEXT_LENGTH } + ), + company: stringFieldSchema('Company name', { max: MAX_TEXT_LENGTH }), + country: stringFieldSchema('Country', { min: 1 }), + timezone: stringFieldSchema('Time zone', { min: 1 }), + projectName: stringFieldSchema('Project name', { min: 1, max: MAX_TEXT_LENGTH }), + projectDescription: stringFieldSchema('Project description', { + min: 1, + max: MIN_TEXT_AREA_LENGTH + }), + impact: stringFieldSchema('Impact', { + min: 1, + max: TEXT_AREA_LONG_LENGTH + }), + howIsItDifferent: stringFieldSchema('How is it different?', { + min: 1, + max: TEXT_AREA_LONG_LENGTH + }), + proposalAttachment: z + .any() + .refine(file => !!file, 'Proposal is required.') + .refine(file => file?.size <= MAX_PROPOSAL_FILE_SIZE, `Max file size is 4MB.`) + .refine( + file => ACCEPTED_FILE_TYPES.includes(file?.type || file?.mimetype), + 'Only .pdf files are accepted.' + ), + projectCategory: stringFieldSchema('Project category', { min: 1 }), + fiatCurrency: stringFieldSchema('Fiat currency', { min: 1 }), + requestAmount: stringFieldSchema('Total budget', { min: 1, max: 20 }), + referralSource: stringFieldSchema('Referral source', { min: 1 }), + referralSourceIfOther: stringFieldSchema('Field', { max: MAX_TEXT_AREA_LENGTH }).optional(), + linkedinProfile: z.union([z.literal(''), z.string().trim().url()]), + twitter: stringFieldSchema('Twitter handle', { max: 16 }).optional(), + website: z.union([z.literal(""), z.string().trim().url()]), + alternativeContact: stringFieldSchema('Alternative contact info', { max: 150 }).optional(), + repeatApplicant: z.boolean(), + additionalInfo: stringFieldSchema('Additional info', { max: MAX_TEXT_AREA_LENGTH }).optional(), + captchaToken: stringFieldSchema('Captcha', { min: 1 }) +}).refine((data) => { + if (data.individualOrTeam === 'Individual') return true + return data.company !== undefined && data.company.trim() !== '' +}, { message: 'Organization name is required', path: ['company'] }) +.refine((data) => { + if (data.individualOrTeam === 'Individual') return true + return data.company.length <= MAX_TEXT_LENGTH +}, { message: 'Organization name cannot exceed 255 characters', path: ['company'] }) +.refine((data) => { + if (data.individualOrTeam === 'Individual') return true + return !containURL(data.company) +}, { message: "Organization name cannot contain a URL", path: ['company'] }) + +export type PectraPGRData = z.infer \ No newline at end of file diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index bc74c01a..c7e7ce94 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -20,7 +20,8 @@ import { DataCollectionLayout, AcademicGrants2023Layout, ZKGrantsLayout, - DataChallengeLayout + DataChallengeLayout, + PectraPGRLayout, } from '../layout'; import { Nav } from '../../components'; @@ -40,7 +41,8 @@ import { DATA_COLLECTION_ROUND_URL, ACADEMIC_GRANTS_2023_URL, ZK_GRANTS_URL, - DATA_CHALLENGE_ROUND_URL + DATA_CHALLENGE_ROUND_URL, + PECTRA_PGR_URL, } from '../../constants'; export const Layout: FC = ({ children, ...props }) => { @@ -198,6 +200,16 @@ export const Layout: FC = ({ children, ...props }) => { ); } + if (router.pathname === PECTRA_PGR_URL) { + return ( + + + {children} + + + ); + } + if (GRANTS_URLS.includes(router.pathname)) { return ( diff --git a/src/components/layout/PectraPGRLayout.tsx b/src/components/layout/PectraPGRLayout.tsx new file mode 100644 index 00000000..09167694 --- /dev/null +++ b/src/components/layout/PectraPGRLayout.tsx @@ -0,0 +1,30 @@ +import { Stack } from '@chakra-ui/react' +import { FC, ReactNode } from 'react'; + +import { GrantsHero } from '../UI'; + +import pectraPGRHero from '../../../public/images/pectra-pgr-hero.jpeg'; + +type Props = { + children: ReactNode; +}; + +export const PectraPGRLayout: FC = ({ children }) => { + return ( + + + The Ethereum Foundation is sponsoring a wave of grants to support Ethereum ecosystem in preparation for the upcoming Pectra network upgrade. This grants round has 200k in total available funds. Proposals are due February 23rd. All of the details you’ll need to apply can be found below. + + + {children} + + ); +}; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 91e06ea2..ed8a47a8 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -13,4 +13,5 @@ export * from './RunANodeGrantLayout'; export * from './DataCollectionLayout'; export * from './ZKGrantsLayout'; export * from './DataChallengeLayout'; +export * from './PectraPGRLayout'; export * from './Layout'; diff --git a/src/constants.ts b/src/constants.ts index 0043228f..aa0c612b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -168,6 +168,19 @@ export const SIDEBAR_ACADEMIC_GRANTS_2024_LINKS: SidebarLink[] = [ { text: 'FAQ', href: `${ACADEMIC_GRANTS_URL_2024}/#faq` } ]; +export const PECTRA_PGR_URL = '/pectra-pgr'; +export const SIDEBAR_PECTRA_PGR_LINKS: SidebarLink[] = [ + { text: "About the Grant Round", href: `${PECTRA_PGR_URL}/#about-the-grant-round` }, + { text: "Key focus areas", href: `${PECTRA_PGR_URL}/#key-focus-areas` }, + { text: "Timeline", href: `${PECTRA_PGR_URL}/#timeline` }, + { text: "Eligibility", href: `${PECTRA_PGR_URL}/#eligibility` }, + { text: "What is NOT eligible", href: `${PECTRA_PGR_URL}/#what-is-not-eligible` }, + { text: "How to apply", href: `${PECTRA_PGR_URL}/#how-to-apply` }, + { text: "Grant size", href: `${PECTRA_PGR_URL}/#grant-size` }, + { text: "Resources", href: `${PECTRA_PGR_URL}/#resources` }, + { text: "Get involved", href: `${PECTRA_PGR_URL}/#get-involved` }, +]; + export const ACCOUNT_ABSTRACTION_GRANTS_URL = '/account-abstraction-grants'; export const SIDEBAR_ACCOUNT_ABSTRACTION_GRANTS_LINKS: SidebarLink[] = [ { text: 'Summary', href: `${ACCOUNT_ABSTRACTION_GRANTS_URL}/#summary` }, @@ -261,6 +274,7 @@ export const DATA_COLLECTION_APPLY_URL = '/data-collection-grants/apply'; export const PSE_SPONSORSHIPS_APPLY_URL = '/pse-sponsorships/apply'; export const PSE_APPLICATION_APPLY_URL = '/pse-grants/apply'; export const EPF_APPLICATION_APPLY_URL = '/epf-application/apply'; +export const PECTRA_PGR_APPLY_URL = '/pectra-pgr/apply'; // grantee finance form export const GRANTEE_FINANCE_URL = '/applicants/grantee-finance'; @@ -278,6 +292,7 @@ export const DATA_COLLECTION_THANK_YOU_PAGE_URL = '/data-collection-grants/thank export const PSE_SPONSORSHIPS_THANK_YOU_PAGE_URL = '/pse-sponsorships/thank-you'; export const EPF_APPLICATION_THANK_YOU_PAGE_URL = '/epf-application/thank-you'; export const PSE_APPLICATION_THANK_YOU_PAGE_URL = '/pse-grants/thank-you'; +export const PECTRA_PGR_THANK_YOU_PAGE_URL = '/pectra-pgr/thank-you'; // ethereum ecosystem export const ETHEREUM_ORG_URL = 'https://ethereum.org/'; @@ -308,6 +323,7 @@ export const SEMAPHORE_GRANT_EMAIL_ADDRESS = 'semaphore-grants@ethereum.org'; export const LAYER_2_GRANTS_EMAIL_ADDRESS = 'layer2grants@ethereum.org'; export const ACCOUNT_ABSTRACTION_GRANTS_EMAIL_ADDRESS = 'account-abstraction@ethereum.org'; export const GRANTS_EMAIL_ADDRESS = 'grant-rounds@ethereum.org'; +export const PECTRA_PGR_EMAIL_ADDRESS = 'pectra-pgr@ethereum.org'; // TODO: CONFIRM EMAIL ADDRESS // applicants tabs export const APPLICANTS_TABS = ['Overview', 'Office Hours', 'Small Grants', 'Project Grants']; diff --git a/src/pages/api/pectra-pgr.ts b/src/pages/api/pectra-pgr.ts new file mode 100644 index 00000000..a40b4573 --- /dev/null +++ b/src/pages/api/pectra-pgr.ts @@ -0,0 +1,153 @@ +import fs from 'fs'; +import jsforce from 'jsforce'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { File } from 'formidable'; + +import { multipartyParse, sanitizeFields, verifyCaptcha } from '../../middlewares'; + +import { PectraPGRSchema } from '../../components/forms/schemas/PectraPGR'; + +import { MAX_PROPOSAL_FILE_SIZE } from '../../constants'; +import { truncateString } from '../../utils/truncateString'; + +async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + return new Promise(resolve => { + const fields = { ...req.fields, ...req.files }; + const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } = + process.env; + + // validate fields against the schema + const result = PectraPGRSchema.safeParse(fields); + if (!result.success) { + const formatted = result.error.format(); + console.error('Validation Error:', formatted); + + res.status(500).end(); + return resolve(); + } + + const conn = new jsforce.Connection({ + // you can change loginUrl to connect to sandbox or prerelease env. + loginUrl: SF_PROD_LOGIN_URL + }); + + conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => { + if (err) { + console.error('Salesforce Login Error:', err); + res.status(500).json({ status: 'fail', message: 'Salesforce login failed.' }); + return resolve(); + } + + // SF mapping + const application = { + FirstName: result.data.firstName, + LastName: result.data.lastName, + Email: result.data.email, + Individual_or_Team__c: result.data.individualOrTeam.trim(), + Company: result.data.company, + npsp__CompanyCountry__c: result.data.country, + Time_Zone__c: result.data.timezone, + Project_Name__c: result.data.projectName, + Project_Description__c: result.data.projectDescription, + Impact__c: result.data.impact, + How_is_it_different__c: result.data.howIsItDifferent, + Category__c: result.data.projectCategory, + CurrencyIsoCode: result.data.fiatCurrency, + Requested_Amount__c: result.data.requestAmount, + Referral_Source__c: result.data.referralSource, + Referral_Source_if_Other__c: result.data.referralSourceIfOther, + LinkedIn_Profile__c: result.data.linkedinProfile, + Twitter__c: result.data.twitter, + Website: result.data.website, + Alternative_Contact__c: result.data.alternativeContact, + Repeat_Applicant__c: result.data.repeatApplicant, + Additional_Information__c: result.data.additionalInfo, + RecordTypeId: process.env.SF_RECORD_TYPE_GRANTS_ROUND!, + Proactive_Community_Grants_Round__c: 'Pectra', + LeadSource: 'Webform' + }; + + // Single record creation + conn.sobject('Lead').create(application, async (err, ret) => { + if (err || !ret.success) { + console.error('Salesforce Lead Creation Error:', err, ret); + res.status(400).json({ status: 'fail', message: 'Failed to create Lead in Salesforce.' }); + return resolve(); + } + + console.log(`Pectra PGR Lead with ID: ${ret.id} has been created!`); + + const createdLeadID = ret.id; + console.log({ createdLeadID }); + + const uploadProposal = result.data.proposalAttachment as File; + console.log({ uploadProposal }); + + if (!uploadProposal) { + res.status(200).json({ status: 'ok' }); + return resolve(); + } + + let uploadProposalContent; + try { + // turn file into base64 encoding + uploadProposalContent = fs.readFileSync(uploadProposal.filepath, { + encoding: 'base64' + }); + } catch (error) { + console.error('File Read Error:', error); + res.status(500).json({ status: 'fail', message: 'Failed to read proposal file.' }); + return resolve(); + } + + // Document upload + conn.sobject('ContentVersion').create( + { + Title: `[PROPOSAL] ${truncateString( + application.Project_Name__c || '', + 200 + )} - ${createdLeadID}`, + PathOnClient: uploadProposal.originalFilename, + VersionData: uploadProposalContent // base64 encoded file content + }, + async (err, uploadedFile) => { + if (err || !uploadedFile.success) { + console.error('Salesforce ContentVersion Upload Error:', err); + res.status(400).json({ status: 'fail', message: 'Failed to upload proposal file.' }); + return resolve(); + } else { + console.log({ uploadedFile }); + console.log(`Document has been uploaded successfully!`); + + const contentDocument = await conn + .sobject<{ + Id: string; + ContentDocumentId: string; + }>('ContentVersion') + .retrieve(uploadedFile.id); + + await conn.sobject('ContentDocumentLink').create({ + ContentDocumentId: contentDocument.ContentDocumentId, + LinkedEntityId: createdLeadID, + ShareType: 'V' + }); + + res.status(200).json({ status: 'ok' }); + return resolve(); + } + } + ); + }); + }); + }) +} + +export const config = { + api: { + bodyParser: false + } + }; + +export default multipartyParse(sanitizeFields(verifyCaptcha(handler)), { + maxFileSize: MAX_PROPOSAL_FILE_SIZE +}); diff --git a/src/pages/pectra-pgr/apply.tsx b/src/pages/pectra-pgr/apply.tsx new file mode 100644 index 00000000..fe34e373 --- /dev/null +++ b/src/pages/pectra-pgr/apply.tsx @@ -0,0 +1,47 @@ +import { Box, Link, Stack } from '@chakra-ui/react' +import type { NextPage } from 'next' + +import { PageMetadata, PageSubheading, PageText } from '../../components/UI' +import { PECTRA_PGR_EMAIL_ADDRESS } from '../../constants' + +const PectraPGRApply: NextPage = () => { + return ( + <> + + + + + + + Apply to Pectra Proactive Grant Round + + + + If you have questions before submitting a grant application, you may contact us at{' '} + + {PECTRA_PGR_EMAIL_ADDRESS} + + . + + + + + > + ) +}; + +export default PectraPGRApply diff --git a/src/pages/pectra-pgr/index.tsx b/src/pages/pectra-pgr/index.tsx new file mode 100644 index 00000000..c1aa4989 --- /dev/null +++ b/src/pages/pectra-pgr/index.tsx @@ -0,0 +1,347 @@ +import { + Box, + Flex, + forwardRef, + Link, + ListItem, + Stack, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react' +import { useInView } from 'react-intersection-observer' +import type { NextPage } from 'next' + +import { + ApplicantsSidebar, + List, + OrderedList, + PageMetadata, + PageSection, + PageText, + ReadyToApply, +} from '../../components/UI' + +import { + PECTRA_PGR_APPLY_URL, + PECTRA_PGR_EMAIL_ADDRESS, + SIDEBAR_PECTRA_PGR_LINKS +} from '../../constants' + +import pectraPGRHero from '../../../public/images/pectra-pgr-hero.jpeg' + +const Section = forwardRef((props, ref) => ( + +)) + +const PectraPGR: NextPage = () => { + const [ref, inView] = useInView({ threshold: 0 }) + const [ref2, inView2] = useInView({ threshold: 0, initialInView: false }) + const [ref3, inView3] = useInView({ threshold: 0.5, initialInView: false }) + const [ref4, inView4] = useInView({ threshold: 0.5, initialInView: false }) + const [ref5, inView5] = useInView({ threshold: 0.3, initialInView: false }) + const [ref6, inView6] = useInView({ threshold: 0.5, initialInView: false }) + const [ref7, inView7] = useInView({ threshold: 0.5, initialInView: false }) + const [ref8, inView8] = useInView({ threshold: 0.5, initialInView: false }) + const [ref9, inView9] = useInView({ threshold: 0.5, initialInView: false }) + const [ref10, inView10] = useInView({ threshold: 0.5, initialInView: false }) + return ( + <> + + + + + + + + + + + About the Grant Round + + The Pectra Proactive Grant Round aims to address the lag in tooling, infrastructure, and ecosystem readiness following major Ethereum upgrades. + + + The Ethereum Foundation recognizes the challenges introduced by delayed updates in essential tooling post-upgrade. This grant round is a targeted initiative to ensure the ecosystem is: + + + + Well-prepared ahead of the Pectra upgrade. + + + Equipped with key tools and infrastructure. + + + Educated about upcoming changes. + + + + By providing funding and fostering proactive preparation, we aim to eliminate reactive, last-minute solutions and promote a culture of early readiness. + + + Read more about the Pectra upgrade + + + + + + + Key focus areas + + Proposals submitted to the Pectra Proactive Grant Round should align with one or more of the following focus areas: + + + Core Protocol Support + + + Development of tooling and libraries that directly support protocol-level changes introduced in Pectra. + + + Creation of infrastructure to ensure seamless integration of Pectra-related updates into the core protocol. + + + + Tooling and Infrastructure + + + + Updates to essential tools for builders, stakers, and end users. + + + Creation of new tools to support EIPs directly tied to Pectra. + + + Testing and Security + + + Enhancements to testing frameworks and infrastructure. + + + Tools that improve network security before and after Pectra upgrades. + + + + Adoption and Impact Analysis + + + + Projects that track and analyze the adoption of changes introduced by Pectra. + + + Tools or frameworks to measure the impact of Pectra related EIPs on the Ethereum ecosystem and protocol. + + + + + Note: We also maintain a wishlist of ideas and priorities for the Pectra upgrade. You are not required to submit a project from the wishlist—it is provided for inspiration. + + + Check out the Pectra wishlist + + + + + + + Timeline + + + + + Milestone + Date + + + + + Proposal Submission Opens + February 3rd + + + Submission Deadline + February 23rd + + + Review and Selection + February 24th - March 9th + + + Grant Awards Announced + March 24th + + + + + + + + + + Eligibility + + If you have an idea that aligns with the key focus area of Pectra Proactive Grant Round, we encourage you to apply! + + + + Projects must be open source with a free and permissive license. + + + Projects must be aligned with the stated goals and wishlist for this round. + + + We accept proposals from individuals, teams, or organizations. + + + We do not fund past work. + + + Builders of any age, origin, identity, or background are welcome to apply. + + + + + + + + What is NOT eligible + + + Anything that is not legal within the jurisdiction where the work is taking place. + + + Financial products (e.g., trading, investment products, lending, betting, etc.). + + + Art projects or social impact projects that don't fit within the scope of this round. + + + Projects requesting retroactive funding + + + + + + + + How to apply + + + Read the + Pectra Proactive Grand Round Proposal Template + + . + + + Submit your proposal following the guidelines. + + + + Start your proposal here. + + + + + + + + Grant size + + Grants will vary based on the project scope and deliverables. Submit a clear budget breakdown and timeline. + + + + + + + Resources + + + + Pectra upgrade overview + + + + + Proposal template + + + + + + + + + Get involved + Have questions? Want to learn more? + + + Reach out to use via Email. + + + + + + + + + + + + + + + > + ) +} + +export default PectraPGR \ No newline at end of file diff --git a/src/pages/pectra-pgr/thank-you.tsx b/src/pages/pectra-pgr/thank-you.tsx new file mode 100644 index 00000000..52e01e95 --- /dev/null +++ b/src/pages/pectra-pgr/thank-you.tsx @@ -0,0 +1,56 @@ +import { Box, Heading, Link, Stack } from '@chakra-ui/react' +import type { NextPage } from 'next' +import Head from 'next/head' + +import { PageMetadata, PageSubheading, PageText } from '../../components/UI' +import { PECTRA_PGR_EMAIL_ADDRESS } from '../../constants' + +const PectraPGRThankYou: NextPage = () => { + return ( + <> + + + + + + + + + + Thank you! + + + + for applying to Pectra Proactive Grant Round + + + + You should receive a confirmation email from us soon. If you have any questions in the + meantime, you can reach us at{' '} + + {PECTRA_PGR_EMAIL_ADDRESS} + + . + + + + + > + ) +} + +export default PectraPGRThankYou \ No newline at end of file diff --git a/src/theme/foundations/colors.ts b/src/theme/foundations/colors.ts index 2a84d638..50d833bf 100644 --- a/src/theme/foundations/colors.ts +++ b/src/theme/foundations/colors.ts @@ -81,6 +81,13 @@ export const colors = { end: 'rgba(235, 209, 251, 0)' } }, + pectraPGRHero: { + titleWhiteBox: 'rgba(255, 255, 255, 0.6)', + bgGradient: { + start: '#cc9eb0', + end: 'rgba(235, 209, 251, 0)' + } + }, accountAbstractionHero: { titleWhiteBox: 'rgba(255, 255, 255, 0.8)', bgGradient: { diff --git a/src/utils/getLayoutHeight.ts b/src/utils/getLayoutHeight.ts index 9c7fdbe0..405f661e 100644 --- a/src/utils/getLayoutHeight.ts +++ b/src/utils/getLayoutHeight.ts @@ -12,7 +12,8 @@ import { ACADEMIC_GRANTS_2023_URL, ZK_GRANTS_URL, DATA_CHALLENGE_ROUND_URL, - EPF_APPLICATION_APPLY_URL + EPF_APPLICATION_APPLY_URL, + PECTRA_PGR_URL, } from '../constants'; export const getLayoutHeight = (path: string) => @@ -31,7 +32,8 @@ export const getLayoutHeight = (path: string) => DATA_COLLECTION_ROUND_URL, ZK_GRANTS_URL, DATA_CHALLENGE_ROUND_URL, - EPF_APPLICATION_APPLY_URL + EPF_APPLICATION_APPLY_URL, + PECTRA_PGR_URL ].includes(path) ? '810px' : '550px'; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index ed470f2e..1c903585 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,5 +1,8 @@ import validator from 'validator'; -export const isURL = (value: string) => validator.isURL(value); +export const isURL = (value: string) => validator.isURL(value, { + protocols: ['https'], + require_protocol: true +}); export const containURL = (value: string) => value.split(' ').some(isURL);