Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -10,6 +10,20 @@ type BookingEmailData = {
|
|||||||
price?: number;
|
price?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const APP_BASE_URL = process.env.APP_BASE_URL ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
function formatFriendlyDate(isoDate: string): string {
|
function formatFriendlyDate(isoDate: string): string {
|
||||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
||||||
if (!match) return isoDate;
|
if (!match) return isoDate;
|
||||||
@@ -27,7 +41,7 @@ const BASE_STYLES = `
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #f4f4f5;
|
background-color: #edf7f4;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
* {
|
* {
|
||||||
@@ -44,11 +58,11 @@ function wrapLayout(content: string) {
|
|||||||
<title>Playzer</title>
|
<title>Playzer</title>
|
||||||
<style>${BASE_STYLES}</style>
|
<style>${BASE_STYLES}</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
<body style="margin:0;padding:0;background-color:#edf7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;min-height:100vh;">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#edf7f4;min-height:100vh;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding:32px 16px;">
|
<td align="center" style="padding:32px 16px;">
|
||||||
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:28px;overflow:hidden;box-shadow:0 24px 70px rgba(15,23,42,0.12);border:1px solid rgba(5,9,20,0.1);">
|
||||||
${content}
|
${content}
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@@ -103,63 +117,110 @@ function dateTimeBlock(date: string, startTime: string, endTime: string): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function bookingConfirmationHtml(data: BookingEmailData): string {
|
export function bookingConfirmationHtml(data: BookingEmailData): string {
|
||||||
const formattedPrice =
|
const formattedPrice = formatBookingPrice(data.price ?? 0);
|
||||||
data.price !== undefined && data.price > 0
|
const friendlyDate = formatFriendlyDate(data.date);
|
||||||
? new Intl.NumberFormat('es-AR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'ARS',
|
|
||||||
maximumFractionDigits: Number.isInteger(data.price) ? 0 : 2,
|
|
||||||
}).format(data.price)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const content = `
|
const content = `
|
||||||
${headerSection()}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:32px 32px 8px;">
|
<td style="padding:32px 32px 24px;">
|
||||||
<h1 style="margin:0;font-size:22px;font-weight:700;color:#111827;text-align:center;">
|
|
||||||
Reserva confirmada
|
|
||||||
</h1>
|
|
||||||
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
|
|
||||||
${data.complexName}
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:20px 32px 12px;">
|
|
||||||
${dateTimeBlock(data.date, data.startTime, data.endTime)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
${divider()}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:24px 32px;">
|
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
${detailRow('Código', `<span style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</span>`)}
|
<tr>
|
||||||
${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
|
<td valign="top">
|
||||||
${detailRow('Cliente', data.customerName)}
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f0fdf4;border-radius:999px;padding:4px 12px;">
|
||||||
${formattedPrice ? detailRow('Precio', formattedPrice) : ''}
|
<tr>
|
||||||
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#15803d;text-transform:uppercase;line-height:1.25rem;">
|
||||||
|
✓ Reserva confirmada
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
||||||
|
${data.complexName}
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
<td valign="top" align="right" style="white-space:nowrap;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="padding-right:8px;">
|
||||||
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
||||||
|
</td>
|
||||||
|
<td valign="middle">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
${divider()}
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:24px 32px;">
|
<td style="padding:0 32px 24px;">
|
||||||
<p style="margin:0;font-size:13px;color:#6b7280;text-align:center;line-height:1.5;">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
||||||
Presentá el código de reserva al llegar al complejo.
|
<tr>
|
||||||
<br />
|
<td style="padding:20px 24px;">
|
||||||
Si necesitás cancelar o modificar, contactate directamente con el complejo.
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="bottom">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
||||||
|
Tu turno
|
||||||
</p>
|
</p>
|
||||||
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
||||||
|
${friendlyDate}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
||||||
|
${data.startTime} — ${data.endTime}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td valign="bottom" align="right" style="padding-left:16px;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:rgba(255,255,255,0.8);border-radius:16px;border:1px solid rgba(5,150,105,0.2);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:12px 16px;text-align:right;min-width:120px;">
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:500;color:#6b7280;">Precio del turno</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:20px;font-weight:700;color:#111827;">${formattedPrice}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
|
<td style="padding:0 32px 24px;">
|
||||||
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
||||||
Playzer — Reserva de canchas online
|
<tr>
|
||||||
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cancha
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.courtName} — ${data.sportName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Código de reserva
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
|
||||||
|
${data.bookingCode}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 32px 32px;">
|
||||||
|
<p style="margin:0;font-size:13px;color:#6b7280;text-align:center;line-height:1.5;">
|
||||||
|
Presentá el código de reserva al llegar al complejo.
|
||||||
|
<br />
|
||||||
|
Si necesitás cancelar o modificar, contactate directamente con el complejo.
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ test('rejects when the invite target already belongs to the complex', async () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(caught).toBeInstanceOf(ComplexMembersError);
|
expect(caught).toBeInstanceOf(ComplexMembersError);
|
||||||
expect((caught as ComplexMembersError).status).toBe(409);
|
expect((caught as InstanceType<typeof ComplexMembersError>).status).toBe(409);
|
||||||
expect(transactionMock).not.toHaveBeenCalled();
|
expect(transactionMock).not.toHaveBeenCalled();
|
||||||
expect(sendMailMock).not.toHaveBeenCalled();
|
expect(sendMailMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
apps/frontend/public/playzer-favicon-512-transparent.png
Normal file
BIN
apps/frontend/public/playzer-favicon-512-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Reference in New Issue
Block a user