1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-23 18:53:36 +02:00

Doc: Add Joplin Cloud Teams offer to website

This commit is contained in:
Laurent Cozic 2022-04-07 15:35:15 +01:00
parent 84d40b805e
commit b3d09ce776
9 changed files with 296 additions and 109 deletions

View File

@ -1073,6 +1073,10 @@ footer .bottom-links-row p {
display: none; display: none;
} }
.joplin-cloud-feature-list table {
width: 100%;
}
.price-row .plan-type { .price-row .plan-type {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -6,11 +6,11 @@
</div> </div>
<div class="plan-price plan-price-monthly"> <div class="plan-price plan-price-monthly">
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month</sub> {{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month{{#footnote}} (*){{/footnote}}</sub>
</div> </div>
<div class="plan-price plan-price-yearly"> <div class="plan-price plan-price-yearly">
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month</sub> {{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month{{#footnote}} (*){{/footnote}}</sub>
</div> </div>
</div> </div>
@ -20,17 +20,19 @@
</div> </div>
</div> </div>
{{#featuresOn}} {{#featureLabelsOn}}
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p> <p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
{{/featuresOn}} {{/featureLabelsOn}}
{{#featuresOff}} {{#featureLabelsOff}}
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p> <p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p>
{{/featuresOff}} {{/featureLabelsOff}}
<p class="text-center subscribe-wrapper"> <p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a> <a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
</p> </p>
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
</div> </div>
<script> <script>

View File

@ -42,13 +42,23 @@
{{> plan}} {{> plan}}
{{/plans.pro}} {{/plans.pro}}
{{#plans.business}} {{#plans.teams}}
{{> plan}} {{> plan}}
{{/plans.business}} {{/plans.teams}}
<p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p> <p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
</div> </div>
<div class="row">
<div>
<h1>Feature comparison</h1>
<div class="joplin-cloud-feature-list">
{{{featureListHtml}}}
</div>
<p>&nbsp;</p>
</div>
</div>
<div class="row"> <div class="row">
{{{faqHtml}}} {{{faqHtml}}}
</div> </div>

View File

@ -1,4 +1,26 @@
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '../markdownUtils';
type FeatureId = string;
export enum PlanName {
Basic = 'basic',
Pro = 'pro',
Teams = 'teams',
}
interface PlanFeature {
title: string;
basic: boolean;
pro: boolean;
teams: boolean;
basicInfo?: string;
proInfo?: string;
teamsInfo?: string;
basicInfoShort?: string;
proInfoShort?: string;
teamsInfoShort?: string;
}
export interface Plan { export interface Plan {
name: string; name: string;
@ -7,10 +29,13 @@ export interface Plan {
priceYearly: StripePublicConfigPrice; priceYearly: StripePublicConfigPrice;
featured: boolean; featured: boolean;
iconName: string; iconName: string;
featuresOn: string[]; featuresOn: FeatureId[];
featuresOff: string[]; featuresOff: FeatureId[];
featureLabelsOn: string[];
featureLabelsOff: string[];
cfaLabel: string; cfaLabel: string;
cfaUrl: string; cfaUrl: string;
footnote: string;
} }
export enum PricePeriod { export enum PricePeriod {
@ -40,31 +65,6 @@ export interface StripePublicConfig {
webhookBaseUrl: string; webhookBaseUrl: string;
} }
export interface PlanFeature {
label: string;
enabled: boolean;
}
export function getFeatureList(plan: Plan): PlanFeature[] {
const output: PlanFeature[] = [];
for (const f of plan.featuresOn) {
output.push({
label: f,
enabled: true,
});
}
for (const f of plan.featuresOff) {
output.push({
label: f,
enabled: false,
});
}
return output;
}
function formatPrice(amount: string | number, currency: PriceCurrency): string { function formatPrice(amount: string | number, currency: PriceCurrency): string {
amount = typeof amount === 'number' ? (Math.ceil(amount * 100) / 100).toFixed(2) : amount; amount = typeof amount === 'number' ? (Math.ceil(amount * 100) / 100).toFixed(2) : amount;
if (currency === PriceCurrency.EUR) return `${amount}`; if (currency === PriceCurrency.EUR) return `${amount}`;
@ -110,28 +110,181 @@ export function findPrice(prices: StripePublicConfigPrice[], query: FindPriceQue
return output; return output;
} }
const businessAccountEmailBody = `Hello, const features: Record<FeatureId, PlanFeature> = {
maxItemSize: {
title: 'Publish notes to the internet',
basic: true,
pro: true,
teams: true,
basicInfo: '10 MB per note or attachment',
proInfo: '200 MB per note or attachment',
teamsInfo: '200 MB per note or attachment',
basicInfoShort: '10 MB',
proInfoShort: '200 MB',
teamsInfoShort: '200 MB',
},
maxStorage: {
title: 'Storage space',
basic: true,
pro: true,
teams: true,
basicInfo: '1 GB storage space',
proInfo: '200 GB storage space',
teamsInfo: '200 GB storage space',
basicInfoShort: '1 GB',
proInfoShort: '200 GB',
teamsInfoShort: '200 GB',
},
publishNote: {
title: 'Publish notes to the internet',
basic: true,
pro: true,
teams: true,
},
sync: {
title: 'Sync as many devices as you want',
basic: true,
pro: true,
teams: true,
},
clipper: {
title: 'Web Clipper',
basic: true,
pro: true,
teams: true,
},
collaborate: {
title: 'Share and collaborate on a notebook',
basic: false,
pro: true,
teams: true,
},
multiUsers: {
title: 'Manage multiple users',
basic: false,
pro: false,
teams: true,
},
consolidatedBilling: {
title: 'Consolidated billing',
basic: false,
pro: false,
teams: true,
},
sharingAccessControl: {
title: 'Sharing access control',
basic: false,
pro: false,
teams: true,
},
prioritySupport: {
title: 'Priority support',
basic: false,
pro: false,
teams: true,
},
};
This is an automatically generated email. The Business feature is coming soon, and in the meantime we offer a business discount if you would like to register multiple users. export const getFeatureIdsByPlan = (planName: PlanName, featureOn: boolean): FeatureId[] => {
const output: FeatureId[] = [];
If so please let us know the following details and we will get back to you as soon as possible: for (const [k, v] of Object.entries(features)) {
if (v[planName] === featureOn) {
output.push(k);
}
}
- Name: return output;
};
- Email: export const getFeatureLabelsByPlan = (planName: PlanName, featureOn: boolean): string[] => {
const output: FeatureId[] = [];
- Number of users: `; for (const [featureId, v] of Object.entries(features)) {
if (v[planName] === featureOn) {
output.push(getFeatureLabel(planName, featureId));
}
}
export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan> { return output;
const features = { };
publishNote: 'Publish notes to the internet',
sync: 'Sync as many devices as you want', export const getAllFeatureIds = (): FeatureId[] => {
clipper: 'Web Clipper', return Object.keys(features);
collaborate: 'Share and collaborate on a notebook', };
multiUsers: 'Up to 10 users',
prioritySupport: 'Priority support', export const getFeatureById = (featureId: FeatureId): PlanFeature => {
return features[featureId];
};
export const getFeaturesByPlan = (planName: PlanName, featureOn: boolean): PlanFeature[] => {
const output: PlanFeature[] = [];
for (const [, v] of Object.entries(features)) {
if (v[planName] === featureOn) {
output.push(v);
}
}
return output;
};
export const getFeatureLabel = (planName: PlanName, featureId: FeatureId): string => {
const feature = features[featureId];
const k = `${planName}Info`;
if ((feature as any)[k]) return (feature as any)[k];
return feature.title;
};
export const getFeatureEnabled = (planName: PlanName, featureId: FeatureId): boolean => {
const feature = features[featureId];
return feature[planName];
};
export const createFeatureTableMd = () => {
const headers: MarkdownTableHeader[] = [
{
name: 'featureLabel',
label: 'Feature',
},
{
name: 'basic',
label: 'Basic',
},
{
name: 'pro',
label: 'Pro',
},
{
name: 'teams',
label: 'Teams',
},
];
const rows: MarkdownTableRow[] = [];
const getCellInfo = (planName: PlanName, feature: PlanFeature) => {
if (!feature[planName]) return '-';
const infoShort: string = (feature as any)[`${planName}InfoShort`];
if (infoShort) return infoShort;
return '✔️';
}; };
for (const [, feature] of Object.entries(features)) {
const row: MarkdownTableRow = {
featureLabel: feature.title,
basic: getCellInfo(PlanName.Basic, feature),
pro: getCellInfo(PlanName.Pro, feature),
teams: getCellInfo(PlanName.Teams, feature),
};
rows.push(row);
}
return markdownUtils.createMarkdownTable(headers, rows);
};
export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Plan> {
return { return {
basic: { basic: {
name: 'basic', name: 'basic',
@ -146,20 +299,13 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
}), }),
featured: false, featured: false,
iconName: 'basic-icon', iconName: 'basic-icon',
featuresOn: [ featuresOn: getFeatureIdsByPlan(PlanName.Basic, true),
'Max 10 MB per note or attachment', featuresOff: getFeatureIdsByPlan(PlanName.Basic, false),
features.publishNote, featureLabelsOn: getFeatureLabelsByPlan(PlanName.Basic, true),
features.sync, featureLabelsOff: getFeatureLabelsByPlan(PlanName.Basic, false),
features.clipper,
'1 GB storage space',
],
featuresOff: [
features.collaborate,
features.multiUsers,
features.prioritySupport,
],
cfaLabel: 'Try it now', cfaLabel: 'Try it now',
cfaUrl: '', cfaUrl: '',
footnote: '',
}, },
pro: { pro: {
@ -175,42 +321,35 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
}), }),
featured: true, featured: true,
iconName: 'pro-icon', iconName: 'pro-icon',
featuresOn: [ featuresOn: getFeatureIdsByPlan(PlanName.Pro, true),
'Max 200 MB per note or attachment', featuresOff: getFeatureIdsByPlan(PlanName.Pro, false),
features.publishNote, featureLabelsOn: getFeatureLabelsByPlan(PlanName.Pro, true),
features.sync, featureLabelsOff: getFeatureLabelsByPlan(PlanName.Pro, false),
features.clipper,
'10 GB storage space',
features.collaborate,
],
featuresOff: [
features.multiUsers,
features.prioritySupport,
],
cfaLabel: 'Try it now', cfaLabel: 'Try it now',
cfaUrl: '', cfaUrl: '',
footnote: '',
}, },
business: { teams: {
name: 'business', name: 'teams',
title: 'Business', title: 'Teams',
priceMonthly: { accountType: 3, formattedMonthlyAmount: '49.99€' } as any, priceMonthly: findPrice(stripeConfig.prices, {
priceYearly: { accountType: 3, formattedMonthlyAmount: '39.99€', formattedAmount: '479.88€' } as any, accountType: 3,
period: PricePeriod.Monthly,
}),
priceYearly: findPrice(stripeConfig.prices, {
accountType: 3,
period: PricePeriod.Yearly,
}),
featured: false, featured: false,
iconName: 'business-icon', iconName: 'business-icon',
featuresOn: [ featuresOn: getFeatureIdsByPlan(PlanName.Teams, true),
'Max 200 MB per note or attachment', featuresOff: getFeatureIdsByPlan(PlanName.Teams, false),
features.publishNote, featureLabelsOn: getFeatureLabelsByPlan(PlanName.Teams, true),
features.sync, featureLabelsOff: getFeatureLabelsByPlan(PlanName.Teams, false),
features.clipper, cfaLabel: 'Try it now',
'10 GB storage space', cfaUrl: '',
features.collaborate, footnote: 'Per user. Minimum of 2 users.',
features.multiUsers,
features.prioritySupport,
],
featuresOff: [],
cfaLabel: 'Contact us',
cfaUrl: `mailto:business@joplincloud.com?subject=${encodeURIComponent('Joplin Cloud Business Account Order')}&body=${encodeURIComponent(businessAccountEmailBody)}`,
}, },
}; };
} }

View File

@ -2,7 +2,7 @@ import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router'; import Router from '../../utils/Router';
import { RouteType } from '../../utils/types'; import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types'; import { AppContext } from '../../utils/types';
import { findPrice, getFeatureList, getPlans, PricePeriod } from '@joplin/lib/utils/joplinCloud'; import { findPrice, PricePeriod, PlanName, getFeatureLabel, getFeatureEnabled, getAllFeatureIds } from '@joplin/lib/utils/joplinCloud';
import config from '../../config'; import config from '../../config';
import defaultView from '../../utils/defaultView'; import defaultView from '../../utils/defaultView';
import { stripeConfig, stripePriceIdByUserId, updateSubscriptionType } from '../../utils/stripe'; import { stripeConfig, stripePriceIdByUserId, updateSubscriptionType } from '../../utils/stripe';
@ -28,21 +28,23 @@ router.get('upgrade', async (_path: SubPath, ctx: AppContext) => {
proLabel: string; proLabel: string;
} }
const plans = getPlans(stripeConfig()); const featureIds = getAllFeatureIds();
const basicFeatureList = getFeatureList(plans.basic);
const proFeatureList = getFeatureList(plans.pro);
const planRows: PlanRow[] = []; const planRows: PlanRow[] = [];
for (let i = 0; i < basicFeatureList.length; i++) { for (let i = 0; i < featureIds.length; i++) {
const basic = basicFeatureList[i]; const featureId = featureIds[i];
const pro = proFeatureList[i];
if (basic.label === pro.label && basic.enabled === pro.enabled) continue; const basicLabel = getFeatureLabel(PlanName.Basic, featureId);
const proLabel = getFeatureLabel(PlanName.Pro, featureId);
const basicEnabled = getFeatureEnabled(PlanName.Basic, featureId);
const proEnabled = getFeatureEnabled(PlanName.Pro, featureId);
if (basicLabel === proLabel && basicEnabled === proEnabled) continue;
planRows.push({ planRows.push({
basicLabel: basic.enabled ? basic.label : '-', basicLabel: basicEnabled ? basicLabel : '-',
proLabel: pro.label, proLabel: proLabel,
}); });
} }

View File

@ -30,6 +30,20 @@
"period": "yearly", "period": "yearly",
"amount": "57.48", "amount": "57.48",
"currency": "EUR" "currency": "EUR"
},
{
"accountType": 3,
"id": "price_1Kl9uBLx4fybOTqJhx7q4zzj",
"period": "monthly",
"amount": "7.99",
"currency": "EUR"
},
{
"accountType": 3,
"id": "price_1Kl9uNLx4fybOTqJpsB2l3Kg",
"period": "yearly",
"amount": "80.28",
"currency": "EUR"
} }
] ]
}, },
@ -64,6 +78,20 @@
"period": "yearly", "period": "yearly",
"amount": "57.48", "amount": "57.48",
"currency": "EUR" "currency": "EUR"
},
{
"accountType": 3,
"id": "price_1Kl9jyLx4fybOTqJN0i1A88B",
"period": "monthly",
"amount": "7.99",
"currency": "EUR"
},
{
"accountType": 3,
"id": "price_1Kl9nLLx4fybOTqJYTtts35z",
"period": "yearly",
"amount": "80.28",
"currency": "EUR"
} }
] ]
} }

View File

@ -3,7 +3,7 @@ import { rootDir } from '../tool-utils';
import { pressCarouselItems } from './utils/pressCarousel'; import { pressCarouselItems } from './utils/pressCarousel';
import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render'; import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render';
import { AssetUrls, Env, PlanPageParams, Sponsors, TemplateParams } from './utils/types'; import { AssetUrls, Env, PlanPageParams, Sponsors, TemplateParams } from './utils/types';
import { getPlans, loadStripeConfig } from '@joplin/lib/utils/joplinCloud'; import { createFeatureTableMd, getPlans, loadStripeConfig } from '@joplin/lib/utils/joplinCloud';
import { MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter'; import { MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter';
import { dirname, basename } from 'path'; import { dirname, basename } from 'path';
import { readmeFileTitle, replaceGitHubByWebsiteLinks } from './utils/parser'; import { readmeFileTitle, replaceGitHubByWebsiteLinks } from './utils/parser';
@ -14,7 +14,7 @@ const glob = require('glob');
const path = require('path'); const path = require('path');
const md5File = require('md5-file/promise'); const md5File = require('md5-file/promise');
const env = Env.Prod; const env = Env.Dev;
const docDir = `${dirname(dirname(dirname(dirname(__dirname))))}/joplin-website/docs`; const docDir = `${dirname(dirname(dirname(dirname(__dirname))))}/joplin-website/docs`;
@ -281,6 +281,7 @@ async function main() {
templateHtml: plansTemplateHtml, templateHtml: plansTemplateHtml,
plans: getPlans(stripeConfig), plans: getPlans(stripeConfig),
faqHtml: planPageFaqHtml, faqHtml: planPageFaqHtml,
featureListHtml: getMarkdownIt().render(createFeatureTableMd(), {}),
stripeConfig, stripeConfig,
}; };

View File

@ -24,15 +24,11 @@ export function renderMustache(contentHtml: string, templateParams: TemplatePara
} }
export function getMarkdownIt() { export function getMarkdownIt() {
return new MarkdownIt({ const markdownIt = new MarkdownIt({
breaks: true, breaks: true,
linkify: true, linkify: true,
html: true, html: true,
}); });
}
export function markdownToPageHtml(md: string, templateParams: TemplateParams): string {
const markdownIt = getMarkdownIt();
markdownIt.core.ruler.push('tableClass', (state: any) => { markdownIt.core.ruler.push('tableClass', (state: any) => {
const tokens = state.tokens; const tokens = state.tokens;
@ -47,7 +43,11 @@ export function markdownToPageHtml(md: string, templateParams: TemplateParams):
} }
}); });
markdownIt.use(headerAnchor); return markdownIt;
}
export function markdownToPageHtml(md: string, templateParams: TemplateParams): string {
const markdownIt = getMarkdownIt();
markdownIt.use(headerAnchor);
return renderMustache(markdownIt.render(md), templateParams); return renderMustache(markdownIt.render(md), templateParams);
} }

View File

@ -79,5 +79,6 @@ export interface TemplateParams {
export interface PlanPageParams extends TemplateParams { export interface PlanPageParams extends TemplateParams {
plans: Record<string, Plan>; plans: Record<string, Plan>;
faqHtml: string; faqHtml: string;
featureListHtml: string;
stripeConfig: StripePublicConfig; stripeConfig: StripePublicConfig;
} }