From 3f7b2d51dbcae2610c698df1a0b966bc6a292381 Mon Sep 17 00:00:00 2001 From: Michilis Date: Sun, 23 Aug 2026 05:16:41 +0000 Subject: [PATCH] Redesign the PDF ticket around the branded card layout. The old ticket was a centred stack of Helvetica on white that read as a receipt: the QR sat in open space, the event, attendee and code lines were indistinguishable at a glance, and nothing on the page identified Spanglish beyond a text heading. The page is now a full-bleed card -- orange rule, cream field, navy footer -- with the logo and event title in the header, the QR raised into a white rounded panel above its code, and labelled venue and ticket holder blocks below it, so door staff can find the code and the name without reading the page. The ticket is bilingual, driven by the ticket's preferredLanguage: the labels, the terms line, the Spanish event title and the date format all follow it, with 24h time and day-first ordering in Spanish. Events store the venue as one string, so the text before the first comma is treated as the venue name and the remainder as its address. Layout adapts rather than overflowing: long titles wrap to two lines at a smaller size, and the QR panel flexes so the detail block always lands just above the footer. The single and combined generators were copies of each other and now share one page renderer, with multi-ticket bookings marked by a small counter in the panel. The logo ships as backend/assets/logo-spanglish.png, resolved from both src/lib and dist/lib, falling back to the frontend copy and then to a text wordmark, so a deployment that misses the asset still produces a ticket. Co-Authored-By: Claude Opus 5 --- backend/assets/logo-spanglish.png | Bin 0 -> 65482 bytes backend/src/lib/pdf.ts | 551 +++++++++++++++++++----------- backend/src/routes/tickets.ts | 37 +- 3 files changed, 376 insertions(+), 212 deletions(-) create mode 100644 backend/assets/logo-spanglish.png diff --git a/backend/assets/logo-spanglish.png b/backend/assets/logo-spanglish.png new file mode 100644 index 0000000000000000000000000000000000000000..e2865c0a3be178adb611c0235f8d51a35ec2803b GIT binary patch literal 65482 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

! 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 literal 0 HcmV?d00001 diff --git a/backend/src/lib/pdf.ts b/backend/src/lib/pdf.ts index b25f4c4..153afe2 100644 --- a/backend/src/lib/pdf.ts +++ b/backend/src/lib/pdf.ts @@ -1,6 +1,8 @@ // PDF Ticket Generation Service import PDFDocument from 'pdfkit'; import QRCode from 'qrcode'; +import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; interface TicketData { id: string; @@ -15,235 +17,390 @@ interface TicketData { locationUrl?: string; }; timezone?: string; + /** 'en' | 'es' - drives the labels and the date/time format on the ticket */ + locale?: string; + /** Optional perk line shown under the ticket holder (falls back to the terms line) */ + note?: string; +} + +// ==================== Brand ==================== + +const COLORS = { + navy: '#002F44', + orange: '#F5821F', + cream: '#FDF8F0', + card: '#FFFFFF', + cardBorder: '#EFE6D8', + divider: '#E7DFD1', + label: '#9AA3AC', + muted: '#6B7580', + footerMuted: '#7FA3B5', +}; + +const PAGE_W = 595.28; +const PAGE_H = 841.89; +const MARGIN = 48; +const CONTENT_W = PAGE_W - MARGIN * 2; +const ACCENT_H = 10; +const FOOTER_H = 48; + +const LOGO_RATIO = 1158 / 324; + +const STRINGS = { + en: { + scan: 'SCAN AT THE ENTRANCE', + venue: 'VENUE', + holder: 'TICKET HOLDER', + terms: 'This ticket is non-transferable. One scan per entry.', + }, + es: { + scan: 'ESCANEÁ AL INGRESAR', + venue: 'LUGAR', + holder: 'TITULAR', + terms: 'Esta entrada es personal e intransferible. Un escaneo por ingreso.', + }, +} as const; + +function strings(locale?: string) { + return locale === 'es' ? STRINGS.es : STRINGS.en; } /** - * Generate a QR code as a data URL + * Locate the logo. `../../assets` resolves to backend/assets from both + * src/lib (tsx) and dist/lib (compiled), with the frontend copy as a fallback. + */ +function loadLogo(): Buffer | null { + const candidates = [ + new URL('../../assets/logo-spanglish.png', import.meta.url), + new URL('../../../frontend/public/images/logo-spanglish.png', import.meta.url), + ].map((u) => fileURLToPath(u)); + + for (const path of candidates) { + if (existsSync(path)) return readFileSync(path); + } + return null; +} + +let logoCache: Buffer | null | undefined; +function getLogo(): Buffer | null { + if (logoCache === undefined) logoCache = loadLogo(); + return logoCache; +} + +/** + * Generate a QR code as a PNG buffer */ async function generateQRCode(data: string): Promise { return QRCode.toBuffer(data, { type: 'png', - width: 200, - margin: 2, + width: 600, + margin: 1, errorCorrectionLevel: 'M', + color: { dark: '#000000', light: '#FFFFFF' }, }); } /** - * Format date for display using site timezone + * Short date + time as shown in the ticket header: + * en -> "JUL 25 · 4:30 PM" es -> "25 JUL · 16:30" */ -function formatDate(dateStr: string, timezone: string = 'America/Asuncion'): string { - const date = new Date(dateStr); - return date.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - timeZone: timezone, - }); +function formatWhen( + startStr: string, + endStr: string | undefined, + timezone: string, + locale: string +): string { + const isEs = locale === 'es'; + const start = new Date(startStr); + const tag = isEs ? 'es-ES' : 'en-US'; + + const day = start.toLocaleDateString(tag, { day: 'numeric', timeZone: timezone }); + const month = start + .toLocaleDateString(tag, { month: 'short', timeZone: timezone }) + .replace(/\.$/, '') + .toUpperCase(); + + const time = (d: Date) => + d + .toLocaleTimeString(tag, { + hour: isEs ? '2-digit' : 'numeric', + minute: '2-digit', + hour12: !isEs, + timeZone: timezone, + }) + .toUpperCase(); + + const date = isEs ? `${day} ${month}` : `${month} ${day}`; + const end = endStr ? new Date(endStr) : null; + const when = end ? `${time(start)} – ${time(end)}` : time(start); + + return `${date} · ${when}`; } /** - * Format time for display using site timezone + * Events store the venue as a single string; the part before the first comma + * reads as the venue name and the remainder as its address. */ -function formatTime(dateStr: string, timezone: string = 'America/Asuncion'): string { - const date = new Date(dateStr); - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true, - timeZone: timezone, +function splitLocation(location: string): { name: string; address?: string } { + const idx = location.indexOf(','); + if (idx === -1) return { name: location.trim() }; + return { + name: location.slice(0, idx).trim(), + address: location.slice(idx + 1).trim() || undefined, + }; +} + +// ==================== Drawing helpers ==================== + +function drawLabel(doc: PDFKit.PDFDocument, text: string, y: number, width = CONTENT_W, x = MARGIN) { + doc + .font('Helvetica-Bold') + .fontSize(8) + .fillColor(COLORS.label) + .text(text.toUpperCase(), x, y, { width, characterSpacing: 1.6 }); +} + +function drawDivider(doc: PDFKit.PDFDocument, y: number) { + doc + .moveTo(MARGIN, y) + .lineTo(PAGE_W - MARGIN, y) + .lineWidth(1) + .strokeColor(COLORS.divider) + .stroke(); +} + +/** Centered text with letter spacing: pdfkit also spaces the last glyph, so nudge it back. */ +function drawSpacedCentered( + doc: PDFKit.PDFDocument, + text: string, + x: number, + y: number, + width: number, + spacing: number +) { + doc.text(text, x - spacing / 2, y, { width, align: 'center', characterSpacing: spacing }); +} + +interface DetailBlock { + label: string; + value: string; + sub?: string; +} + +/** + * Draw (or, with `measureOnly`, just measure) the venue / ticket holder / note + * block. Returns its total height so the caller can anchor it above the footer. + */ +function renderDetails( + doc: PDFKit.PDFDocument, + blocks: DetailBlock[], + note: string, + yStart: number, + measureOnly: boolean +): number { + let y = yStart; + + blocks.forEach((block, i) => { + if (i > 0) { + y += 14; + if (!measureOnly) drawDivider(doc, y); + y += 18; + } + + if (!measureOnly) drawLabel(doc, block.label, y); + y += 15; + + doc.font('Helvetica-Bold').fontSize(13); + if (!measureOnly) doc.fillColor(COLORS.navy).text(block.value, MARGIN, y, { width: CONTENT_W }); + y += doc.heightOfString(block.value, { width: CONTENT_W }) + 3; + + if (block.sub) { + doc.font('Helvetica').fontSize(10.5); + if (!measureOnly) doc.fillColor(COLORS.muted).text(block.sub, MARGIN, y, { width: CONTENT_W }); + y += doc.heightOfString(block.sub, { width: CONTENT_W }) + 3; + } }); + + y += 16; + doc.font('Helvetica').fontSize(10.5); + if (!measureOnly) doc.fillColor(COLORS.muted).text(note, MARGIN, y, { width: CONTENT_W }); + y += doc.heightOfString(note, { width: CONTENT_W }); + + return y - yStart; +} + +/** + * Render one full-page ticket. Assumes the page is already added. + */ +function renderTicketPage( + doc: PDFKit.PDFDocument, + ticket: TicketData, + qrBuffer: Buffer, + siteDomain: string, + index = 0, + total = 1 +) { + const locale = ticket.locale === 'es' ? 'es' : 'en'; + const t = strings(locale); + const tz = ticket.timezone || 'America/Asuncion'; + const footerY = PAGE_H - FOOTER_H; + + // ==================== Background ==================== + doc.rect(0, 0, PAGE_W, PAGE_H).fill(COLORS.cream); + doc.rect(0, 0, PAGE_W, ACCENT_H).fill(COLORS.orange); + + // ==================== Logo ==================== + const logo = getLogo(); + let headerY = MARGIN + 6; + + if (logo) { + const logoW = 158; + doc.image(logo, MARGIN, headerY, { width: logoW }); + headerY += logoW / LOGO_RATIO; + } else { + doc.font('Helvetica-Bold').fontSize(21).fillColor(COLORS.navy).text('spanglish social', MARGIN, headerY); + headerY += 26; + } + + // ==================== Title + date ==================== + const titleY = headerY + 30; + const when = formatWhen(ticket.event.startDatetime, ticket.event.endDatetime, tz, locale); + + doc.font('Helvetica-Bold').fontSize(11.5); + const whenW = Math.min(doc.widthOfString(when) + 2, CONTENT_W * 0.5); + const titleW = CONTENT_W - whenW - 20; + + doc.font('Helvetica-Bold').fontSize(26); + if (doc.widthOfString(ticket.event.title) > titleW) doc.fontSize(20); + doc.fillColor(COLORS.navy).text(ticket.event.title, MARGIN, titleY, { width: titleW }); + const titleBottom = doc.y; + + doc + .font('Helvetica-Bold') + .fontSize(11.5) + .fillColor(COLORS.orange) + .text(when, PAGE_W - MARGIN - whenW, titleY + 9, { width: whenW, align: 'right' }); + + // ==================== Layout: card fills what the detail block leaves ==================== + const venue = splitLocation(ticket.event.location); + const note = ticket.note || t.terms; + const blocks: DetailBlock[] = [ + { label: t.venue, value: venue.name, sub: venue.address }, + { label: t.holder, value: ticket.attendeeName, sub: ticket.attendeeEmail }, + ]; + + const detailsH = renderDetails(doc, blocks, note, 0, true); + const detailsY = footerY - 46 - detailsH; + + const cardY = Math.max(titleBottom, titleY + 36) + 24; + const cardX = MARGIN; + const cardW = CONTENT_W; + const cardH = Math.max(300, Math.min(detailsY - 32 - cardY, 430)); + + doc + .roundedRect(cardX, cardY, cardW, cardH, 14) + .lineWidth(1) + .fillAndStroke(COLORS.card, COLORS.cardBorder); + + // ==================== QR card contents ==================== + const labelH = 12; + const codeH = 24; + const qrSize = Math.min(236, cardH - (labelH + 20 + 22 + codeH + 44)); + const stackH = labelH + 20 + qrSize + 22 + codeH; + let inner = cardY + (cardH - stackH) / 2; + + doc.font('Helvetica-Bold').fontSize(8.5).fillColor(COLORS.label); + drawSpacedCentered(doc, t.scan, cardX, inner, cardW, 2); + + if (total > 1) { + doc + .font('Helvetica-Bold') + .fontSize(8.5) + .fillColor(COLORS.label) + .text(`${index + 1} / ${total}`, cardX, inner, { width: cardW - 22, align: 'right', characterSpacing: 1 }); + } + + inner += labelH + 20; + doc.image(qrBuffer, (PAGE_W - qrSize) / 2, inner, { width: qrSize, height: qrSize }); + inner += qrSize + 22; + + const code = ticket.qrCode || ticket.id.slice(0, 8).toUpperCase(); + doc.font('Courier-Bold').fontSize(19).fillColor(COLORS.navy); + drawSpacedCentered(doc, code, cardX, inner, cardW, 3); + + // ==================== Venue / ticket holder / note ==================== + renderDetails(doc, blocks, note, detailsY, false); + + // ==================== Footer ==================== + doc.rect(0, footerY, PAGE_W, FOOTER_H).fill(COLORS.navy); + + doc + .font('Courier') + .fontSize(7.5) + .fillColor(COLORS.footerMuted) + .text(ticket.id, MARGIN, footerY + FOOTER_H / 2 - 4, { width: CONTENT_W * 0.6, lineBreak: false }); + + doc + .font('Helvetica') + .fontSize(10) + .fillColor('#FFFFFF') + .text(siteDomain, MARGIN, footerY + FOOTER_H / 2 - 5.5, { width: CONTENT_W, align: 'right' }); +} + +function createDoc(): PDFKit.PDFDocument { + return new PDFDocument({ size: 'A4', margin: 0 }); +} + +function collect(doc: PDFKit.PDFDocument): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + doc.on('data', (chunk: Buffer) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + }); +} + +function siteUrl(): { base: string; domain: string } { + const base = process.env.FRONTEND_URL || 'https://spanglishcommunity.com'; + let domain = base; + try { + domain = new URL(base).host.replace(/^www\./, ''); + } catch { + domain = base.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/$/, ''); + } + return { base, domain }; } /** * Generate a PDF ticket for a single ticket */ export async function generateTicketPDF(ticket: TicketData): Promise { - return new Promise(async (resolve, reject) => { - try { - const doc = new PDFDocument({ - size: 'A4', - margin: 50, - }); - - const chunks: Buffer[] = []; - doc.on('data', (chunk: Buffer) => chunks.push(chunk)); - doc.on('end', () => resolve(Buffer.concat(chunks))); - doc.on('error', reject); - - const frontendUrl = process.env.FRONTEND_URL || 'https://spanglishcommunity.com'; - - // Generate QR code with ticket URL - const qrUrl = `${frontendUrl}/ticket/${ticket.id}`; - const qrBuffer = await generateQRCode(qrUrl); - - // ==================== Header ==================== - doc.fontSize(28).fillColor('#1a1a1a').text('Spanglish', { align: 'center' }); - doc.moveDown(0.5); - doc.fontSize(12).fillColor('#666').text('Language Exchange Community', { align: 'center' }); - - // Divider line - doc.moveDown(1); - doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); - doc.moveDown(1); - - // ==================== Event Info ==================== - doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' }); - doc.moveDown(0.5); - - // Date and time (using site timezone) - const tz = ticket.timezone || 'America/Asuncion'; - doc.fontSize(14).fillColor('#333'); - doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' }); - - const startTime = formatTime(ticket.event.startDatetime, tz); - const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null; - const timeRange = endTime ? `${startTime} - ${endTime}` : startTime; - doc.text(timeRange, { align: 'center' }); - - doc.moveDown(0.5); - doc.fontSize(12).fillColor('#666').text(ticket.event.location, { align: 'center' }); - - // ==================== QR Code ==================== - doc.moveDown(2); - - // Center the QR code - const qrSize = 180; - const pageWidth = 595; // A4 width in points - const qrX = (pageWidth - qrSize) / 2; - - doc.image(qrBuffer, qrX, doc.y, { width: qrSize, height: qrSize }); - doc.y += qrSize + 10; - - // ==================== Attendee Info ==================== - doc.moveDown(1); - doc.fontSize(16).fillColor('#1a1a1a').text(ticket.attendeeName, { align: 'center' }); - - if (ticket.attendeeEmail) { - doc.fontSize(10).fillColor('#888').text(ticket.attendeeEmail, { align: 'center' }); - } - - // ==================== Ticket ID ==================== - doc.moveDown(1); - doc.fontSize(9).fillColor('#aaa').text(`Ticket ID: ${ticket.id}`, { align: 'center' }); - doc.text(`Code: ${ticket.qrCode}`, { align: 'center' }); - - // ==================== Footer ==================== - doc.moveDown(2); - doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); - doc.moveDown(0.5); - - doc.fontSize(10).fillColor('#888').text('Scan this QR code at the entrance', { align: 'center' }); - doc.moveDown(0.3); - doc.fontSize(8).fillColor('#aaa').text('This ticket is non-transferable. One scan per entry.', { align: 'center' }); - - doc.end(); - } catch (error) { - reject(error); - } - }); + return generateCombinedTicketsPDF([ticket]); } /** - * Generate a combined PDF with multiple tickets + * Generate a combined PDF with multiple tickets (one page each) */ export async function generateCombinedTicketsPDF(tickets: TicketData[]): Promise { - return new Promise(async (resolve, reject) => { - try { - const doc = new PDFDocument({ - size: 'A4', - margin: 50, - }); + const doc = createDoc(); + const done = collect(doc); + const { base, domain } = siteUrl(); - const chunks: Buffer[] = []; - doc.on('data', (chunk: Buffer) => chunks.push(chunk)); - doc.on('end', () => resolve(Buffer.concat(chunks))); - doc.on('error', reject); + try { + for (let i = 0; i < tickets.length; i++) { + const ticket = tickets[i]; + if (i > 0) doc.addPage(); - const frontendUrl = process.env.FRONTEND_URL || 'https://spanglishcommunity.com'; - - for (let i = 0; i < tickets.length; i++) { - const ticket = tickets[i]; - - if (i > 0) { - doc.addPage(); - } - - // Generate QR code - const qrUrl = `${frontendUrl}/ticket/${ticket.id}`; - const qrBuffer = await generateQRCode(qrUrl); - - // ==================== Header ==================== - doc.fontSize(28).fillColor('#1a1a1a').text('Spanglish', { align: 'center' }); - doc.moveDown(0.5); - doc.fontSize(12).fillColor('#666').text('Language Exchange Community', { align: 'center' }); - - // Divider line - doc.moveDown(1); - doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); - doc.moveDown(1); - - // ==================== Event Info ==================== - doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' }); - doc.moveDown(0.5); - - // Date and time (using site timezone) - const tz = ticket.timezone || 'America/Asuncion'; - doc.fontSize(14).fillColor('#333'); - doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' }); - - const startTime = formatTime(ticket.event.startDatetime, tz); - const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null; - const timeRange = endTime ? `${startTime} - ${endTime}` : startTime; - doc.text(timeRange, { align: 'center' }); - - doc.moveDown(0.5); - doc.fontSize(12).fillColor('#666').text(ticket.event.location, { align: 'center' }); - - // ==================== QR Code ==================== - doc.moveDown(2); - - const qrSize = 180; - const pageWidth = 595; - const qrX = (pageWidth - qrSize) / 2; - - doc.image(qrBuffer, qrX, doc.y, { width: qrSize, height: qrSize }); - doc.y += qrSize + 10; - - // ==================== Attendee Info ==================== - doc.moveDown(1); - doc.fontSize(16).fillColor('#1a1a1a').text(ticket.attendeeName, { align: 'center' }); - - if (ticket.attendeeEmail) { - doc.fontSize(10).fillColor('#888').text(ticket.attendeeEmail, { align: 'center' }); - } - - // ==================== Ticket ID ==================== - doc.moveDown(1); - doc.fontSize(9).fillColor('#aaa').text(`Ticket ID: ${ticket.id}`, { align: 'center' }); - doc.text(`Code: ${ticket.qrCode}`, { align: 'center' }); - - // Ticket number for multi-ticket bookings - if (tickets.length > 1) { - doc.text(`Ticket ${i + 1} of ${tickets.length}`, { align: 'center' }); - } - - // ==================== Footer ==================== - doc.moveDown(2); - doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); - doc.moveDown(0.5); - - doc.fontSize(10).fillColor('#888').text('Scan this QR code at the entrance', { align: 'center' }); - doc.moveDown(0.3); - doc.fontSize(8).fillColor('#aaa').text('This ticket is non-transferable. One scan per entry.', { align: 'center' }); - } - - doc.end(); - } catch (error) { - reject(error); + const qrBuffer = await generateQRCode(`${base}/ticket/${ticket.id}`); + renderTicketPage(doc, ticket, qrBuffer, domain, i, tickets.length); } - }); + doc.end(); + } catch (error) { + doc.end(); + throw error; + } + + return done; } export default { diff --git a/backend/src/routes/tickets.ts b/backend/src/routes/tickets.ts index 9cee47a..ea3ad38 100644 --- a/backend/src/routes/tickets.ts +++ b/backend/src/routes/tickets.ts @@ -548,20 +548,24 @@ ticketsRouter.get('/booking/:bookingId/pdf', async (c) => { ); const timezone = settings?.timezone || 'America/Asuncion'; - const ticketsData = confirmedTickets.map((ticket: any) => ({ - id: ticket.id, - qrCode: ticket.qrCode, - attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(), - attendeeEmail: ticket.attendeeEmail, - event: { - title: event.title, - startDatetime: event.startDatetime, - endDatetime: event.endDatetime, - location: event.location, - locationUrl: event.locationUrl, - }, - timezone, - })); + const ticketsData = confirmedTickets.map((ticket: any) => { + const locale = ticket.preferredLanguage === 'es' ? 'es' : 'en'; + return { + id: ticket.id, + qrCode: ticket.qrCode, + attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(), + attendeeEmail: ticket.attendeeEmail, + event: { + title: locale === 'es' && event.titleEs ? event.titleEs : event.title, + startDatetime: event.startDatetime, + endDatetime: event.endDatetime, + location: event.location, + locationUrl: event.locationUrl, + }, + timezone, + locale, + }; + }); const pdfBuffer = await generateCombinedTicketsPDF(ticketsData); @@ -625,19 +629,22 @@ ticketsRouter.get('/:id/pdf', async (c) => { ); const timezone = settings?.timezone || 'America/Asuncion'; + const locale = ticket.preferredLanguage === 'es' ? 'es' : 'en'; + const pdfBuffer = await generateTicketPDF({ id: ticket.id, qrCode: ticket.qrCode, attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(), attendeeEmail: ticket.attendeeEmail, event: { - title: event.title, + title: locale === 'es' && event.titleEs ? event.titleEs : event.title, startDatetime: event.startDatetime, endDatetime: event.endDatetime, location: event.location, locationUrl: event.locationUrl, }, timezone, + locale, }); // Set response headers for PDF download