mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
New Startups page (#5885)
* new startups page * connect hubspot form * create and use hubspot form * hide date placeholder hack * remove old form * fix images * move to /startups/apply * update bootstrapped value * tidied up startups page, application form --------- Co-authored-by: Cory Watilo <cww@watilo.com>
This commit is contained in:
@@ -20,7 +20,7 @@ images:
|
||||
|
||||
import { getImage, GatsbyImage } from 'gatsby-plugin-image'
|
||||
|
||||
<div class="w-[calc(100%_+_4rem)] relative -mx-4 px-8 md:-mx-16 pt-32 pb-48 -mt-32 md:-mt-44">
|
||||
<div class="w-[calc(100%_+_4rem)] relative -mx-4 px-8 md:-mx-16 pt-32 pb-72 -mt-32 md:-mt-44">
|
||||
<div class="md:hidden absolute top-0 left-0 w-full bottom-0 opacity-40">
|
||||
<GatsbyImage image={getImage(props?.images[0])} alt="PostHog for startups" objectFit="cover" className="" />
|
||||
</div>
|
||||
@@ -40,18 +40,10 @@ import { getImage, GatsbyImage } from 'gatsby-plugin-image'
|
||||
title="PostHog for startups"
|
||||
subtitle="$50k in credits (plus extras you'll actually use) to help you get to product-market fit"
|
||||
ctas={[
|
||||
<CallToAction href="https://app.posthog.com/signup">Get started - free</CallToAction>
|
||||
<CallToAction href="/startups/apply">Apply now</CallToAction>
|
||||
]}
|
||||
/>
|
||||
|
||||
<p class="text-center text-xs p-4 relative">
|
||||
Apply for the program by following three simple steps:
|
||||
<ul class="list-none">
|
||||
<li>1. Create an account using the button above</li>
|
||||
<li>2. Subscribe to the Scale plan on the billing page in-app</li>
|
||||
<li>3. Fill in this <a href="/signup/cloud/startup">form</a> and we will review your application</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,25 +89,25 @@ import { getImage, GatsbyImage } from 'gatsby-plugin-image'
|
||||
|
||||
<section class="my-24 px-5">
|
||||
<div class="max-w-screen-2xl mx-auto">
|
||||
<h3 class="text-2xl lg:text-3xl m-0 text-center mb-6 sm:mb-16">Extra perks</h3>
|
||||
<h3 class="text-2xl lg:text-3xl m-0 text-center mb-6 sm:mb-16 lg:mb-8">Extra perks</h3>
|
||||
<div class="flex justify-center">
|
||||
<ul class="m-0 p-0 list-none inline-grid sm:grid-cols-2 justify-evenly relative after:border-t after:border-dashed after:border-gray-accent-light after:absolute after:w-full after:left-0 after:top-1/2 after:-translateY-1/2 before:absolute before:h-full before:top-0 before:left-1/2 after:-translateX-1/2 before:border-l before:border-dashed before:border-gray-accent-light sm:after:block after:hidden sm:before:block before:hidden">
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 sm:border-b-0 border-b border-dashed border-gray-accent-light last:border-b-0">
|
||||
<ul class="m-0 p-0 list-none inline-grid sm:grid-cols-2 lg:grid-cols-4 justify-evenly relative after:border-t lg:after:border-t-0 after:border-dashed after:border-gray-accent-light after:absolute after:w-full after:left-0 after:top-1/2 after:-translateY-1/2 before:absolute before:h-full before:top-0 before:left-1/2 after:-translateX-1/2 before:border-l lg:before:border-0 before:border-dashed before:border-gray-accent-light sm:after:block after:hidden sm:before:block lg:before:hidden lg:after:hidden before:hidden">
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 lg:px-8 sm:border-b-0 border-b lg:border-b-0 border-dashed border-gray-accent-light last:border-b-0">
|
||||
<GatsbyImage image={getImage(props?.images[5])} alt="Startup spotlight" objectFit="contain" style={{ maxWidth: 200 }} />
|
||||
<h5 class="text-xl font-extrabold m-0 pb-1 pr-4">Startup spotlight</h5>
|
||||
<p class="m-0 text-[15px]">Every month, we will pick a startup of the month to spotlight. We'll do a short interview with you which we'll share on posthog.com and our socials.</p>
|
||||
</li>
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 sm:border-b-0 border-b border-dashed border-gray-accent-light last:border-b-0">
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 lg:px-8 sm:border-b-0 border-b lg:border-b-0 border-dashed border-gray-accent-light last:border-b-0">
|
||||
<GatsbyImage image={getImage(props?.images[6])} alt="Startup merch" objectFit="contain" style={{ maxWidth: 200 }} />
|
||||
<h5 class="text-xl font-extrabold m-0 pb-1 pr-4">Free PostHog merch</h5>
|
||||
<p class="m-0 text-[15px]">Because you can never have too much</p>
|
||||
</li>
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 sm:border-b-0 border-b border-dashed border-gray-accent-light last:border-b-0">
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 lg:px-8 sm:border-b-0 border-b lg:border-b-0 border-dashed border-gray-accent-light last:border-b-0">
|
||||
<GatsbyImage image={getImage(props?.images[7])} alt="Startup launch" objectFit="contain" style={{ maxWidth: 200 }} />
|
||||
<h5 class="text-xl font-extrabold m-0 pb-1 pr-4">We'll promote your launch</h5>
|
||||
<p class="m-0 text-[15px]">Doing a launch? Let us know and we'll retweet, upvote you on ProductHunt, and generally spread the word.</p>
|
||||
</li>
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 sm:border-b-0 border-b border-dashed border-gray-accent-light last:border-b-0">
|
||||
<li class="relative md:max-w-md py-4 md:py-8 pl-4 pr-2 md:px-12 lg:px-8 sm:border-b-0 border-b lg:border-b-0 border-dashed border-gray-accent-light last:border-b-0">
|
||||
<GatsbyImage image={getImage(props?.images[8])} alt="Startup credit" objectFit="contain" style={{ maxWidth: 200 }} />
|
||||
<h5 class="text-xl font-extrabold m-0 pb-1 pr-4">Extra credit</h5>
|
||||
<p class="m-0 text-[15px]">Send this page to your investor or accelerator where they can apply to be a partner. If they sign up, we'll double your credits <em>and</em> send you a custom pair of PostHog AirPods.</p>
|
||||
@@ -131,14 +123,14 @@ import { getImage, GatsbyImage } from 'gatsby-plugin-image'
|
||||
<h3 class="text-2xl sm:text-4xl lg:text-5xl xl:text-6xl m-0 text-center mb-6 sm:mb-8">Docs & resources</h3>
|
||||
<ul class="border border-dashed border-gray-accent-light p-0">
|
||||
<li class="list-none px-4 py-2"><a href="/tracks">PostHog Tracks: How to use PostHog</a></li>
|
||||
<li class="list-none px-4 py-2"><a href="/blog/story-about-pivots">A story about pivots</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/early-stage-analytics">The 80/20 of early-stage startup analytics</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/posthog-first-five">What we learned about hiring from our first five employees</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/how-to-run-a-transparent-company">How to run a transparent startup</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/making-something-people-want">How we made something people want</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/startup-ops-toolkit">The ops toolkit for early-stage startups</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/startup-finance-without-finance">How to run finance at your startup without hiring a finance person</a></li>
|
||||
<li class="list-none px-4 py-2 border-t border-dashed border-gray-accent-light"><a href="/blog/dev-marketing-for-startups">Developer marketing for early-stage startups – what we’ve learned</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold"><a href="/blog/story-about-pivots">A story about pivots</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/early-stage-analytics">The 80/20 of early-stage startup analytics</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/posthog-first-five">What we learned about hiring from our first five employees</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/how-to-run-a-transparent-company">How to run a transparent startup</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/making-something-people-want">How we made something people want</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/startup-ops-toolkit">The ops toolkit for early-stage startups</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/startup-finance-without-finance">How to run finance at your startup without hiring a finance person</a></li>
|
||||
<li class="list-none px-4 py-2 font-semibold border-t border-dashed border-gray-accent-light"><a href="/blog/dev-marketing-for-startups">Developer marketing for early-stage startups – what we’ve learned</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -189,3 +181,10 @@ Because all of those are available if you just go to the company <a href="https:
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="text-center">
|
||||
|
||||
<h2 className="text-3xl lg:text-5xl pb-8">Ready to get started?</h2>
|
||||
<CallToAction href="/startups/apply">Apply now</CallToAction>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,7 @@ hover:text-light-yellow
|
||||
justify-start
|
||||
justify-start
|
||||
left-16
|
||||
lg:before:hidden
|
||||
lg:grid-cols-1
|
||||
lg:grid-cols-2
|
||||
lg:grid-cols-3
|
||||
@@ -65,6 +66,7 @@ lg:grid-rows-1
|
||||
lg:grid-rows-2
|
||||
lg:grid-rows-3
|
||||
lg:grid-rows-4
|
||||
lg:mb-8
|
||||
lg:ml-6
|
||||
lg:my-0
|
||||
max-w-2xl
|
||||
@@ -85,6 +87,7 @@ opacity-40
|
||||
opacity-60
|
||||
opacity-75
|
||||
opacity-100
|
||||
pb-72
|
||||
pb-48
|
||||
pr-32
|
||||
rotate-180
|
||||
|
||||
21
src/api/hubspot-form.ts
Normal file
21
src/api/hubspot-form.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby'
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
const handler = async (req: GatsbyFunctionRequest, res: GatsbyFunctionResponse) => {
|
||||
const { formID } = req.query
|
||||
if (!formID) return res.status(500).send('Missing form ID')
|
||||
|
||||
try {
|
||||
const form = await fetch(`https://api.hubapi.com/forms/v2/forms/${formID}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.HUBSPOT_FORM_ACCESS_TOKEN}`,
|
||||
},
|
||||
}).then((res) => res.json())
|
||||
return res.status(200).send(form)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return res.status(500).send(err)
|
||||
}
|
||||
}
|
||||
|
||||
export default handler
|
||||
412
src/components/HubSpotForm/index.tsx
Normal file
412
src/components/HubSpotForm/index.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import { Form, Formik, useFormikContext } from 'formik'
|
||||
import React, { createContext, InputHTMLAttributes, RefObject, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { button } from 'components/CallToAction'
|
||||
import { useLocation } from '@reach/router'
|
||||
import Confetti from 'react-confetti'
|
||||
import { animateScroll as scroll } from 'react-scroll'
|
||||
|
||||
interface CustomFieldOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
formID: string
|
||||
validationSchema?: any
|
||||
customMessage?: React.ReactNode
|
||||
customFields?: {
|
||||
[key: string]: {
|
||||
type: 'radioGroup'
|
||||
options: CustomFieldOption[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Form {
|
||||
portalId: number
|
||||
guid: string
|
||||
name: string
|
||||
action: string
|
||||
method: string
|
||||
cssClass: string
|
||||
redirect: string
|
||||
submitText: string
|
||||
followUpId: string
|
||||
notifyRecipients: string
|
||||
leadNurturingCampaignId: string
|
||||
formFieldGroups: FormFieldGroup[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
performableHtml: string
|
||||
migratedFrom: string
|
||||
ignoreCurrentValues: boolean
|
||||
metaData: MetaDatum[]
|
||||
deletable: boolean
|
||||
inlineMessage: string
|
||||
tmsId: string
|
||||
captchaEnabled: boolean
|
||||
campaignGuid: string
|
||||
cloneable: boolean
|
||||
editable: boolean
|
||||
formType: string
|
||||
deletedAt: number
|
||||
themeName: string
|
||||
parentId: number
|
||||
style: string
|
||||
isPublished: boolean
|
||||
publishAt: number
|
||||
unpublishAt: number
|
||||
publishedAt: number
|
||||
customUid: string
|
||||
createMarketableContact: boolean
|
||||
editVersion: number
|
||||
thankYouMessageJson: string
|
||||
themeColor: string
|
||||
alwaysCreateNewCompany: boolean
|
||||
internalUpdatedAt: number
|
||||
businessUnitId: number
|
||||
portableKey: string
|
||||
paymentSessionTemplateIds: any[]
|
||||
selectedExternalOptions: any[]
|
||||
embedVersion: string
|
||||
}
|
||||
|
||||
export interface FormFieldGroup {
|
||||
fields: Field[]
|
||||
default: boolean
|
||||
isSmartGroup: boolean
|
||||
richText: RichText
|
||||
isPageBreak: boolean
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
name: string
|
||||
label: string
|
||||
type: string
|
||||
fieldType: string
|
||||
description: string
|
||||
groupName: string
|
||||
displayOrder: number
|
||||
required: boolean
|
||||
selectedOptions: any[]
|
||||
options: any[]
|
||||
validation: Validation
|
||||
enabled: boolean
|
||||
hidden: boolean
|
||||
defaultValue: string
|
||||
isSmartField: boolean
|
||||
unselectedLabel: string
|
||||
placeholder: string
|
||||
dependentFieldFilters: any[]
|
||||
labelHidden: boolean
|
||||
propertyObjectType: string
|
||||
metaData: MetaDatum[]
|
||||
objectTypeId: string
|
||||
}
|
||||
|
||||
export interface MetaDatum {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Validation {
|
||||
name: string
|
||||
message: string
|
||||
data: string
|
||||
useDefaultBlockList: boolean
|
||||
blockedEmailAddresses: any[]
|
||||
}
|
||||
|
||||
export interface RichText {
|
||||
content: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const FormContext = createContext<{
|
||||
fields: Field[]
|
||||
openOptions: string[]
|
||||
setOpenOptions: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}>({
|
||||
fields: [],
|
||||
openOptions: [],
|
||||
setOpenOptions: () => null,
|
||||
})
|
||||
|
||||
function Radio({
|
||||
value,
|
||||
label,
|
||||
name,
|
||||
reference,
|
||||
}: {
|
||||
value: string | number
|
||||
label: string
|
||||
name: string
|
||||
reference?: RefObject<HTMLInputElement>
|
||||
}) {
|
||||
const { fields, openOptions, setOpenOptions } = useContext(FormContext)
|
||||
const { setFieldValue, values } = useFormikContext()
|
||||
|
||||
const handleChange = async (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget
|
||||
name && value && (await setFieldValue(name, value))
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
const nextIndex = fields.findIndex((field) => field.name === name) + 1
|
||||
const nextField = fields[nextIndex]
|
||||
if (nextField && typeof window !== 'undefined') {
|
||||
const nextName = nextField?.name
|
||||
const nextValue =
|
||||
nextField?.options && (nextField?.options[0]?.value || nextField?.options[0]?.hubspotValue)
|
||||
const nextID = `${nextName}${nextValue ? '-' + nextValue : ''}`
|
||||
!openOptions.includes(nextName) && setOpenOptions([...openOptions, nextName])
|
||||
const nextEl = document.getElementById(nextID)
|
||||
if (!values[nextName]) {
|
||||
setTimeout(() => {
|
||||
nextEl?.focus()
|
||||
setFieldValue(nextName, nextValue || '')
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
onMouseUp={handleClick}
|
||||
className="relative w-full text-center cursor-pointer"
|
||||
htmlFor={`${name}-${value}`}
|
||||
>
|
||||
<input
|
||||
checked={values[name] == value}
|
||||
className="absolute opacity-0 peer inset-0"
|
||||
type="radio"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
id={`${name}-${value}`}
|
||||
{...(reference ? { ref: reference } : {})}
|
||||
/>
|
||||
<span className="block py-2 w-full rounded-md border-[2px] border-black/10 peer-focus:border-black/40 peer-checked:!border-black/80 text-sm">
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroup({
|
||||
options,
|
||||
name,
|
||||
placeholder,
|
||||
}: {
|
||||
options: CustomFieldOption[]
|
||||
name: string
|
||||
placeholder: string
|
||||
}) {
|
||||
if (!name) return null
|
||||
const { openOptions, setOpenOptions } = useContext(FormContext)
|
||||
const { errors, values, setFieldValue } = useFormikContext()
|
||||
const error = errors[name]
|
||||
const open = openOptions.includes(name)
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
return (
|
||||
<div
|
||||
onFocus={() => {
|
||||
!openOptions.includes(name) && setOpenOptions([...openOptions, name])
|
||||
}}
|
||||
onClick={() => {
|
||||
if (options && !openOptions.includes(name)) {
|
||||
setOpenOptions([...openOptions, name])
|
||||
if (!values[name]) {
|
||||
ref.current?.focus()
|
||||
setFieldValue(name, options[0]?.value)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`${inputContainerClasses} ${error ? 'pb-8' : ''} cursor-pointer`}
|
||||
>
|
||||
<p className={`m-0 ${open ? 'text-sm opacity-100' : 'opacity-50'} transition-all`} id={`group-${name}`}>
|
||||
{placeholder}
|
||||
</p>
|
||||
<motion.div className="overflow-hidden" animate={{ height: open ? 'auto' : 0 }} initial={{ height: 0 }}>
|
||||
<p className="m-0 mt-1 mb-4 text-xs">
|
||||
<strong>Tip:</strong> Use{' '}
|
||||
<kbd
|
||||
className="text-xs border border-b-2 border-gray-accent-light/50 dark:border-gray-accent-dark/50 rounded-sm px-1.5 py-0.5 text-black/40 dark:text-white/40 font-sans mr-1"
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
←
|
||||
</kbd>
|
||||
<kbd
|
||||
className="text-xs border border-b-2 border-gray-accent-light/50 dark:border-gray-accent-dark/50 rounded-sm px-1.5 py-0.5 text-black/40 dark:text-white/40 font-sans"
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
→
|
||||
</kbd>{' '}
|
||||
to advance through options
|
||||
</p>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby={`group-${name}`}
|
||||
className={`mt-2 grid grid-cols-2 gap-x-2 gap-y-2 ${open ? 'opacity-100' : 'opacity-0 absolute'}`}
|
||||
>
|
||||
{options?.map((option, index) => {
|
||||
const { value, label } = option
|
||||
return (
|
||||
<Radio
|
||||
{...(index === 0 && ref ? { reference: ref } : {})}
|
||||
key={value}
|
||||
value={value}
|
||||
label={label}
|
||||
name={name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
{error && <p className="text-red font-semibold m-0 text-sm absolute bottom-1">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputContainerClasses = `p-4 bg-tan group active:bg-white focus-within:bg-white relative text-left`
|
||||
|
||||
const Input = (props: InputHTMLAttributes<HTMLInputElement>) => {
|
||||
const { name, placeholder } = props
|
||||
if (!name) return null
|
||||
const [type, setType] = useState('text')
|
||||
const { errors, validateField, setFieldValue } = useFormikContext()
|
||||
const error = errors[name]
|
||||
return (
|
||||
<label className={`${inputContainerClasses} ${error ? 'pb-8' : ''}`} htmlFor={props.id}>
|
||||
<input
|
||||
onChange={(e) => setFieldValue(name, e.target.value)}
|
||||
onBlur={() => {
|
||||
validateField(name)
|
||||
setType('text')
|
||||
}}
|
||||
className={`bg-transparent w-full outline-none absolute left-0 px-4 ${
|
||||
error ? 'bottom-6 placeholder-shown:bottom-8' : 'bottom-2 placeholder-shown:bottom-4'
|
||||
} peer placeholder-shown:placeholder-transparent transition-all border-0 py-0 shadow-none ring-0 focus:ring-0`}
|
||||
{...props}
|
||||
onFocus={() => setType(props.type ?? 'text')}
|
||||
type={type}
|
||||
/>
|
||||
<span className="relative -top-3 peer-placeholder-shown:top-0 text-xs peer-placeholder-shown:text-base peer-placeholder-shown:opacity-50 transition-all">
|
||||
{placeholder}
|
||||
</span>
|
||||
{error && <p className="text-red font-semibold m-0 text-sm absolute bottom-1">{error}</p>}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HubSpotForm({ formID, customFields, customMessage, validationSchema }: IProps) {
|
||||
const { href } = useLocation()
|
||||
const [openOptions, setOpenOptions] = useState<string[]>([])
|
||||
const [form, setForm] = useState<{ fields: Field[]; buttonText: string; message: string }>({
|
||||
fields: [],
|
||||
buttonText: '',
|
||||
message: '',
|
||||
})
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [confetti, setConfetti] = useState(true)
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const submission = {
|
||||
pageUri: href,
|
||||
fields: form.fields.map(({ name, objectTypeId }) => {
|
||||
const value = values[name]
|
||||
return {
|
||||
objectTypeId,
|
||||
name,
|
||||
value,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const res = await fetch(`https://api.hsforms.com/submissions/v3/integration/submit/6958578/${formID}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submission),
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
return err
|
||||
})
|
||||
if (res.status === 200) {
|
||||
setSubmitted(true)
|
||||
scroll.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/hubspot-form?formID=${formID}`)
|
||||
.then((res) => res.json())
|
||||
.then((form: Form) => {
|
||||
const fields = form.formFieldGroups
|
||||
.map((group) => {
|
||||
return group.fields
|
||||
})
|
||||
.flat()
|
||||
setForm({
|
||||
fields,
|
||||
buttonText: form.submitText,
|
||||
message: form.inlineMessage,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return form.fields.length > 0 ? (
|
||||
submitted ? (
|
||||
<>
|
||||
{confetti && (
|
||||
<div className="fixed inset-0">
|
||||
<Confetti onConfettiComplete={() => setConfetti(false)} recycle={false} numberOfPieces={1000} />
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-gray-accent-light px-6 py-8 rounded-md mt-4">
|
||||
{customMessage || <p>{form.message}</p>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<FormContext.Provider value={{ fields: form.fields, openOptions, setOpenOptions }}>
|
||||
<Formik
|
||||
validateOnChange={false}
|
||||
validationSchema={validationSchema}
|
||||
initialValues={Object.fromEntries(form.fields.map(({ name }) => [name, '']))}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Form>
|
||||
<div className="grid divide-y divide-dashed divide-gray-accent-light border border-gray-accent-light border-dashed">
|
||||
{form.fields.map(({ name, label, type, required }, index) => {
|
||||
if (customFields && customFields[name])
|
||||
return {
|
||||
radioGroup: (
|
||||
<RadioGroup
|
||||
options={customFields[name].options}
|
||||
name={name}
|
||||
placeholder={label}
|
||||
/>
|
||||
),
|
||||
}[customFields[name]?.type]
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={`${name}-${index}`}
|
||||
type={type === 'string' ? 'text' : type}
|
||||
name={name}
|
||||
placeholder={label}
|
||||
required={required}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button className={button(undefined, 'full', 'mt-4', 'sm')} type="submit">
|
||||
{form.buttonText}
|
||||
</button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</FormContext.Provider>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
BIN
src/components/Startups/images/belay-on.png
Normal file
BIN
src/components/Startups/images/belay-on.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
src/components/Startups/images/on-belay.png
Normal file
BIN
src/components/Startups/images/on-belay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
171
src/components/Startups/index.tsx
Normal file
171
src/components/Startups/index.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Check2 } from 'components/Icons'
|
||||
import Layout from 'components/Layout'
|
||||
import { GatsbyImage, getImage, StaticImage } from 'gatsby-plugin-image'
|
||||
import React from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import Link from 'components/Link'
|
||||
import SEO from 'components/seo'
|
||||
import HubSpotForm from 'components/HubSpotForm'
|
||||
|
||||
const benefits = [
|
||||
'A year of PostHog',
|
||||
'Free PostHog merch',
|
||||
'$50,000 in PostHog credit',
|
||||
'Private office hours',
|
||||
'Startup spotlight',
|
||||
'Opportunities for extra credits',
|
||||
]
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
firstname: Yup.string().required('Please enter your first name'),
|
||||
lastname: Yup.string().required('Please enter your last name'),
|
||||
email: Yup.string().email('Please enter a valid email address').required('Please enter a valid email address'),
|
||||
name: Yup.string().required('Please enter your company name'),
|
||||
domain: Yup.string().required('Please enter your company name'),
|
||||
self_registration_organization_name: Yup.string().required('Please enter your company name'),
|
||||
self_registration_raised: Yup.number().required('Please select a value'),
|
||||
self_registration_company_founded: Yup.string().required('Please enter a date'),
|
||||
})
|
||||
|
||||
const Spotlight = ({ frontmatter: { title, featuredImage }, excerpt, fields: { slug } }) => {
|
||||
return (
|
||||
<div className="p-4 border border-gray-accent-light rounded-md max-w-sm">
|
||||
<h4>{title}</h4>
|
||||
<GatsbyImage className="rounded-md" image={getImage(featuredImage)} />
|
||||
<p className="my-4 text-[15px]">{excerpt}</p>
|
||||
<Link to={slug} external>
|
||||
Read the full story
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Startups() {
|
||||
const { spotlight } = useStaticQuery(graphql`
|
||||
{
|
||||
spotlight: mdx(fields: { slug: { eq: "/blog/startup-tigris" } }) {
|
||||
frontmatter {
|
||||
title
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
excerpt(pruneLength: 200)
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title={'Startups - PostHog'} />
|
||||
<section className="text-center py-20 max-w-screen-lg mx-auto relative px-5">
|
||||
<div className="absolute right-0 -bottom-12 md:-bottom-20 md:max-w-[240px] max-w-[150px]">
|
||||
<StaticImage width={240} src="./images/belay-on.png" />
|
||||
</div>
|
||||
<div className="absolute left-0 bottom-0 max-w-[120px] md:max-w-[200px]">
|
||||
<StaticImage width={200} src="./images/on-belay.png" />
|
||||
</div>
|
||||
<div className="relative hidden lg:block">
|
||||
<h1 className="max-w-lg mx-auto pb-2 text-center">Apply for PostHog's startup program</h1>
|
||||
|
||||
<div className="max-w-sm rounded p-4 text-left bg-gray-accent-light mx-auto">
|
||||
<h3 className="text-lg mb-1">How to apply:</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<Link to="https://app.posthog.com/signup" externalNoIcon>
|
||||
Sign up
|
||||
</Link>{' '}
|
||||
for PostHog Cloud
|
||||
</li>
|
||||
<li>
|
||||
Visit the billing page and upgrade to the <em>Paid</em> plan
|
||||
</li>
|
||||
<li>Complete the application below</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="relative lg:hidden -mb-12">
|
||||
<h1 className="max-w-lg mx-auto pb-2 text-center">Apply for PostHog's startup program</h1>
|
||||
|
||||
<div className="max-w-sm rounded p-4 text-left bg-gray-accent-light mx-auto">
|
||||
<h3 className="text-lg mb-1">How to apply:</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<Link to="https://app.posthog.com/signup" externalNoIcon>
|
||||
Sign up
|
||||
</Link>{' '}
|
||||
for PostHog Cloud
|
||||
</li>
|
||||
<li>
|
||||
Visit the billing page and upgrade to the <em>Paid</em> plan
|
||||
</li>
|
||||
<li>Complete the application below</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="grid md:grid-cols-2 gap-y-8 md:gap-y-0 md:gap-x-12 max-w-[1100px] mx-auto px-5 my-24">
|
||||
<div>
|
||||
<h4>Benefits of joining</h4>
|
||||
<ul className="list-none p-0 m-0 grid grid-flow-row md:grid-cols-2 gap-y-4">
|
||||
{benefits.map((benefit) => {
|
||||
return (
|
||||
<li className="flex items-start space-x-2" key={benefit}>
|
||||
<Check2 className="w-4 opacity-57 flex-shrink-0 mt-[2px]" />
|
||||
<span className="leading-tight text-[15px]">{benefit}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<div className="mt-6 md:mt-8">
|
||||
<Spotlight {...spotlight} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h3 className="mb-0">Finish your application</h3>
|
||||
<p>Remember to complete the steps listed above!</p>
|
||||
<HubSpotForm
|
||||
validationSchema={validationSchema}
|
||||
formID="aa91765b-e790-4e90-847e-46c7ebf43705"
|
||||
customMessage={
|
||||
<>
|
||||
<h4>
|
||||
✅ <strong>Application received!</strong>
|
||||
</h4>
|
||||
<p>We'll get back to you once we've had a chance to review your information. </p>
|
||||
<p>
|
||||
<strong>Reminder:</strong> If you haven't signed up for PostHog yet, be sure to
|
||||
follow steps 1-2 above!
|
||||
</p>
|
||||
<p className="mb-0">
|
||||
In the meantime, why not join <Link to="/slack">our Slack community</Link>?
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
customFields={{
|
||||
self_registration_raised: {
|
||||
type: 'radioGroup',
|
||||
options: [
|
||||
{ label: 'Boostrapped', value: 0 },
|
||||
{ label: 'Under $100k', value: 100_000 },
|
||||
{ label: '$100k - $500k', value: 500_000 },
|
||||
{ label: '$500k - $1m', value: 1_000_000 },
|
||||
{ label: '$1m - $5m', value: 5_000_000 },
|
||||
{ label: 'More than $5m', value: 100_000_000_000 },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// AUTO GENERATED FILE
|
||||
// AUTO GENERATED FILE
|
||||
|
||||
import { ArrayCTA } from './components/ArrayCTA'
|
||||
import { BasicHedgehogImage } from './components/BasicHedgehogImage'
|
||||
@@ -9,6 +9,7 @@ import { CompensationCalculator } from './components/CompensationCalculator'
|
||||
import { FeatureAvailability } from './components/FeatureAvailability'
|
||||
import { GDPRForm } from './components/GDPRForm'
|
||||
import { HiddenSection } from './components/HiddenSection'
|
||||
import { HubSpotForm } from './components/HubSpotForm'
|
||||
import { LPCTA } from './components/LPCTA'
|
||||
import { NewsletterTutorial } from './components/NewsletterTutorial'
|
||||
import { OverflowXSection } from './components/OverflowXSection'
|
||||
@@ -17,25 +18,28 @@ import { ProductLayout } from './components/ProductLayout'
|
||||
import { Quote2 } from './components/Quote2'
|
||||
import { Squeak } from './components/Squeak'
|
||||
import { StarRepoButton } from './components/StarRepoButton'
|
||||
import { Startups } from './components/Startups'
|
||||
import { TracksCTA } from './components/TracksCTA'
|
||||
|
||||
export const shortcodes = {
|
||||
ArrayCTA,
|
||||
BasicHedgehogImage,
|
||||
BorderWrapper,
|
||||
CallToAction,
|
||||
Caption,
|
||||
CompensationCalculator,
|
||||
FeatureAvailability,
|
||||
GDPRForm,
|
||||
HiddenSection,
|
||||
LPCTA,
|
||||
NewsletterTutorial,
|
||||
OverflowXSection,
|
||||
Quote,
|
||||
ProductLayout,
|
||||
Quote2,
|
||||
Squeak,
|
||||
StarRepoButton,
|
||||
TracksCTA
|
||||
}
|
||||
ArrayCTA,
|
||||
BasicHedgehogImage,
|
||||
BorderWrapper,
|
||||
CallToAction,
|
||||
Caption,
|
||||
CompensationCalculator,
|
||||
FeatureAvailability,
|
||||
GDPRForm,
|
||||
HiddenSection,
|
||||
HubSpotForm,
|
||||
LPCTA,
|
||||
NewsletterTutorial,
|
||||
OverflowXSection,
|
||||
Quote,
|
||||
ProductLayout,
|
||||
Quote2,
|
||||
Squeak,
|
||||
StarRepoButton,
|
||||
Startups,
|
||||
TracksCTA,
|
||||
}
|
||||
|
||||
3
src/pages/startups/apply.tsx
Normal file
3
src/pages/startups/apply.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import Startups from 'components/Startups'
|
||||
|
||||
export default Startups
|
||||
@@ -213,7 +213,7 @@ h4 {
|
||||
@apply p-2 pl-4 cursor-pointer border-gray-accent-light dark:border-gray-accent-dark border-dashed border;
|
||||
|
||||
> summary {
|
||||
@apply text-red list-none pl-4 relative before:absolute before:left-[3px] before:top-[8px] before:w-[10px] before:h-[7px];
|
||||
@apply text-red font-semibold list-none pl-4 relative before:absolute before:left-[3px] before:top-[8px] before:w-[10px] before:h-[7px];
|
||||
|
||||
&:before {
|
||||
background: url("data:image/svg+xml,%3Csvg width='10' height='7' viewBox='0 0 10 7' fill='none' xmlns='http://www.w3.org/2000/svg'%0A%3E%3Cpath d='M8.15448 0.316976L5.00049 3.47201L1.8465 0.316976C1.42387 -0.105659 0.73923 -0.105659 0.316596 0.316976C-0.105532 0.739104 -0.105532 1.42425 0.316596 1.84636L4.23586 5.76563C4.65799 6.18726 5.34211 6.18726 5.76421 5.76563L9.68296 1.84688V1.84637C10.1056 1.42425 10.1056 0.740128 9.68347 0.317507C9.26134 -0.105115 8.57722 -0.105128 8.1546 0.317L8.15448 0.316976Z' fill='%23EF7632' /%3E%3C/svg%3E");
|
||||
|
||||
Reference in New Issue
Block a user