This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-20 12:34:56 +01:00
parent 6e997e62f1
commit 56f1c5e894
16 changed files with 601 additions and 4 deletions

View File

@@ -4,6 +4,21 @@ import EmailResetPassword, {
zEmailResetPassword,
} from './email-reset-password';
import TrailEndingSoon, { zTrailEndingSoon } from './trial-ending-soon';
import OnboardingWelcome, {
zOnboardingWelcome,
} from './onboarding-welcome';
import OnboardingWhatToTrack, {
zOnboardingWhatToTrack,
} from './onboarding-what-to-track';
import OnboardingDashboards, {
zOnboardingDashboards,
} from './onboarding-dashboards';
import OnboardingReplaceStack, {
zOnboardingReplaceStack,
} from './onboarding-replace-stack';
import OnboardingTrialEnding, {
zOnboardingTrialEnding,
} from './onboarding-trial-ending';
export const templates = {
invite: {
@@ -24,6 +39,31 @@ export const templates = {
Component: TrailEndingSoon,
schema: zTrailEndingSoon,
},
'onboarding-welcome': {
subject: () => "You're in",
Component: OnboardingWelcome,
schema: zOnboardingWelcome,
},
'onboarding-what-to-track': {
subject: () => "What's actually worth tracking",
Component: OnboardingWhatToTrack,
schema: zOnboardingWhatToTrack,
},
'onboarding-dashboards': {
subject: () => 'The part most people skip',
Component: OnboardingDashboards,
schema: zOnboardingDashboards,
},
'onboarding-replace-stack': {
subject: () => 'One provider to rule them all',
Component: OnboardingReplaceStack,
schema: zOnboardingReplaceStack,
},
'onboarding-trial-ending': {
subject: () => 'Your trial ends in a few days',
Component: OnboardingTrialEnding,
schema: zOnboardingTrialEnding,
},
} as const;
export type Templates = typeof templates;

View File

@@ -0,0 +1,49 @@
import { Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingDashboards = z.object({
firstName: z.string().optional(),
dashboardUrl: z.string(),
});
export type Props = z.infer<typeof zOnboardingDashboards>;
export default OnboardingDashboards;
export function OnboardingDashboards({
firstName,
dashboardUrl = 'https://dashboard.openpanel.dev',
}: Props) {
const newUrl = new URL(dashboardUrl);
newUrl.searchParams.set('utm_source', 'email');
newUrl.searchParams.set('utm_medium', 'email');
newUrl.searchParams.set('utm_campaign', 'onboarding-dashboards');
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>
Tracking events is the easy part. The value comes from actually looking
at them.
</Text>
<Text>
If you haven't yet, try building a simple dashboard. Pick one thing you
care about and visualize it. Could be:
</Text>
<Text>
- How many people sign up and then actually do something
</Text>
<Text>- Where users drop off in a flow (funnel)</Text>
<Text>- Which pages lead to conversions (entry page → CTA)</Text>
<Text>
This is usually when people go from "I have analytics" to "I understand
what's happening." It's a different feeling.
</Text>
<Text>Takes maybe 10 minutes to set up. Worth it.</Text>
<Text>
<Link href={newUrl.toString()}>Create your first dashboard</Link>
</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -0,0 +1,37 @@
import { Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingReplaceStack = z.object({
firstName: z.string().optional(),
});
export type Props = z.infer<typeof zOnboardingReplaceStack>;
export default OnboardingReplaceStack;
export function OnboardingReplaceStack({
firstName,
}: Props) {
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>
A lot of people who sign up are using multiple tools: something for
traffic, something for product analytics and something else for seeing
raw events.
</Text>
<Text>OpenPanel can replace that whole setup.</Text>
<Text>
If you're still thinking of web analytics and product analytics as
separate things, try combining them in a single dashboard. Traffic
sources on top, user behavior below. That view tends to be more useful
than either one alone.
</Text>
<Text>
OpenPanel should be able to replace all of them, you can just reach out
if you feel like something is missing.
</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -0,0 +1,66 @@
import { Button, Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingTrialEnding = z.object({
firstName: z.string().optional(),
organizationName: z.string(),
billingUrl: z.string(),
recommendedPlan: z.string().optional(),
});
export type Props = z.infer<typeof zOnboardingTrialEnding>;
export default OnboardingTrialEnding;
export function OnboardingTrialEnding({
firstName,
organizationName = 'your organization',
billingUrl = 'https://dashboard.openpanel.dev',
recommendedPlan,
}: Props) {
const newUrl = new URL(billingUrl);
newUrl.searchParams.set('utm_source', 'email');
newUrl.searchParams.set('utm_medium', 'email');
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ending');
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>Quick heads up: your OpenPanel trial ends soon.</Text>
<Text>
Your tracking will keep working, but you won't be able to see new data
until you upgrade. Everything you've built so far (dashboards, reports,
event history) stays intact.
</Text>
<Text>
If OpenPanel has been useful, upgrading just keeps it going. Plans
start at $2.50/month
{recommendedPlan ? ` and based on your usage we recommend ${recommendedPlan}` : ''}
.
</Text>
<Text>
If something's holding you back, I'd like to hear about it. Just
reply.
</Text>
<Text>
Your project will recieve events for the next 30 days, if you haven't
upgraded by then we'll remove your workspace and projects.
</Text>
<Text>
<Button
href={newUrl.toString()}
style={{
backgroundColor: '#0070f3',
color: 'white',
padding: '12px 20px',
borderRadius: '5px',
textDecoration: 'none',
}}
>
Upgrade Now
</Button>
</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -0,0 +1,43 @@
import { Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingWelcome = z.object({
firstName: z.string().optional(),
dashboardUrl: z.string(),
});
export type Props = z.infer<typeof zOnboardingWelcome>;
export default OnboardingWelcome;
export function OnboardingWelcome({
firstName,
dashboardUrl = 'https://dashboard.openpanel.dev',
}: Props) {
const newUrl = new URL(dashboardUrl);
newUrl.searchParams.set('utm_source', 'email');
newUrl.searchParams.set('utm_medium', 'email');
newUrl.searchParams.set('utm_campaign', 'onboarding-welcome');
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>Thanks for trying OpenPanel.</Text>
<Text>
We built OpenPanel because most analytics tools are either too expensive,
too complicated, or both. OpenPanel is different.
</Text>
<Text>
If you already have setup your tracking you should see your dashboard
getting filled up. If you come from another provider and want to import
your old events you can do that in our{' '}
<Link href={newUrl.toString()}>project settings</Link>.
</Text>
<Text>
If you can't find your provider just reach out and we'll help you out.
</Text>
<Text>Reach out if you have any questions. I answer all emails.</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -0,0 +1,41 @@
import { Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingWhatToTrack = z.object({
firstName: z.string().optional(),
});
export type Props = z.infer<typeof zOnboardingWhatToTrack>;
export default OnboardingWhatToTrack;
export function OnboardingWhatToTrack({
firstName,
}: Props) {
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>
Track the moments that tell you whether your product is working. Track
things that matters to your product the most and then you can easily
create funnels or conversions reports to understand what happening.
</Text>
<Text>For most products, that's something like:</Text>
<Text>- Signups</Text>
<Text>
- The first meaningful action (create something, send something, buy
something)
</Text>
<Text>- Return visits</Text>
<Text>
You don't need 50 events. Five good ones will tell you more than fifty
random ones.
</Text>
<Text>
If you're not sure whether something's worth tracking, just ask. I'm
happy to look at your setup.
</Text>
<Text>Carl</Text>
</Layout>
);
}