1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +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;
}
.joplin-cloud-feature-list table {
width: 100%;
}
.price-row .plan-type {
display: flex;
align-items: center;

View File

@ -6,11 +6,11 @@
</div>
<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 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>
@ -20,17 +20,19 @@
</div>
</div>
{{#featuresOn}}
{{#featureLabelsOn}}
<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>
{{/featuresOff}}
{{/featureLabelsOff}}
<p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
</p>
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
</div>
<script>

View File

@ -42,13 +42,23 @@
{{> plan}}
{{/plans.pro}}
{{#plans.business}}
{{#plans.teams}}
{{> 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>
</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">
{{{faqHtml}}}
</div>

View File

@ -1,4 +1,26 @@
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 {
name: string;
@ -7,10 +29,13 @@ export interface Plan {
priceYearly: StripePublicConfigPrice;
featured: boolean;
iconName: string;
featuresOn: string[];
featuresOff: string[];
featuresOn: FeatureId[];
featuresOff: FeatureId[];
featureLabelsOn: string[];
featureLabelsOff: string[];
cfaLabel: string;
cfaUrl: string;
footnote: string;
}
export enum PricePeriod {
@ -40,31 +65,6 @@ export interface StripePublicConfig {
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 {
amount = typeof amount === 'number' ? (Math.ceil(amount * 100) / 100).toFixed(2) : amount;
if (currency === PriceCurrency.EUR) return `${amount}`;
@ -110,28 +110,181 @@ export function findPrice(prices: StripePublicConfigPrice[], query: FindPriceQue
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> {
const features = {
publishNote: 'Publish notes to the internet',
sync: 'Sync as many devices as you want',
clipper: 'Web Clipper',
collaborate: 'Share and collaborate on a notebook',
multiUsers: 'Up to 10 users',
prioritySupport: 'Priority support',
return output;
};
export const getAllFeatureIds = (): FeatureId[] => {
return Object.keys(features);
};
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 {
basic: {
name: 'basic',
@ -146,20 +299,13 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
}),
featured: false,
iconName: 'basic-icon',
featuresOn: [
'Max 10 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'1 GB storage space',
],
featuresOff: [
features.collaborate,
features.multiUsers,
features.prioritySupport,
],
featuresOn: getFeatureIdsByPlan(PlanName.Basic, true),
featuresOff: getFeatureIdsByPlan(PlanName.Basic, false),
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Basic, true),
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Basic, false),
cfaLabel: 'Try it now',
cfaUrl: '',
footnote: '',
},
pro: {
@ -175,42 +321,35 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
}),
featured: true,
iconName: 'pro-icon',
featuresOn: [
'Max 200 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'10 GB storage space',
features.collaborate,
],
featuresOff: [
features.multiUsers,
features.prioritySupport,
],
featuresOn: getFeatureIdsByPlan(PlanName.Pro, true),
featuresOff: getFeatureIdsByPlan(PlanName.Pro, false),
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Pro, true),
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Pro, false),
cfaLabel: 'Try it now',
cfaUrl: '',
footnote: '',
},
business: {
name: 'business',
title: 'Business',
priceMonthly: { accountType: 3, formattedMonthlyAmount: '49.99€' } as any,
priceYearly: { accountType: 3, formattedMonthlyAmount: '39.99€', formattedAmount: '479.88€' } as any,
teams: {
name: 'teams',
title: 'Teams',
priceMonthly: findPrice(stripeConfig.prices, {
accountType: 3,
period: PricePeriod.Monthly,
}),
priceYearly: findPrice(stripeConfig.prices, {
accountType: 3,
period: PricePeriod.Yearly,
}),
featured: false,
iconName: 'business-icon',
featuresOn: [
'Max 200 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'10 GB storage space',
features.collaborate,
features.multiUsers,
features.prioritySupport,
],
featuresOff: [],
cfaLabel: 'Contact us',
cfaUrl: `mailto:business@joplincloud.com?subject=${encodeURIComponent('Joplin Cloud Business Account Order')}&body=${encodeURIComponent(businessAccountEmailBody)}`,
featuresOn: getFeatureIdsByPlan(PlanName.Teams, true),
featuresOff: getFeatureIdsByPlan(PlanName.Teams, false),
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Teams, true),
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Teams, false),
cfaLabel: 'Try it now',
cfaUrl: '',
footnote: 'Per user. Minimum of 2 users.',
},
};
}

View File

@ -2,7 +2,7 @@ import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } 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 defaultView from '../../utils/defaultView';
import { stripeConfig, stripePriceIdByUserId, updateSubscriptionType } from '../../utils/stripe';
@ -28,21 +28,23 @@ router.get('upgrade', async (_path: SubPath, ctx: AppContext) => {
proLabel: string;
}
const plans = getPlans(stripeConfig());
const basicFeatureList = getFeatureList(plans.basic);
const proFeatureList = getFeatureList(plans.pro);
const featureIds = getAllFeatureIds();
const planRows: PlanRow[] = [];
for (let i = 0; i < basicFeatureList.length; i++) {
const basic = basicFeatureList[i];
const pro = proFeatureList[i];
for (let i = 0; i < featureIds.length; i++) {
const featureId = featureIds[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({
basicLabel: basic.enabled ? basic.label : '-',
proLabel: pro.label,
basicLabel: basicEnabled ? basicLabel : '-',
proLabel: proLabel,
});
}

View File

@ -30,6 +30,20 @@
"period": "yearly",
"amount": "57.48",
"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",
"amount": "57.48",
"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 { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render';
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 { dirname, basename } from 'path';
import { readmeFileTitle, replaceGitHubByWebsiteLinks } from './utils/parser';
@ -14,7 +14,7 @@ const glob = require('glob');
const path = require('path');
const md5File = require('md5-file/promise');
const env = Env.Prod;
const env = Env.Dev;
const docDir = `${dirname(dirname(dirname(dirname(__dirname))))}/joplin-website/docs`;
@ -281,6 +281,7 @@ async function main() {
templateHtml: plansTemplateHtml,
plans: getPlans(stripeConfig),
faqHtml: planPageFaqHtml,
featureListHtml: getMarkdownIt().render(createFeatureTableMd(), {}),
stripeConfig,
};

View File

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

View File

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