Skip to content
This repository has been archived by the owner on May 24, 2022. It is now read-only.

Add pricing page and components #63

Merged
merged 6 commits into from
Apr 4, 2022
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"eslint-config-next": "^12.1.0",
"mdi-react": "^8.2.0",
"prettier": "^2.5.1",
"react-bootstrap": "1.6.1",
"react-bootstrap": "2.0.0",
bretthayes marked this conversation as resolved.
Show resolved Hide resolved
"react-spring": "^9.4.4",
"sass": "^1.49.7",
"typescript": "4.5.5"
Expand Down
203 changes: 203 additions & 0 deletions src/components/Pricing/PricingPlan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { ReactFragment, FunctionComponent } from 'react'

import { Features, FeatureInfo } from './interfaces'
import { PricingPlanFeature } from './PricingPlanFeature'

const FEATURE_INFO: Record<keyof Features, FeatureInfo> = {
codeSearch: {
label: 'Code search',
description:
'Super-fast, intuitive, and powerful code search across 10,000s of repositories, with smart filters and more',
},
codeIntelligence: {
label: 'Code intelligence',
description: 'Code navigation for 30+ languages, with hovers, definitions, and references across repositories',
},
batchChanges: {
label: 'Batch Changes (available add-on)',
description: 'Apply and track large-scale code changes across all of your repositories and code hosts.',
},
batchChangesTrial: {
label: 'Batch Changes (limited trial)',
description:
'Apply and track large-scale code changes across all of your repositories and code hosts (limited to 5 changesets per batch change).',
},
codeHostIntegration: {
label: '1 code host integration',
description:
'Works with GitHub, GitLab, Bitbucket Server/Cloud, and other popular code hosts (or manually add repositories from any VCS)',
},
api: { label: 'Comprehensive API', description: 'A secure, robust GraphQL API for your repository and code data' },

selfHosted: {
label: 'Self-hosted deployment',
description: 'Deploy with Docker, Docker Compose, or Kubernetes on your own infrastructure',
},
singleSignOn: {
label: 'SSO/SAML',
description: 'Single sign-on user authentication with SAML, OAuth, OpenID Connect, and HTTP auth proxy',
},
userAndAdminRoles: {
label: 'User and admin roles',
description:
'Allow only certain users (site admins) to view and edit site configuration and repository/code host credentials',
},
multipleCodeHosts: {
label: 'Multiple code hosts',
description:
'Sync, search, and browse code from more than one code host on your Sourcegraph instance (such as GitHub.com and GitLab)',
},
repositoryPermissions: {
label: 'Repository permissions',
description:
'Apply the repository permissions from your code host to restrict which repositories a user can search and browse',
},
optimizedRepositoryUpdates: {
label: 'Faster repository updates',
description: "Optimized repository syncing, integrated with your code host's webhooks or event system",
},
privateExtensions: {
label: 'Private extension registry',
description:
'Publish and use internal Sourcegraph extensions (instead of just using the Sourcegraph.com extension registry)',
},
deploymentMetricsAndMonitoring: {
label: 'Deployment monitoring',
description:
'Extensive metrics and dashboards to monitor the performance and health of your Sourcegraph cluster',
},
backupRestore: {
label: 'Backup and restore',
description:
'Officially supported scripts to back up and restore your Sourcegraph instance and all configuration and data',
},
customBranding: {
label: 'Custom branding',
description: 'Show your logo, icon, and other branding in the Sourcegraph UI',
},
onlineTraining: {
label: 'Live training sessions',
description: 'Personalized online training sessions for your organization with our Customer Engineering team',
},
customContractLegalBillingTerms: {
label: 'Custom contracts/billing',
description: "Need us to use your organization's legal contracts or purchasing system?",
},
unlimitedCode: {
label: 'Unlimited code scale',
description:
'Free and Team tiers limit the total amount of searchable code. Enterprise offers options that scale to any size codebase.',
},
managedInstance: {
label: 'Managed instance (available add-on)',
description:
'Managed instances are provisioned and managed by the Sourcegraph team so you can deploy Sourcegraph without having to worry about managing it.',
},
codeInsights: {
label: 'Code Insights (available add-on)',
description: 'Track and visualize trends in your entire codebase — kept automatically up to date.',
},
codeInsightsTrial: {
label: 'Code Insights (limited trial)',
description: `Track and visualize trends in your entire codebase — with visualizations that are kept automatically up to date
(limited to maximum of two global insights without a license).`,
},
}

const FEATURE_ORDER: (keyof Features)[] = [
'codeSearch',
'codeIntelligence',
'codeHostIntegration',
'api',
'selfHosted',
'multipleCodeHosts',
'unlimitedCode',
'repositoryPermissions',
'userAndAdminRoles',
'batchChanges',
'batchChangesTrial',
'codeInsights',
'codeInsightsTrial',
'singleSignOn',
'optimizedRepositoryUpdates',
'deploymentMetricsAndMonitoring',
'privateExtensions',
'backupRestore',
'onlineTraining',
'managedInstance',
'customBranding',
'customContractLegalBillingTerms',
]

interface Props {
className?: string

name: string
planProperties: ReactFragment
price: ReactFragment
features: Features

isFree: boolean

buttonLabel: string
buttonClassName: string
buttonOnClick?: () => void
buttonHref: string
}

/**
* A pricing plan on the pricing page.
*/
export const PricingPlan: FunctionComponent<Props> = ({
className = '',

name,
price,
planProperties,
features,

isFree,

buttonLabel,
buttonClassName,
buttonOnClick,
buttonHref,
}) => {
const button = (
<a
className={`pricing-plan__button btn ${buttonClassName} w-100 mx-auto my-0 justify-content-center text-center d-flex`}
href={buttonHref}
onClick={buttonOnClick}
>
{buttonLabel}
</a>
)

return (
<div className={`pricing-plan card ${className}`}>
<h2 className="card-title mt-3 mb-1 text-center pricing-plan__title">{name}</h2>
<div className="card-body pt-3 text-center d-flex flex-column align-items-center">
{button}
<div className="mt-4 mb-2 pb-2 pricing-plan__price text-muted">{price}</div>
{planProperties}
</div>
<ol className="pricing-plan__features list-group list-group-flush py-3">
{!isFree ? (
<li className="pricing-plan-feature list-group-item bg-transparent border-0 px-0">
Everything in the Free tier, plus:
</li>
) : null}
{FEATURE_ORDER.map(feature => (
<div key={FEATURE_INFO[feature].label}>
<PricingPlanFeature
info={FEATURE_INFO[feature]}
value={features[feature]}
tag="li"
className="list-group-item bg-transparent border-0 px-0"
/>
</div>
))}
</ol>
</div>
)
}
52 changes: 52 additions & 0 deletions src/components/Pricing/PricingPlanFeature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FunctionComponent } from 'react'

import CheckIcon from 'mdi-react/CheckIcon'
import QuestionMarkCircleOutlineIcon from 'mdi-react/QuestionMarkCircleOutlineIcon'
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
import Tooltip from 'react-bootstrap/Tooltip'

import { FeatureInfo } from './interfaces'

interface Props {
info: FeatureInfo
value: boolean
tag: 'li'
className?: string
}

export const PricingPlanFeature: FunctionComponent<Props> = ({
info: { label, description },
value,
tag: Tag = 'li',
className = '',
}) =>
value ? (
<Tag
className={`pricing-plan-feature ${className} d-flex justify-content-between ${
value ? 'pricing-plan-feature__value-true' : 'pricing-plan-feature__value-false'
}`}
>
<div>
<CheckIcon
className={`icon-inline ${
value ? 'pricing-plan-feature__icon-true' : 'pricing-plan-feature__icon-false'
}`}
/>{' '}
{label}
</div>
{description && (
<OverlayTrigger
placement="auto"
flip={true}
transition={false}
overlay={<Tooltip id="tooltip">{description}</Tooltip>}
>
{({ ref, ...triggerHandler }) => (
bretthayes marked this conversation as resolved.
Show resolved Hide resolved
<span {...triggerHandler} ref={ref} className="ml-2 pricing-plan-feature__help">
<QuestionMarkCircleOutlineIcon />
</span>
)}
</OverlayTrigger>
)}
</Tag>
) : null
14 changes: 14 additions & 0 deletions src/components/Pricing/PricingPlanProperty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FunctionComponent, ReactFragment } from 'react'

interface Props {
description?: ReactFragment

katjuell marked this conversation as resolved.
Show resolved Hide resolved
className?: string
}

export const PricingPlanProperty: FunctionComponent<Props> = ({ description, className = '', children }) => (
<div className={`pricing-plan-property ${className} pb-1`}>
<div>{children}</div>
<small className="text-muted">{description}</small>
</div>
)
4 changes: 4 additions & 0 deletions src/components/Pricing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './PricingPlan'
export * from './PricingPlanFeature'
export * from './PricingPlanProperty'
export * from './interfaces'
33 changes: 33 additions & 0 deletions src/components/Pricing/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* The features to display for pricing plans.
*/
export interface Features {
codeSearch: boolean
codeIntelligence: boolean
batchChanges: boolean
batchChangesTrial: boolean
codeHostIntegration: boolean
api: boolean
selfHosted: boolean
userAndAdminRoles: boolean
singleSignOn: boolean
multipleCodeHosts: boolean
repositoryPermissions: boolean
optimizedRepositoryUpdates: boolean
privateExtensions: boolean
deploymentMetricsAndMonitoring: boolean
backupRestore: boolean
customBranding: boolean
onlineTraining: boolean
customContractLegalBillingTerms: boolean
unlimitedCode: boolean
managedInstance: boolean
codeInsights: boolean
codeInsightsTrial: boolean
}

export interface FeatureInfo {
label: string
url?: string
description: string
}
8 changes: 7 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export { Blockquote, BlockquoteWithBorder, BlockquoteWithLogoTop, BlockquoteWith
export { Video } from './Video'

// Tracking
export { buttonStyle, buttonLocation } from './tracking'
export { buttonStyle, buttonLocation } from './data/tracking'

// Actions
export {
Expand All @@ -26,6 +26,12 @@ export {
ViewDeveloperDocumentationAction,
} from './Actions'

// Pricing

export { PricingPlanProperty, PricingPlanFeature, PricingPlan } from './Pricing'

export type { Features } from './Pricing'

// Page Specific
export {
CaseStudyJumbotron,
Expand Down
5 changes: 4 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@styles/globals.scss'
import type { AppProps } from 'next/app'
import Script from 'next/script'
import SSRProvider from 'react-bootstrap/SSRProvider'

const App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
Expand Down Expand Up @@ -57,7 +58,9 @@ const App = ({ Component, pageProps }: AppProps): JSX.Element => (
// eslint-disable-next-line quotes
>{`!function(e){var o=document.getElementsByTagName("script")[0];if("object"==typeof e.ClearbitForHubspot)return console.log("Clearbit For HubSpot included more than once"),!1;e.ClearbitForHubspot={},e.ClearbitForHubspot.forms=[],e.ClearbitForHubspot.addForm=function(o){var t=o[0];"function"==typeof e.ClearbitForHubspot.onFormReady?e.ClearbitForHubspot.onFormReady(t):e.ClearbitForHubspot.forms.push(t)};var t=document.createElement("script");t.async=!0,t.src="https://hubspot.clearbit.com/v1/forms/pk_a66b9ed76e62c713c06aab39bfae7234/forms.js",o.parentNode.insertBefore(t,o),e.addEventListener("message",function(o){if("hsFormCallback"===o.data.type&&"onFormReady"===o.data.eventName)if(document.querySelectorAll('form[data-form-id="'+o.data.id+'"]').length>0)e.ClearbitForHubspot.addForm(document.querySelectorAll('form[data-form-id="'+o.data.id+'"]'));else if(document.querySelectorAll("iframe.hs-form-iframe").length>0){document.querySelectorAll("iframe.hs-form-iframe").forEach(function(t){t.contentWindow.document.querySelectorAll('form[data-form-id="'+o.data.id+'"]').length>0&&e.ClearbitForHubspot.addForm(t.contentWindow.document.querySelectorAll('form[data-form-id="'+o.data.id+'"]'))})}})}(window);`}</Script>

<Component {...pageProps} />
<SSRProvider>
<Component {...pageProps} />
</SSRProvider>
</>
)

Expand Down
Loading