This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-21 08:25:32 +01:00
parent 56f1c5e894
commit a58761e8d7
29 changed files with 777 additions and 298 deletions

View File

@@ -3,22 +3,23 @@ import { EmailInvite, zEmailInvite } from './email-invite';
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 OnboardingFeatureRequest, {
zOnboardingFeatureRequest,
} from './onboarding-feature-request';
import OnboardingTrialEnded, {
zOnboardingTrialEnded,
} from './onboarding-trial-ended';
import OnboardingTrialEnding, {
zOnboardingTrialEnding,
} from './onboarding-trial-ending';
import OnboardingWelcome, { zOnboardingWelcome } from './onboarding-welcome';
import OnboardingWhatToTrack, {
zOnboardingWhatToTrack,
} from './onboarding-what-to-track';
import TrailEndingSoon, { zTrailEndingSoon } from './trial-ending-soon';
export const templates = {
invite: {
@@ -38,31 +39,43 @@ export const templates = {
'Your trial is ending soon',
Component: TrailEndingSoon,
schema: zTrailEndingSoon,
category: 'billing' as const,
},
'onboarding-welcome': {
subject: () => "You're in",
Component: OnboardingWelcome,
schema: zOnboardingWelcome,
category: 'onboarding' as const,
},
'onboarding-what-to-track': {
subject: () => "What's actually worth tracking",
Component: OnboardingWhatToTrack,
schema: zOnboardingWhatToTrack,
category: 'onboarding' as const,
},
'onboarding-dashboards': {
subject: () => 'The part most people skip',
Component: OnboardingDashboards,
schema: zOnboardingDashboards,
category: 'onboarding' as const,
},
'onboarding-replace-stack': {
'onboarding-featue-request': {
subject: () => 'One provider to rule them all',
Component: OnboardingReplaceStack,
schema: zOnboardingReplaceStack,
Component: OnboardingFeatureRequest,
schema: zOnboardingFeatureRequest,
category: 'onboarding' as const,
},
'onboarding-trial-ending': {
subject: () => 'Your trial ends in a few days',
Component: OnboardingTrialEnding,
schema: zOnboardingTrialEnding,
category: 'onboarding' as const,
},
'onboarding-trial-ended': {
subject: () => 'Your trial has ended',
Component: OnboardingTrialEnded,
schema: zOnboardingTrialEnded,
category: 'onboarding' as const,
},
} as const;

View File

@@ -2,6 +2,7 @@ import { Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
import { List } from '../components/list';
export const zOnboardingDashboards = z.object({
firstName: z.string().optional(),
@@ -30,20 +31,34 @@ export function OnboardingDashboards({
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>
<List
items={[
'How many people sign up and then actually do something',
'Where users drop off in a flow (funnel)',
'Which pages lead to conversions (entry page CTA)',
]}
/>
<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>
Best regards,
<br />
Carl
</Text>
<Text>Carl</Text>
<span style={{ margin: '0 -20px', display: 'block' }}>
<img
src="https://openpanel.dev/_next/image?url=%2Fscreenshots%2Fdashboard-dark.webp&w=3840&q=75"
alt="Dashboard"
style={{
width: '100%',
height: 'auto',
borderRadius: '5px',
}}
/>
</span>
</Layout>
);
}

View File

@@ -0,0 +1,39 @@
import { Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
export const zOnboardingFeatureRequest = z.object({
firstName: z.string().optional(),
});
export type Props = z.infer<typeof zOnboardingFeatureRequest>;
export default OnboardingFeatureRequest;
export function OnboardingFeatureRequest({ firstName }: Props) {
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>
OpenPanel aims to be the one stop shop for all your analytics needs.
</Text>
<Text>
We have already in a very short time become one of the most popular
open-source analytics platforms out there and we're working hard to add
more features to make it the best analytics platform.
</Text>
<Text>
Do you feel like you're missing a feature that's important to you? If
that's the case, please reply here or go to our feedback board and add
your request there.
</Text>
<Text>
<Link href={'https://feedback.openpanel.dev'}>Feedback board</Link>
</Text>
<Text>
Best regards,
<br />
Carl
</Text>
</Layout>
);
}

View File

@@ -1,37 +0,0 @@
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,55 @@
import { Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Button } from '../components/button';
import { Layout } from '../components/layout';
export const zOnboardingTrialEnded = z.object({
firstName: z.string().optional(),
billingUrl: z.string(),
recommendedPlan: z.string().optional(),
});
export type Props = z.infer<typeof zOnboardingTrialEnded>;
export default OnboardingTrialEnded;
export function OnboardingTrialEnded({
firstName,
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-ended');
return (
<Layout>
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
<Text>Your OpenPanel trial has ended.</Text>
<Text>
Your tracking is still running in the background, but you won't be able
to see any new data until you upgrade. All your dashboards, reports, and
event history are still there waiting for you.
</Text>
<Text>
Important: If you don't upgrade within 30 days, your workspace and
projects will be permanently deleted.
</Text>
<Text>
To keep your data and continue using OpenPanel, upgrade to a paid plan.{' '}
{recommendedPlan
? `Based on your usage we recommend upgrading to the ${recommendedPlan}`
: 'Plans start at $2.50/month'}
.
</Text>
<Text>
If you have any questions or something's holding you back, just reply to
this email.
</Text>
<Text>
<Button href={newUrl.toString()}>Upgrade Now</Button>
</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -1,6 +1,7 @@
import { Button, Link, Text } from '@react-email/components';
import { Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Button } from '../components/button';
import { Layout } from '../components/layout';
export const zOnboardingTrialEnding = z.object({
@@ -33,32 +34,21 @@ export function OnboardingTrialEnding({
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}` : ''}
To continue using OpenPanel, you'll need to upgrade to a paid plan.{' '}
{recommendedPlan
? `Based on your usage we recommend upgrading to the ${recommendedPlan} plan`
: 'Plans start at $2.50/month'}
.
</Text>
<Text>
If something's holding you back, I'd like to hear about it. Just
reply.
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>
<Button href={newUrl.toString()}>Upgrade Now</Button>
</Text>
<Text>Carl</Text>
</Layout>

View File

@@ -1,7 +1,8 @@
import { Link, Text } from '@react-email/components';
import { Heading, Link, Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
import { List } from '../components/list';
export const zOnboardingWelcome = z.object({
firstName: z.string().optional(),
@@ -10,34 +11,42 @@ export const zOnboardingWelcome = z.object({
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');
export function OnboardingWelcome({ firstName }: Props) {
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.
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>.
We hope you find OpenPanel useful and if you have any questions,
regarding tracking or how to import your existing events, just reach
out. We're here to help.
</Text>
<Text>To get started, you can:</Text>
<List
items={[
<Link
key=""
href={'https://openpanel.dev/docs/get-started/install-openpanel'}
>
Install tracking script
</Link>,
<Link
key=""
href={'https://openpanel.dev/docs/get-started/track-events'}
>
Start tracking your events
</Link>,
]}
/>
<Text>
If you can't find your provider just reach out and we'll help you out.
Best regards,
<br />
Carl
</Text>
<Text>Reach out if you have any questions. I answer all emails.</Text>
<Text>Carl</Text>
</Layout>
);
}

View File

@@ -2,6 +2,7 @@ import { Text } from '@react-email/components';
import React from 'react';
import { z } from 'zod';
import { Layout } from '../components/layout';
import { List } from '../components/list';
export const zOnboardingWhatToTrack = z.object({
firstName: z.string().optional(),
@@ -9,33 +10,34 @@ export const zOnboardingWhatToTrack = z.object({
export type Props = z.infer<typeof zOnboardingWhatToTrack>;
export default OnboardingWhatToTrack;
export function OnboardingWhatToTrack({
firstName,
}: Props) {
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.
Tracking can be overwhelming at first, and that's why its important to
focus on what's matters. For most products, that's something like:
</Text>
<Text>For most products, that's something like:</Text>
<Text>- Signups</Text>
<List
items={[
'Find good funnels to track (onboarding or checkout)',
'Conversions (how many clicks your hero CTA)',
'What did the user do after clicking the CTA',
]}
/>
<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.
Start small and incrementally add more events as you go is usually the
best approach.
</Text>
<Text>
If you're not sure whether something's worth tracking, just ask. I'm
happy to look at your setup.
If you're not sure whether something's worth tracking, or have any
questions, just reply here.
</Text>
<Text>
Best regards,
<br />
Carl
</Text>
<Text>Carl</Text>
</Layout>
);
}