Skip to content

Commit 6de1c56

Browse files
committed
Team plans
1 parent 8eaa2c1 commit 6de1c56

27 files changed

Lines changed: 1426 additions & 496 deletions

File tree

landing/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
"autoprefixer": "9.6.4",
2020
"classnames": "2.2.6",
2121
"isomorphic-unfetch": "3.0.0",
22-
"next": "9.0.7",
22+
"next": "9.1.4",
2323
"next-transpile-modules": "2.3.1",
24+
"qs": "6.9.1",
2425
"react": "16.10.2",
2526
"react-dom": "16.10.2",
2627
"react-stripe-elements": "5.0.1",

landing/pages/styles/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ body {
3232
pointer-events: none;
3333
}
3434

35-
a {
35+
a.transform-on-hover {
3636
margin-top: 1px;
3737
margin-bottom: 1px;
3838
}

landing/src/components/common/Select.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function Select<T extends string>(props: SelectProps<T>) {
3939
style={{
4040
top: selectedIndex >= 0 ? `-${110 * selectedIndex + 6}%` : 0,
4141
left: -1,
42+
zIndex: 1000,
4243
}}
4344
>
4445
<span className="flex flex-col bg-default border border-bg-less-2 rounded-lg shadow overflow-hidden">
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import classNames from 'classnames'
2+
import React, { ReactElement } from 'react'
3+
4+
export interface TabsProps<TabId extends string> {
5+
className?: string
6+
children: Array<ReactElement<TabProps<TabId>>>
7+
onTabChange: (id: TabId) => void
8+
}
9+
10+
export function Tabs<TabId extends string>(props: TabsProps<TabId>) {
11+
const { children, className, onTabChange } = props
12+
13+
return (
14+
<div className="flex flex-row items-center justify-center">
15+
<div
16+
className={classNames(
17+
'flex flex-row self-center mx-auto bg-less-1 rounded-full',
18+
className,
19+
)}
20+
>
21+
{React.Children.map(children, child => (
22+
<div
23+
className="cursor-pointer"
24+
onClick={() => onTabChange(child.props.id)}
25+
>
26+
{child}
27+
</div>
28+
))}
29+
</div>
30+
</div>
31+
)
32+
}
33+
34+
export interface TabProps<TabId extends string> {
35+
active?: boolean
36+
id: TabId
37+
title: React.ReactNode
38+
}
39+
40+
Tabs.Tab = function Tab<TabId extends string>(props: TabProps<TabId>) {
41+
const { active, title } = props
42+
43+
return (
44+
<div
45+
className={classNames(
46+
'flex flex-col m-1 py-1 px-6 text-default text-center rounded-full',
47+
active && 'bg-lighther-2 font-bold',
48+
)}
49+
>
50+
<span>{title}</span>
51+
<span
52+
className="font-bold invisible pointer-events-none"
53+
style={{ marginTop: '-1.5rem' }}
54+
>
55+
{title}
56+
</span>
57+
</div>
58+
)
59+
}

landing/src/components/sections/pricing/PricingPlanBlock.tsx

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import {
2+
activePlans,
3+
formatPrice,
4+
formatPriceAndInterval,
5+
Plan,
6+
} from '@brunolemos/devhub-core'
17
import classNames from 'classnames'
8+
import qs from 'qs'
29
import React from 'react'
310

4-
import { activePlans, Plan } from '@brunolemos/devhub-core'
511
import { useAuth } from '../../../context/AuthContext'
6-
import { formatPrice, formatPriceAndInterval } from '../../../helpers'
712
import Button from '../../common/buttons/Button'
813
import CheckLabel from '../../common/CheckLabel'
914

@@ -37,7 +42,11 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
3742
? plan.amount * 4
3843
: plan.interval === 'year'
3944
? plan.amount / 12
40-
: plan.amount) / (plan.intervalCount || 1)
45+
: plan.amount) /
46+
(plan.intervalCount || 1) /
47+
(plan.transformUsage && plan.transformUsage.divideBy > 1
48+
? plan.transformUsage.divideBy
49+
: 1)
4150

4251
const _priceLabel = formatPrice(estimatedMonthlyPrice, plan)
4352
const _cents = estimatedMonthlyPrice % 100
@@ -50,6 +59,23 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
5059
? _priceLabel.substr(-3)
5160
: ''
5261

62+
let footerText = ''
63+
64+
if (!plan.amount && plan.trialPeriodDays) {
65+
footerText = footerText + `Free for ${plan.trialPeriodDays} days`
66+
}
67+
68+
if (estimatedMonthlyPrice !== plan.amount) {
69+
footerText =
70+
footerText +
71+
`*Billed ${plan.amount % 100 > 50 ? '~' : ''}${formatPriceAndInterval(
72+
plan.amount % 100 > 50
73+
? plan.amount + (100 - (plan.amount % 100))
74+
: plan.amount,
75+
plan,
76+
)}`
77+
}
78+
5379
return (
5480
<section className="pricing-plan flex flex-col flex-shrink-0 w-64">
5581
<div
@@ -92,16 +118,25 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
92118
<small>
93119
<small>
94120
<small>
95-
<small>{priceLabelCents}</small>
121+
<small className="text-muted-65">{priceLabelCents}</small>
96122
</small>
97123
</small>
98124
</small>
99125
</small>
100126
)}
101127
</div>
102-
<div className="text-sm text-muted-65">{`/month${
103-
estimatedMonthlyPrice !== plan.amount ? '*' : ''
104-
}`}</div>
128+
<div className="text-sm text-muted-65">
129+
&nbsp;
130+
{`${
131+
plan.type === 'team' ||
132+
(plan.transformUsage && plan.transformUsage.divideBy > 1)
133+
? '/user'
134+
: ''
135+
}${plan.interval ? '/month' : ''}${
136+
estimatedMonthlyPrice !== plan.amount ? '*' : ''
137+
}`}
138+
&nbsp;
139+
</div>
105140
{/* {plan.interval ? (
106141
<div className="text-sm text-muted-65">{`/${
107142
plan.intervalCount > 1 ? `${plan.intervalCount}-` : ''
@@ -114,27 +149,28 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
114149

115150
<div className="mb-2 text-sm text-muted-65 italic">
116151
&nbsp;
117-
{estimatedMonthlyPrice !== plan.amount
118-
? `*Billed ${
119-
plan.amount % 100 > 50 ? '~' : ''
120-
}${formatPriceAndInterval(
121-
plan.amount % 100 > 50
122-
? plan.amount + (100 - (plan.amount % 100))
123-
: plan.amount,
124-
plan,
125-
)}`
126-
: !plan.amount && plan.trialPeriodDays
127-
? `Free for ${plan.trialPeriodDays} days`
128-
: ''}
152+
{footerText}
129153
&nbsp;
130154
</div>
131155

132156
<div className="pb-6" />
133157

134158
{isMyPlan ? (
135-
<Button type="primary" href="/account">
136-
{'Manage'}
137-
</Button>
159+
plan.type === 'team' ? (
160+
<Button
161+
type="primary"
162+
href={`/subscribe${qs.stringify(
163+
{ plan: userPlan && userPlan.id },
164+
{ addQueryPrefix: true },
165+
)}`}
166+
>
167+
Update
168+
</Button>
169+
) : (
170+
<Button type="primary" href="/account">
171+
Manage
172+
</Button>
173+
)
138174
) : (
139175
<Button type="primary" href={buttonLink}>
140176
{buttonLabel || 'Get started'}

landing/src/components/sections/pricing/PricingPlans.tsx

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
1-
import React, { Fragment } from 'react'
1+
import { activePlans, PlanType } from '@brunolemos/devhub-core'
2+
import React, { Fragment, useMemo, useState } from 'react'
23

3-
import { activePlans } from '@brunolemos/devhub-core'
4-
import Link from 'next/link'
4+
import { useAuth } from '../../../context/AuthContext'
5+
import { Tabs } from '../../common/Tabs'
56
import { PricingPlanBlock } from './PricingPlanBlock'
67

78
export interface PricingPlansProps {}
89

9-
const pricingPlanComponents = activePlans.map(plan =>
10-
plan.amount > 0 ? (
11-
<PricingPlanBlock
12-
key={`pricing-plan-${plan.id}`}
13-
banner={plan.banner}
14-
buttonLink={`/subscribe?plan=${plan.cannonicalId}`}
15-
plan={plan}
16-
/>
17-
) : (
18-
<PricingPlanBlock
19-
key={`pricing-plan-${plan.cannonicalId}`}
20-
banner
21-
buttonLink={`/download?plan=${plan.cannonicalId}`}
22-
buttonLabel="Download"
23-
plan={plan}
24-
/>
25-
),
26-
)
27-
2810
export function PricingPlans(_props: PricingPlansProps) {
11+
const { authData } = useAuth()
12+
13+
const [tab, setTab] = useState<PlanType>(
14+
(authData && authData.plan && authData.plan.type) === 'team'
15+
? 'team'
16+
: 'individual',
17+
)
18+
19+
const pricingPlanComponents = useMemo(
20+
() =>
21+
activePlans
22+
.filter(
23+
plan =>
24+
!!(
25+
plan &&
26+
(plan.type === tab || (tab === 'individual' && !plan.type))
27+
),
28+
)
29+
.map(plan =>
30+
plan.amount > 0 ? (
31+
<PricingPlanBlock
32+
key={`pricing-plan-${plan.id}`}
33+
banner={plan.banner}
34+
buttonLink={`/subscribe?plan=${plan.cannonicalId}`}
35+
plan={plan}
36+
/>
37+
) : (
38+
<PricingPlanBlock
39+
key={`pricing-plan-${plan.cannonicalId}`}
40+
banner
41+
buttonLink={`/download?plan=${plan.cannonicalId}`}
42+
buttonLabel="Download"
43+
plan={plan}
44+
/>
45+
),
46+
),
47+
[tab],
48+
)
49+
2950
return (
3051
<div className="container">
52+
<Tabs<NonNullable<PlanType>>
53+
className="mb-6"
54+
onTabChange={id => setTab(id)}
55+
>
56+
<Tabs.Tab
57+
active={tab === 'individual'}
58+
id="individual"
59+
title="Individual"
60+
/>
61+
<Tabs.Tab active={tab === 'team'} id="team" title="Team" />
62+
</Tabs>
63+
3164
<div className="flex flex-row lg:justify-center items-stretch -ml-8 sm:ml-0 -mr-8 sm:mr-0 pl-8 sm:pl-0 pr-8 sm:pr-0 overflow-x-scroll md:overflow-x-auto">
3265
{pricingPlanComponents.map((component, index) => (
3366
<Fragment key={`${component.key}-container`}>

0 commit comments

Comments
 (0)