Skip to content

Commit 18ea1b1

Browse files
committed
feat: Integrate Printerz API for PDF generation and add customizable title input
1 parent 3d013ab commit 18ea1b1

7 files changed

Lines changed: 334 additions & 35 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const fetch = require('node-fetch');
2+
3+
exports.handler = async function(event, context) {
4+
// Only allow POST requests
5+
if (event.httpMethod !== 'POST') {
6+
return {
7+
statusCode: 405,
8+
body: JSON.stringify({ error: 'Method Not Allowed' })
9+
};
10+
}
11+
12+
try {
13+
// Parse the request body
14+
const requestBody = JSON.parse(event.body);
15+
const { templateId, printerzData } = requestBody;
16+
17+
// Validate required fields
18+
if (!templateId || !printerzData) {
19+
return {
20+
statusCode: 400,
21+
body: JSON.stringify({ error: 'Missing required fields: templateId and printerzData' })
22+
};
23+
}
24+
25+
// Get API key from environment variables
26+
const apiKey = process.env.VITE_PRINTERZ_API_KEY;
27+
if (!apiKey) {
28+
return {
29+
statusCode: 500,
30+
body: JSON.stringify({ error: 'API key not configured' })
31+
};
32+
}
33+
34+
// Make the request to Printerz API
35+
const response = await fetch(`https://api.printerz.dev/templates/${templateId}/render`, {
36+
method: 'POST',
37+
headers: {
38+
'x-api-key': apiKey,
39+
'Content-Type': 'application/json'
40+
},
41+
body: JSON.stringify(printerzData)
42+
});
43+
44+
// If the API call failed, return the error
45+
if (!response.ok) {
46+
const errorText = await response.text();
47+
return {
48+
statusCode: response.status,
49+
body: errorText
50+
};
51+
}
52+
53+
// Get the PDF data as an ArrayBuffer
54+
const pdfBuffer = await response.arrayBuffer();
55+
56+
// Return the PDF as binary data
57+
return {
58+
statusCode: 200,
59+
headers: {
60+
'Content-Type': 'application/pdf',
61+
'Content-Disposition': 'attachment; filename="transcription.pdf"'
62+
},
63+
body: Buffer.from(pdfBuffer).toString('base64'),
64+
isBase64Encoded: true
65+
};
66+
} catch (error) {
67+
console.error('Error in printerz-proxy function:', error);
68+
return {
69+
statusCode: 500,
70+
body: JSON.stringify({ error: 'Internal Server Error', message: error.message })
71+
};
72+
}
73+
};

package-lock.json

Lines changed: 28 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dependencies": {
1515
"@ffmpeg/ffmpeg": "^0.12.6",
1616
"@ffmpeg/util": "^0.12.1",
17+
"@radix-ui/react-label": "^2.1.2",
1718
"@radix-ui/react-slot": "^1.1.2",
1819
"@radix-ui/react-tabs": "^1.1.3",
1920
"@tabler/icons-react": "^3.31.0",

src/components/TranscriptionResult.tsx

Lines changed: 136 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import JSZip from 'jszip';
55
import { saveAs } from 'file-saver';
66
import { marked } from 'marked';
77
import { Document as DocxDocument, Paragraph, Packer } from 'docx';
8-
import { jsPDF } from 'jspdf';
98
// Add these imports for UTF-8 font support
109
import 'jspdf-autotable';
1110
import pdfMake from 'pdfmake/build/pdfmake';
1211
import pdfFonts from 'pdfmake/build/vfs_fonts';
12+
import { Input } from './ui/input';
13+
import { Label } from './ui/label';
1314

1415
interface TranscriptionResultProps {
1516
transcription: string;
@@ -20,7 +21,19 @@ export function TranscriptionResult({ transcription }: TranscriptionResultProps)
2021
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
2122
const [isGeneratingDocx, setIsGeneratingDocx] = useState(false);
2223
const [fontsLoaded, setFontsLoaded] = useState(false);
23-
const [pdfTitle, setPdfTitle] = useState("Transcription");
24+
25+
// Set default title with timestamp
26+
const getDefaultTitle = () => {
27+
const now = new Date();
28+
return `Transcription ${now.toLocaleDateString('en-US', {
29+
weekday: 'short',
30+
month: 'short',
31+
day: 'numeric',
32+
year: 'numeric'
33+
})} ${now.toLocaleTimeString('en-US')}`;
34+
};
35+
36+
const [pdfTitle, setPdfTitle] = useState(getDefaultTitle());
2437

2538
useEffect(() => {
2639
// Initialize pdfMake with default fonts
@@ -154,15 +167,82 @@ export function TranscriptionResult({ transcription }: TranscriptionResultProps)
154167
saveAs(content, "transcription-files.zip");
155168
};
156169

157-
const handlePrinterzExport = () => {
158-
const printerzData = {
159-
title: pdfTitle, // Use the custom title from your state
160-
content: transcription
161-
};
170+
const handlePrinterzExport = async () => {
171+
try {
172+
// Get current date and time in a readable format
173+
const now = new Date();
174+
const formattedDate = now.toLocaleString('en-US', {
175+
weekday: 'short',
176+
month: 'short',
177+
day: 'numeric',
178+
year: 'numeric',
179+
hour: '2-digit',
180+
minute: '2-digit',
181+
second: '2-digit'
182+
});
183+
184+
// Default title includes timestamp if user hasn't changed it
185+
if (pdfTitle === "Transcription") {
186+
setPdfTitle(`Transcription ${formattedDate}`);
187+
}
188+
189+
// Prepare data for Printerz
190+
const printerzData = {
191+
variables: {
192+
title: pdfTitle,
193+
content: transcription,
194+
timestamp: formattedDate
195+
},
196+
options: {
197+
printBackground: true
198+
}
199+
};
200+
201+
const templateId = '9fa3ff8e-c6dc-49b5-93ba-3532638cfe47';
202+
203+
// Show loading indicator
204+
setIsGeneratingPdf(true);
205+
206+
// Use our proxy server instead of calling Printerz directly
207+
const isNetlify = typeof window !== 'undefined' &&
208+
(window.location.hostname.includes('netlify.app') ||
209+
process.env.DEPLOY_ENV === 'netlify');
210+
211+
const apiUrl = isNetlify
212+
? '/.netlify/functions/printerz-proxy'
213+
: '/api/printerz/render';
214+
215+
const response = await fetch(apiUrl, {
216+
method: 'POST',
217+
headers: {
218+
'Content-Type': 'application/json'
219+
},
220+
body: JSON.stringify({
221+
templateId,
222+
printerzData
223+
})
224+
});
162225

163-
console.log("Sending to Printerz:", printerzData);
164-
// Integration with Printerz API would go here
165-
// For example: window.open("https://app.printerz.app/generate?template=your-template-id&data=" + encodeURIComponent(JSON.stringify(printerzData)), "_blank");
226+
if (!response.ok) {
227+
throw new Error(`Proxy error: ${response.status} ${response.statusText}`);
228+
}
229+
230+
// Get PDF as blob
231+
const pdfBuffer = await response.arrayBuffer();
232+
const blob = new Blob([pdfBuffer], { type: 'application/pdf' });
233+
234+
// Save the file
235+
saveAs(blob, `${pdfTitle.replace(/[/\\?%*:|"<>]/g, '-')}.pdf`);
236+
237+
// Clear loading state
238+
setIsGeneratingPdf(false);
239+
240+
} catch (error) {
241+
console.error('Error generating PDF with Printerz:', error);
242+
setIsGeneratingPdf(false);
243+
// Show error message to user
244+
alert('Error generating PDF. Please try again later.');
245+
}
166246
};
167247

168248
const renderMarkdown = () => {
@@ -207,35 +287,59 @@ export function TranscriptionResult({ transcription }: TranscriptionResultProps)
207287
</TabsContent>
208288

209289
<TabsContent value="pdf" className="mt-4">
210-
<div className="bg-gray-50 dark:bg-gray-900 rounded-md border border-gray-200 dark:border-gray-800 p-4 min-h-80 flex flex-col items-center">
211-
{isGeneratingPdf ? (
212-
<div className="flex items-center justify-center h-80">
213-
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
214-
</div>
215-
) : pdfUrl ? (
216-
<div className="w-full h-80 overflow-auto">
217-
<iframe
218-
src={pdfUrl}
219-
className="w-full h-full border-0"
220-
title="PDF Preview"
221-
/>
222-
</div>
223-
) : (
224-
<div className="flex flex-col items-center justify-center h-80 space-y-4">
225-
<p className="text-gray-500">Preview your PDF before downloading</p>
226-
<Button onClick={generatePdf} disabled={!fontsLoaded}>
227-
Generate PDF Preview
228-
</Button>
229-
</div>
230-
)}
290+
<div className="bg-gray-50 dark:bg-gray-900 rounded-md border border-gray-200 dark:border-gray-800 p-4">
291+
{/* Add title customization field */}
292+
<div className="mb-4">
293+
<Label htmlFor="pdf-title" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
294+
Choose a title for your PDF
295+
</Label>
296+
<Input
297+
id="pdf-title"
298+
type="text"
299+
value={pdfTitle}
300+
onChange={(e) => setPdfTitle(e.target.value)}
301+
className="block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
302+
placeholder="Transcription with timestamp will be added automatically"
303+
/>
304+
</div>
305+
306+
<div className="min-h-80 flex flex-col items-center">
307+
{isGeneratingPdf ? (
308+
<div className="flex items-center justify-center h-80">
309+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
310+
</div>
311+
) : pdfUrl ? (
312+
<div className="w-full h-80 overflow-auto">
313+
<iframe
314+
src={pdfUrl}
315+
className="w-full h-full border-0"
316+
title="PDF Preview"
317+
/>
318+
</div>
319+
) : (
320+
<div className="flex flex-col items-center justify-center h-80 space-y-4">
321+
<p className="text-gray-500">Preview your PDF before downloading</p>
322+
<Button onClick={generatePdf} disabled={!fontsLoaded}>
323+
Generate PDF Preview
324+
</Button>
325+
</div>
326+
)}
327+
</div>
231328
</div>
232-
<div className="mt-4 flex justify-end">
329+
<div className="mt-4 flex justify-end space-x-2">
233330
<Button
234331
onClick={() => handleDownload('pdf')}
235332
disabled={isGeneratingPdf || !fontsLoaded}
236333
>
237334
{isGeneratingPdf ? 'Generating...' : 'Download as PDF'}
238335
</Button>
336+
<Button
337+
onClick={handlePrinterzExport}
338+
disabled={isGeneratingPdf}
339+
className="bg-emerald-600 hover:bg-emerald-700"
340+
>
341+
{isGeneratingPdf ? 'Generating...' : 'Generate with Printerz'}
342+
</Button>
239343
</div>
240344
</TabsContent>
241345

src/components/ui/input.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6+
return (
7+
<input
8+
type={type}
9+
data-slot="input"
10+
className={cn(
11+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14+
className
15+
)}
16+
{...props}
17+
/>
18+
)
19+
}
20+
21+
export { Input }

0 commit comments

Comments
 (0)