use svg instead of puppeteer
This commit is contained in:
parent
32848a9a06
commit
7d4e695d30
5 changed files with 627 additions and 12465 deletions
11376
package-lock.json
generated
11376
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -25,7 +25,6 @@
|
||||||
"@types/highlight.js": "^10.1.0",
|
"@types/highlight.js": "^10.1.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.17.15",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/puppeteer": "^7.0.4",
|
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
"@types/react-dom": "^19.1.1",
|
"@types/react-dom": "^19.1.1",
|
||||||
"astro": "^5.5.6",
|
"astro": "^5.5.6",
|
||||||
|
@ -42,7 +41,6 @@
|
||||||
"next": "^15.1.2",
|
"next": "^15.1.2",
|
||||||
"pocketbase": "^0.25.1",
|
"pocketbase": "^0.25.1",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"puppeteer": "^24.10.1",
|
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { initializeEmailServices, authenticatePocketBase, getStatusColor, getStatusText, getNextStepsText } from '../../../scripts/email/EmailHelpers';
|
import { initializeEmailServices, authenticatePocketBase, getStatusColor, getStatusText, getNextStepsText } from '../../../scripts/email/EmailHelpers';
|
||||||
|
|
||||||
// Add function to generate status image URL
|
// Add function to generate status image URL (now SVG-based)
|
||||||
function getStatusImageUrl(status: string, baseUrl: string = '', emailOptimized: boolean = true): string {
|
function getStatusImageUrl(status: string, baseUrl: string = ''): string {
|
||||||
const emailParam = emailOptimized ? '&email=true' : '';
|
return `${baseUrl}/api/generate-status-image?status=${status}&width=500&height=150`;
|
||||||
return `${baseUrl}/api/generate-status-image?status=${status}&width=500&height=150${emailParam}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ url }) => {
|
export const GET: APIRoute = async ({ url }) => {
|
||||||
try {
|
try {
|
||||||
const searchParams = new URL(url).searchParams;
|
const searchParams = new URL(url).searchParams;
|
||||||
const status = searchParams.get('status') || 'submitted';
|
const status = searchParams.get('status') || 'submitted';
|
||||||
const width = parseInt(searchParams.get('width') || '600');
|
const width = parseInt(searchParams.get('width') || '500');
|
||||||
const height = parseInt(searchParams.get('height') || '200');
|
const height = parseInt(searchParams.get('height') || '150');
|
||||||
const emailOptimized = searchParams.get('email') === 'true'; // New email optimization flag
|
|
||||||
|
|
||||||
console.log('🎨 Generating status image for:', { status, width, height, emailOptimized });
|
console.log('🎨 Generating SVG status image for:', { status, width, height });
|
||||||
|
|
||||||
// Generate status progress bar HTML (email-optimized version for transparent backgrounds)
|
// Generate SVG status progress bar
|
||||||
function generateEmailOptimizedProgressBar(currentStatus: string): string {
|
function generateSVGProgressBar(currentStatus: string): string {
|
||||||
const statusOrder = ['submitted', 'under_review', 'approved', 'in_progress', 'paid'];
|
const statusOrder = ['submitted', 'under_review', 'approved', 'in_progress', 'paid'];
|
||||||
const rejectedStatus = ['submitted', 'under_review', 'rejected'];
|
const rejectedStatus = ['submitted', 'under_review', 'rejected'];
|
||||||
|
|
||||||
|
@ -38,429 +36,133 @@ export const GET: APIRoute = async ({ url }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentIndex = statuses.indexOf(currentStatus);
|
const currentIndex = statuses.indexOf(currentStatus);
|
||||||
|
const circleRadius = 22;
|
||||||
|
const lineY = height / 2;
|
||||||
|
const totalWidth = width * 0.8; // Use 80% of width
|
||||||
|
const startX = width * 0.1; // Start at 10% from left
|
||||||
|
const stepWidth = totalWidth / (statuses.length - 1);
|
||||||
|
|
||||||
return `
|
let svgElements = '';
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
// Generate background line (behind circles) - decreased height
|
||||||
<head>
|
svgElements += `<line x1="${startX}" y1="${lineY + 1}" x2="${startX + totalWidth}" y2="${lineY + 1}" stroke="#e2e8f0" stroke-width="4" opacity="0.6"/>`;
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
// Generate progress line up to current status
|
||||||
<style>
|
if (currentIndex >= 0) {
|
||||||
* { box-sizing: border-box; }
|
const progressEndX = startX + (currentIndex * stepWidth);
|
||||||
body {
|
let progressColor = '#3b82f6'; // Default blue
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
// Set progress color based on current status
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
if (currentStatus === 'rejected') {
|
||||||
background: transparent;
|
progressColor = '#ef4444';
|
||||||
width: ${width * 2}px;
|
} else if (currentStatus === 'paid') {
|
||||||
height: ${height * 2}px;
|
progressColor = '#10b981';
|
||||||
display: flex;
|
} else if (currentStatus === 'in_progress') {
|
||||||
align-items: center;
|
progressColor = '#f59e0b';
|
||||||
justify-content: center;
|
}
|
||||||
overflow: hidden;
|
|
||||||
}
|
svgElements += `<line x1="${startX}" y1="${lineY + 1}" x2="${progressEndX}" y2="${lineY + 1}" stroke="${progressColor}" stroke-width="3" opacity="0.9"/>`;
|
||||||
.progress-wrapper {
|
}
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
// Generate status circles and labels
|
||||||
display: flex;
|
statuses.forEach((statusName, index) => {
|
||||||
flex-direction: column;
|
const x = startX + (index * stepWidth);
|
||||||
align-items: center;
|
const y = lineY;
|
||||||
justify-content: center;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
.progress-title {
|
|
||||||
margin: 0 0 40px 0;
|
|
||||||
color: #1e293b;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.progress-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
height: 120px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.progress-line {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(50% - 14px); /* Align with center of 56px circles (28px from top) */
|
|
||||||
left: 15%;
|
|
||||||
right: 15%;
|
|
||||||
height: 6px;
|
|
||||||
background: linear-gradient(90deg, #e2e8f0 0%, #cbd5e1 100%);
|
|
||||||
border-radius: 3px;
|
|
||||||
z-index: 1;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.status-items {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 80%;
|
|
||||||
z-index: 2;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.status-circle {
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: 4px solid white;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
.status-label {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="progress-wrapper">
|
|
||||||
<h3 class="progress-title">Request Progress</h3>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-line"></div>
|
|
||||||
<div class="status-items">
|
|
||||||
${statuses.map((statusName, index) => {
|
|
||||||
const isActive = index <= currentIndex;
|
|
||||||
const isCurrent = statusName === currentStatus;
|
|
||||||
|
|
||||||
let backgroundColor, textColor;
|
|
||||||
if (isCurrent) {
|
|
||||||
if (statusName === 'rejected') {
|
|
||||||
backgroundColor = '#ef4444';
|
|
||||||
textColor = 'white';
|
|
||||||
} else if (statusName === 'paid') {
|
|
||||||
backgroundColor = '#10b981';
|
|
||||||
textColor = 'white';
|
|
||||||
} else if (statusName === 'in_progress') {
|
|
||||||
backgroundColor = '#f59e0b';
|
|
||||||
textColor = 'white';
|
|
||||||
} else {
|
|
||||||
backgroundColor = '#3b82f6';
|
|
||||||
textColor = 'white';
|
|
||||||
}
|
|
||||||
} else if (isActive) {
|
|
||||||
backgroundColor = '#e2e8f0';
|
|
||||||
textColor = '#475569';
|
|
||||||
} else {
|
|
||||||
backgroundColor = '#f8fafc';
|
|
||||||
textColor = '#94a3b8';
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelColor = isCurrent ?
|
|
||||||
(statusName === 'rejected' ? '#ef4444' :
|
|
||||||
statusName === 'paid' ? '#10b981' :
|
|
||||||
statusName === 'in_progress' ? '#f59e0b' : '#3b82f6') :
|
|
||||||
isActive ? '#475569' : '#94a3b8';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="status-circle" style="background: ${backgroundColor}; color: ${textColor};">
|
|
||||||
${statusIcons[statusName]}
|
|
||||||
</div>
|
|
||||||
<div class="status-label" style="color: ${labelColor};">
|
|
||||||
${statusLabels[statusName]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate status progress bar HTML (based on the email template)
|
|
||||||
function generateStatusProgressBarHTML(currentStatus: string): string {
|
|
||||||
const statusOrder = ['submitted', 'under_review', 'approved', 'in_progress', 'paid'];
|
|
||||||
const rejectedStatus = ['submitted', 'under_review', 'rejected'];
|
|
||||||
|
|
||||||
const isRejected = currentStatus === 'rejected';
|
|
||||||
const statuses = isRejected ? rejectedStatus : statusOrder;
|
|
||||||
|
|
||||||
const statusIcons: Record<string, string> = {
|
|
||||||
submitted: '→',
|
|
||||||
under_review: '?',
|
|
||||||
approved: '✓',
|
|
||||||
rejected: '✗',
|
|
||||||
in_progress: '○',
|
|
||||||
paid: '$'
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
|
||||||
submitted: 'Submitted',
|
|
||||||
under_review: 'Under Review',
|
|
||||||
approved: 'Approved',
|
|
||||||
rejected: 'Rejected',
|
|
||||||
in_progress: 'In Progress',
|
|
||||||
paid: 'Paid'
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentIndex = statuses.indexOf(currentStatus);
|
|
||||||
|
|
||||||
let progressBarHtml = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: transparent;
|
|
||||||
width: ${(width - 40) * 2}px;
|
|
||||||
height: ${(height - 40) * 2}px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.progress-container {
|
|
||||||
background: rgba(248, 250, 252, 0.95);
|
|
||||||
padding: 60px 40px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 2px solid rgba(226, 232, 240, 0.8);
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.progress-title {
|
|
||||||
margin: 0 0 60px 0;
|
|
||||||
color: #1e293b;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.progress-table {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-collapse: collapse;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.progress-line {
|
|
||||||
height: 4px;
|
|
||||||
background: #e2e8f0;
|
|
||||||
position: absolute;
|
|
||||||
top: 42px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.progress-row {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.status-cell {
|
|
||||||
text-align: center;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
position: relative;
|
|
||||||
width: ${100/statuses.length}%;
|
|
||||||
}
|
|
||||||
.status-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
.status-circle {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 50%;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 64px;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: 6px solid rgba(248, 250, 252, 0.95);
|
|
||||||
margin: 0 auto 16px auto;
|
|
||||||
}
|
|
||||||
.status-label {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.connection-cell {
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
position: relative;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
.connection-line {
|
|
||||||
height: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: 42px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="progress-container">
|
|
||||||
<h3 class="progress-title">Request Progress</h3>
|
|
||||||
<table class="progress-table">
|
|
||||||
<tr class="progress-line-row">
|
|
||||||
<td colspan="${statuses.length * 2 - 1}" class="progress-line"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="progress-row">
|
|
||||||
`;
|
|
||||||
|
|
||||||
statuses.forEach((status, index) => {
|
|
||||||
const isActive = index <= currentIndex;
|
const isActive = index <= currentIndex;
|
||||||
const isCurrent = status === currentStatus;
|
const isCurrent = statusName === currentStatus;
|
||||||
|
|
||||||
let backgroundColor, textColor, lineColor;
|
let backgroundColor, textColor;
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
if (status === 'rejected') {
|
if (statusName === 'rejected') {
|
||||||
backgroundColor = '#ef4444';
|
backgroundColor = '#ef4444';
|
||||||
textColor = 'white';
|
textColor = 'white';
|
||||||
lineColor = '#ef4444';
|
} else if (statusName === 'paid') {
|
||||||
} else if (status === 'paid') {
|
|
||||||
backgroundColor = '#10b981';
|
backgroundColor = '#10b981';
|
||||||
textColor = 'white';
|
textColor = 'white';
|
||||||
lineColor = '#10b981';
|
} else if (statusName === 'in_progress') {
|
||||||
} else if (status === 'in_progress') {
|
|
||||||
backgroundColor = '#f59e0b';
|
backgroundColor = '#f59e0b';
|
||||||
textColor = 'white';
|
textColor = 'white';
|
||||||
lineColor = '#f59e0b';
|
|
||||||
} else {
|
} else {
|
||||||
backgroundColor = '#3b82f6';
|
backgroundColor = '#3b82f6';
|
||||||
textColor = 'white';
|
textColor = 'white';
|
||||||
lineColor = '#3b82f6';
|
|
||||||
}
|
}
|
||||||
} else if (isActive) {
|
} else if (isActive) {
|
||||||
backgroundColor = '#e2e8f0';
|
backgroundColor = '#e2e8f0';
|
||||||
textColor = '#475569';
|
textColor = '#475569';
|
||||||
lineColor = '#cbd5e1';
|
|
||||||
} else {
|
} else {
|
||||||
backgroundColor = '#f8fafc';
|
backgroundColor = '#f8fafc';
|
||||||
textColor = '#94a3b8';
|
textColor = '#94a3b8';
|
||||||
lineColor = '#e2e8f0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status circle
|
const labelColor = isCurrent ?
|
||||||
progressBarHtml += `
|
(statusName === 'rejected' ? '#ef4444' :
|
||||||
<td class="status-cell">
|
statusName === 'paid' ? '#10b981' :
|
||||||
<div class="status-content">
|
statusName === 'in_progress' ? '#f59e0b' : '#3b82f6') :
|
||||||
<div class="status-circle" style="
|
isActive ? '#475569' : '#94a3b8';
|
||||||
background: ${backgroundColor};
|
|
||||||
color: ${textColor};
|
|
||||||
">
|
|
||||||
${statusIcons[status]}
|
|
||||||
</div>
|
|
||||||
<div class="status-label" style="
|
|
||||||
color: ${isCurrent ? (status === 'rejected' ? '#ef4444' : status === 'paid' ? '#10b981' : status === 'in_progress' ? '#f59e0b' : '#3b82f6') : isActive ? '#475569' : '#94a3b8'};
|
|
||||||
">
|
|
||||||
${statusLabels[status]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Connecting line (except for the last status)
|
// Circle with shadow effect
|
||||||
if (index < statuses.length - 1) {
|
svgElements += `<circle cx="${x}" cy="${y}" r="${circleRadius}" fill="${backgroundColor}" stroke="white" stroke-width="3" filter="url(#shadow)"/>`;
|
||||||
const nextIsActive = (index + 1) <= currentIndex;
|
|
||||||
const connectionColor = nextIsActive ? lineColor : '#e2e8f0';
|
// Icon text - properly centered with dominant-baseline
|
||||||
|
svgElements += `<text x="${x}" y="${y}" text-anchor="middle" dominant-baseline="central" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="18" font-weight="bold" fill="${textColor}">${statusIcons[statusName]}</text>`;
|
||||||
progressBarHtml += `
|
|
||||||
<td class="connection-cell">
|
// Label text
|
||||||
<div class="connection-line" style="background: ${connectionColor};"></div>
|
svgElements += `<text x="${x}" y="${y + circleRadius + 18}" text-anchor="middle" dominant-baseline="central" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="600" fill="${labelColor}">${statusLabels[statusName]}</text>`;
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
progressBarHtml += `
|
return `
|
||||||
</tr>
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
</table>
|
<defs>
|
||||||
</div>
|
<style>
|
||||||
</body>
|
text {
|
||||||
</html>
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- Drop shadow filter -->
|
||||||
|
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="${width/2}" y="25" text-anchor="middle" dominant-baseline="central" font-size="16" font-weight="700" fill="#1e293b">Request Progress</text>
|
||||||
|
|
||||||
|
${svgElements}
|
||||||
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return progressBarHtml;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose which HTML to use based on email optimization flag
|
const svg = generateSVGProgressBar(status);
|
||||||
const html = emailOptimized ?
|
|
||||||
generateEmailOptimizedProgressBar(status) :
|
|
||||||
generateStatusProgressBarHTML(status);
|
|
||||||
|
|
||||||
// Launch Puppeteer with high quality settings
|
console.log('✅ SVG status image generated successfully');
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
headless: true,
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--force-device-scale-factor=2' // Higher DPI for better quality
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const page = await browser.newPage();
|
return new Response(svg, {
|
||||||
|
|
||||||
// Set high-resolution viewport for better quality
|
|
||||||
await page.setViewport({
|
|
||||||
width: width * 2, // Double resolution for crisp images
|
|
||||||
height: height * 2,
|
|
||||||
deviceScaleFactor: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set HTML content
|
|
||||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
|
||||||
|
|
||||||
// Take high-quality screenshot with transparent background
|
|
||||||
const screenshot = await page.screenshot({
|
|
||||||
type: 'png',
|
|
||||||
fullPage: false,
|
|
||||||
omitBackground: true, // Transparent background
|
|
||||||
clip: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: width * 2,
|
|
||||||
height: height * 2
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
|
|
||||||
console.log('✅ Status image generated successfully');
|
|
||||||
|
|
||||||
return new Response(screenshot, {
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': 'image/svg+xml',
|
||||||
'Cache-Control': 'public, max-age=3600',
|
'Cache-Control': 'public, max-age=3600',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error generating status image:', error);
|
console.error('❌ Error generating SVG status image:', error);
|
||||||
return new Response('Error generating image', { status: 500 });
|
console.error('Error details:', {
|
||||||
|
name: error instanceof Error ? error.name : 'Unknown',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return more detailed error information
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Failed to generate status image',
|
||||||
|
details: errorMessage,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
Loading…
Reference in a new issue