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:
parent
84d40b805e
commit
b3d09ce776
@ -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;
|
||||
|
@ -6,11 +6,11 @@
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /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>
|
||||
|
@ -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> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{{faqHtml}}}
|
||||
</div>
|
||||
|
@ -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.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -79,5 +79,6 @@ export interface TemplateParams {
|
||||
export interface PlanPageParams extends TemplateParams {
|
||||
plans: Record<string, Plan>;
|
||||
faqHtml: string;
|
||||
featureListHtml: string;
|
||||
stripeConfig: StripePublicConfig;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user