chore: Add free tools on marketing pages
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Color palette for the email editor toolbar
|
||||
* Organized by hue using Tailwind color values
|
||||
*/
|
||||
export const EDITOR_COLOR_GROUPS = [
|
||||
{
|
||||
name: 'Neutrals',
|
||||
colors: ['#000000', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#F3F4F6', '#FFFFFF'],
|
||||
},
|
||||
{
|
||||
name: 'Reds',
|
||||
colors: ['#7F1D1D', '#991B1B', '#DC2626', '#EF4444', '#F87171', '#FCA5A5', '#FEE2E2'],
|
||||
},
|
||||
{
|
||||
name: 'Oranges',
|
||||
colors: ['#7C2D12', '#C2410C', '#EA580C', '#F97316', '#FB923C', '#FDBA74', '#FED7AA'],
|
||||
},
|
||||
{
|
||||
name: 'Yellows',
|
||||
colors: ['#713F12', '#A16207', '#CA8A04', '#EAB308', '#FACC15', '#FDE047', '#FEF08A'],
|
||||
},
|
||||
{
|
||||
name: 'Greens',
|
||||
colors: ['#14532D', '#15803D', '#16A34A', '#22C55E', '#4ADE80', '#86EFAC', '#BBF7D0'],
|
||||
},
|
||||
{
|
||||
name: 'Blues',
|
||||
colors: ['#1E3A8A', '#1D4ED8', '#2563EB', '#3B82F6', '#60A5FA', '#93C5FD', '#DBEAFE'],
|
||||
},
|
||||
{
|
||||
name: 'Purples',
|
||||
colors: ['#581C87', '#6B21A8', '#7C3AED', '#8B5CF6', '#A78BFA', '#C4B5FD', '#E9D5FF'],
|
||||
},
|
||||
{
|
||||
name: 'Pinks',
|
||||
colors: ['#831843', '#9F1239', '#DB2777', '#EC4899', '#F472B6', '#F9A8D4', '#FBCFE8'],
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,170 @@
|
||||
import juice from 'juice';
|
||||
|
||||
/**
|
||||
* Converts modern HTML from Tiptap to email-friendly HTML
|
||||
* - Inlines CSS styles
|
||||
* - Adds email-safe defaults
|
||||
* - Preserves variable placeholders like {{email}}
|
||||
*/
|
||||
export function convertToEmailHtml(html: string): string {
|
||||
// Wrap in email-safe container with basic styling
|
||||
const wrappedHtml = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
color: #111827;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
color: #111827;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #111827;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
a {
|
||||
color: #3B82F6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0 0 16px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
blockquote {
|
||||
margin: 0 0 16px 0;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid #E5E7EB;
|
||||
color: #6B7280;
|
||||
}
|
||||
code {
|
||||
background-color: #F3F4F6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #E5E7EB;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #F3F4F6;
|
||||
font-weight: 600;
|
||||
}
|
||||
.variable-placeholder {
|
||||
display: inline;
|
||||
background-color: #DBEAFE;
|
||||
color: #1E40AF;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #3B82F6;
|
||||
color: #FFFFFF;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 8px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Inline CSS using juice
|
||||
const inlined = juice(wrappedHtml, {
|
||||
preserveMediaQueries: false,
|
||||
preserveFontFaces: false,
|
||||
removeStyleTags: true,
|
||||
applyStyleTags: true,
|
||||
});
|
||||
|
||||
// Extract just the body content
|
||||
const bodyMatch = inlined.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
const bodyContent = bodyMatch && bodyMatch[1] ? bodyMatch[1].trim() : inlined;
|
||||
|
||||
// Clean up Tiptap-specific artifacts
|
||||
const cleaned = bodyContent
|
||||
.replace(/\sdata-pm-slice="[^"]*"/g, '')
|
||||
.replace(/\sclass=""/g, '')
|
||||
.replace(/\sstyle=""/g, '');
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps email fragment HTML in a complete HTML document structure
|
||||
* This is what people expect when they want to send an email
|
||||
*/
|
||||
export function wrapEmailHtml(bodyContent: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email</title>
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts modern HTML from Tiptap to a complete, ready-to-send email HTML
|
||||
* - Inlines CSS styles
|
||||
* - Adds email-safe defaults
|
||||
* - Wraps in complete HTML document structure
|
||||
* - Preserves variable placeholders like {{email}}
|
||||
*/
|
||||
export function convertToCompleteEmailHtml(html: string): string {
|
||||
const fragment = convertToEmailHtml(html);
|
||||
return wrapEmailHtml(fragment);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface EmailVerificationResult {
|
||||
email: string;
|
||||
valid: boolean;
|
||||
isDisposable: boolean;
|
||||
isTypo: boolean;
|
||||
isPlusAddressed: boolean;
|
||||
domainExists: boolean;
|
||||
hasMxRecords: boolean;
|
||||
suggestedEmail?: string;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export async function verifyEmail(email: string): Promise<EmailVerificationResult> {
|
||||
const response = await fetch('/api/verify-email', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({email}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Verification failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {CheckCircle, Mail, Search, Shield} from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Educational content for the email verification tool
|
||||
*/
|
||||
export const EMAIL_VERIFICATION_FEATURES = [
|
||||
{
|
||||
title: 'Improve Deliverability',
|
||||
description: 'Remove invalid emails before sending to reduce bounce rates and improve email deliverability.',
|
||||
icon: Mail,
|
||||
},
|
||||
{
|
||||
title: 'Catch Typos',
|
||||
description: 'Detect common typos like "gmial.com" and suggest corrections to capture valid addresses.',
|
||||
icon: Search,
|
||||
},
|
||||
{
|
||||
title: 'Protect Reputation',
|
||||
description: 'High bounce rates hurt your sender reputation. Verify emails to maintain a good standing.',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
title: 'DNS Validation',
|
||||
description: 'Check if the email domain exists and has properly configured MX records for receiving mail.',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
{
|
||||
title: 'Disposable Detection',
|
||||
description: 'Identify temporary email addresses that are often used for spam or fake signups.',
|
||||
icon: Mail,
|
||||
},
|
||||
{
|
||||
title: 'Plus Addressing',
|
||||
description: 'Detect plus-addressed emails ([email protected]) which can be useful for tracking.',
|
||||
icon: Shield,
|
||||
},
|
||||
] as const;
|
||||
Reference in New Issue
Block a user