Merge pull request #1900 from mermaid-js/release-promotion
Release live editor
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { dismissEditorChooser, isOnMermaidAI } from '$/util/migration/domainMigration';
|
||||
import { dismissEditorChooser } from '$/util/migration/domainMigration';
|
||||
import { logEvent, logMermaidChartClick } from '$/util/stats';
|
||||
import { getCheckoutUrl } from '$/util/util';
|
||||
import CodeIcon from '~icons/custom/code';
|
||||
import OpenSourceIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import ChatIcon from '~icons/material-symbols/chat-outline-rounded';
|
||||
@@ -28,15 +29,8 @@
|
||||
logEvent('chooseEditor', { choice: 'plus' });
|
||||
logMermaidChartClick('editorPicker');
|
||||
close();
|
||||
const utmSource = isOnMermaidAI() ? 'mermaid_ai_live' : 'mermaid_live_editor';
|
||||
window.open(
|
||||
`https://mermaid.ai/app/user/billing/checkout?${new URLSearchParams({
|
||||
coupon: 'dmIuNbqx',
|
||||
tier: 'plus',
|
||||
utm_campaign: 'start_plus',
|
||||
utm_medium: '2_editor_selection',
|
||||
utm_source: utmSource
|
||||
}).toString()}`,
|
||||
getCheckoutUrl({ utmCampaign: 'start_plus', utmMedium: '2_editor_selection' }),
|
||||
'_blank'
|
||||
);
|
||||
};
|
||||
@@ -75,7 +69,7 @@
|
||||
<div class="mb-2 flex justify-center">
|
||||
<span
|
||||
class="rounded-full bg-pink-100 px-3 py-0.5 text-xs font-semibold text-pink-700 dark:bg-pink-950 dark:text-pink-300">
|
||||
Limited time: 10% off
|
||||
10% off with code JS26
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,6 +42,25 @@ const hasPakoData = (): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user arrived from a known Mermaid domain (mermaid.ai or mermaid.js.org).
|
||||
*/
|
||||
const isReferredFromMermaid = (): boolean => {
|
||||
try {
|
||||
const referrer = document.referrer;
|
||||
if (!referrer) return false;
|
||||
const hostname = new URL(referrer).hostname;
|
||||
return (
|
||||
hostname === 'mermaid.ai' ||
|
||||
hostname.endsWith('.mermaid.ai') ||
|
||||
hostname === 'mermaid.js.org' ||
|
||||
hostname.endsWith('.mermaid.js.org')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the editor chooser modal should be shown.
|
||||
* Shows for new users who haven't dismissed it and aren't viewing a shared link.
|
||||
@@ -51,6 +70,7 @@ export const shouldShowEditorChooser = (): boolean => {
|
||||
if (window.localStorage.getItem(C.editorChooserDismissedKey) === 'true') return false;
|
||||
if (hasStoredUserData()) return false;
|
||||
if (hasPakoData()) return false;
|
||||
if (isReferredFromMermaid()) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { isOnMermaidAI } from '$/util/migration/domainMigration';
|
||||
import { getCheckoutUrl } from '$/util/util';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -9,15 +9,7 @@
|
||||
|
||||
let { closeBanner }: Props = $props();
|
||||
|
||||
const utmSource = isOnMermaidAI() ? 'mermaid_ai_live' : 'mermaid_live_editor';
|
||||
|
||||
const url = `https://mermaid.ai/app/user/billing/checkout?${new URLSearchParams({
|
||||
coupon: 'dmIuNbqx',
|
||||
tier: 'plus',
|
||||
utm_campaign: 'newyear',
|
||||
utm_medium: 'banner_ad',
|
||||
utm_source: utmSource
|
||||
}).toString()}`;
|
||||
const url = getCheckoutUrl({ utmCampaign: 'oss_coupon', utmMedium: 'banner_ad' });
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-center bg-[#E0095F] p-1.5" role="banner">
|
||||
@@ -27,11 +19,11 @@
|
||||
target="_blank"
|
||||
class="col-start-1 row-start-1 flex items-center justify-center gap-4 no-underline">
|
||||
<span class="text-base tracking-wider text-white">
|
||||
Limited time: 10% off Mermaid Advanced Editor with code NEWYEAR
|
||||
Try Mermaid Advanced Editor — OSS users get 10% off with code JS26
|
||||
</span>
|
||||
<Button
|
||||
class="shrink-0 rounded-md bg-[#1E1A2E] px-3 py-1.5 text-base font-semibold tracking-wide text-white hover:bg-[#261A56]">
|
||||
Claim discount
|
||||
Get started
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@ import type { Component } from 'svelte';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { localStorage, persist } from '../persist';
|
||||
import April2025 from './April2025.svelte';
|
||||
import NewYear2026 from './NewYear2026.svelte';
|
||||
import JS2026 from './JS2026.svelte';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
@@ -17,10 +17,10 @@ interface Promotion {
|
||||
}
|
||||
|
||||
const promotions: Record<string, Promotion> = {
|
||||
'promo-newyear-2026': {
|
||||
'promo-js-2026': {
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-03-14'),
|
||||
component: NewYear2026,
|
||||
endDate: new Date('2026-12-31'),
|
||||
component: JS2026,
|
||||
hideDurationMs: dayjs.duration(1, 'week').asMilliseconds()
|
||||
},
|
||||
'promo-april-2025': {
|
||||
|
||||
@@ -40,6 +40,23 @@ export const MCBaseURL = env.isEnabledMermaidChartLinks
|
||||
? 'https://mermaid.ai' // 'http://localhost:5174'
|
||||
: 'https://example.com';
|
||||
|
||||
export const getCheckoutUrl = ({
|
||||
utmCampaign,
|
||||
utmMedium
|
||||
}: {
|
||||
utmCampaign: string;
|
||||
utmMedium: string;
|
||||
}): string => {
|
||||
const params = new URLSearchParams({
|
||||
coupon: 'arDfyFT8',
|
||||
tier: 'plus',
|
||||
utm_campaign: utmCampaign,
|
||||
utm_medium: utmMedium,
|
||||
utm_source: getUTMSource()
|
||||
});
|
||||
return `${MCBaseURL}/app/user/billing/checkout?${params.toString()}`;
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
export const errorDebug = (limit = 1000) => {
|
||||
count += 1;
|
||||
|
||||
@@ -8,8 +8,7 @@ test.describe('Check actions', () => {
|
||||
test('should update markdown code', async ({ editPage }) => {
|
||||
const oldText = await editPage.markdownInput.inputValue();
|
||||
await editPage.typeInEditor('C --> HistoryTest', { bottom: true, newline: true });
|
||||
const newText = await editPage.markdownInput.inputValue();
|
||||
expect(newText).not.toBe(oldText);
|
||||
await expect(editPage.markdownInput).not.toHaveValue(oldText);
|
||||
});
|
||||
|
||||
test.skip('should load gists from URL', async ({ page }) => {
|
||||
@@ -21,15 +20,15 @@ test.describe('Check actions', () => {
|
||||
});
|
||||
|
||||
test('should download png and svg', async ({ editPage }) => {
|
||||
const firstPngSize = await editPage.checkAndDownloadPNG(80_000);
|
||||
const firstSvgSize = await editPage.downloadSVG(30_000);
|
||||
const firstPngSize = await editPage.checkAndDownloadPNG(20_000);
|
||||
const firstSvgSize = await editPage.downloadSVG(10_000);
|
||||
|
||||
// Verify downloaded file is different for different diagrams
|
||||
await editPage.toggleSampleDiagrams();
|
||||
await editPage.loadSampleDiagram('Entity Relationship');
|
||||
|
||||
const secondPngSize = await editPage.checkAndDownloadPNG(81_000);
|
||||
const secondSvgSize = await editPage.downloadSVG(40_000);
|
||||
const secondPngSize = await editPage.checkAndDownloadPNG(20_000);
|
||||
const secondSvgSize = await editPage.downloadSVG(10_000);
|
||||
|
||||
// Verify files are actually different
|
||||
expect(firstPngSize).not.toBe(secondPngSize);
|
||||
|
||||
@@ -4,6 +4,9 @@ test.describe('Auto sync tests', () => {
|
||||
test('should automatically defer rendering when complex diagrams are edited', async ({
|
||||
editPage
|
||||
}) => {
|
||||
test.slow(); // Complex diagram rendering can be slow under parallel load
|
||||
// Wait for the default diagram to fully render before typing
|
||||
await editPage.checkTextInView('Christmas');
|
||||
await editPage.typeInEditor(
|
||||
`
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
@@ -27,9 +30,12 @@ A & B & C & D & E --> F & G & H & I & J & K & LongTest`
|
||||
});
|
||||
|
||||
test('supports editing code when code is incorrect', async ({ editPage }) => {
|
||||
test.slow(); // Error display has a 5s debounce; rendering pipeline adds more delay under load
|
||||
await editPage.start(
|
||||
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
|
||||
);
|
||||
// Wait for the gitGraph code to load in the editor before typing
|
||||
await editPage.checkInEditor('gitGraph');
|
||||
await editPage.typeInEditor('branch test', { bottom: true, newline: true });
|
||||
await editPage.checkTextNotInView('test');
|
||||
await editPage.checkInEditor('branch test');
|
||||
|
||||
+3
-1
@@ -71,7 +71,9 @@ export class EditorPage {
|
||||
}
|
||||
|
||||
async checkError(text: string) {
|
||||
await expect(this.page.getByTestId(TID.errorContainer)).toContainText(text);
|
||||
await expect(this.page.getByTestId(TID.errorContainer)).toContainText(text, {
|
||||
timeout: 10_000
|
||||
});
|
||||
}
|
||||
|
||||
async checkInEditor(text: string) {
|
||||
|
||||
@@ -42,7 +42,6 @@ export async function verifyFileSizeGreaterThan(
|
||||
if (!path) throw new Error('Download path not available');
|
||||
const fileSize = statSync(path).size;
|
||||
expect(fileSize).toBeGreaterThan(size);
|
||||
expect(fileSize).toBeLessThan(size * 2);
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user