-
+
@@ -58,108 +72,111 @@ function wrapLayout(content: string) {
`;
}
-function headerSection() {
- return `
-
- |
- Playzer
- |
- `;
-}
+export function bookingConfirmationHtml(data: BookingEmailData): string {
+ const formattedPrice = formatBookingPrice(data.price ?? 0);
+ const friendlyDate = formatFriendlyDate(data.date);
-function divider() {
- return `
+ const content = `
- |
+ |
- | |
+
+
+
+ |
+ ✓ Reserva confirmada
+ |
+
+
+
+ ${data.complexName}
+
+ |
+
+
+
+
+
+ |
+
+ Playzer
+ |
+
+
+ |
|
- `;
-}
+
-function dateTimeBlock(date: string, startTime: string, endTime: string): string {
- const friendlyDate = formatFriendlyDate(date);
-
- return `
-
-
+
+
- |
-
- ${friendlyDate}
+ |
+
+
+ |
+
+ Tu turno
+
+
+ ${friendlyDate}
+
+
+ ${data.startTime} — ${data.endTime}
+
+ |
+
+
+
+ |
+ Precio del turno
+ ${formattedPrice}
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+ Cancha
-
- ${startTime} — ${endTime}
+
+ ${data.courtName} — ${data.sportName}
+
+ |
+
+
+ Código de reserva
+
+
+ ${data.bookingCode}
|
|
- `;
-}
-
-export function bookingConfirmationHtml(data: BookingEmailData): string {
- const formattedPrice =
- data.price !== undefined && data.price > 0
- ? new Intl.NumberFormat('es-AR', {
- style: 'currency',
- currency: 'ARS',
- maximumFractionDigits: Number.isInteger(data.price) ? 0 : 2,
- }).format(data.price)
- : undefined;
-
- const content = `
- ${headerSection()}
-
-
-
- Reserva confirmada
-
-
- ${data.complexName}
-
- |
- |
- ${dateTimeBlock(data.date, data.startTime, data.endTime)}
- |
-
-
- ${divider()}
-
-
-
-
- ${detailRow('Código', `${data.bookingCode}`)}
- ${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
- ${detailRow('Cliente', data.customerName)}
- ${formattedPrice ? detailRow('Precio', formattedPrice) : ''}
-
- |
-
-
- ${divider()}
-
-
- |
+ |
- Presentá el código de reserva al llegar al complejo.
+ Presentá el código de reserva al llegar al complejo.
- Si necesitás cancelar o modificar, contactate directamente con el complejo.
-
- |
-
-
-
- |
-
- Playzer — Reserva de canchas online
+ Si necesitás cancelar o modificar, contactate directamente con el complejo.
|
`;
@@ -168,50 +185,99 @@ export function bookingConfirmationHtml(data: BookingEmailData): string {
}
export function bookingCancelledHtml(data: BookingEmailData): string {
+ const friendlyDate = formatFriendlyDate(data.date);
+
const content = `
- ${headerSection()}
-
-
- Reserva cancelada
-
-
- ${data.complexName}
-
- |
-
-
-
- |
- ${dateTimeBlock(data.date, data.startTime, data.endTime)}
- |
-
-
- ${divider()}
-
-
- |
-
- La reserva ${data.bookingCode}
- fue cancelada. Si tenés dudas, contactate con el complejo.
-
- |
-
-
-
- |
+ |
- ${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
- ${detailRow('Cliente', data.customerName)}
+
+
+
+
+ |
+ ✗ Reserva cancelada
+ |
+
+
+
+ ${data.complexName}
+
+ |
+
+
+
+
+
+ |
+
+ Playzer
+ |
+
+
+ |
+
|
- |
-
- Playzer — Reserva de canchas online
-
+ |
+
+
+ |
+
+ Tu turno
+
+
+ ${friendlyDate}
+
+
+ ${data.startTime} — ${data.endTime}
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+ La reserva ${data.bookingCode}
+ fue cancelada. Si tenés dudas, contactate con el complejo.
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+ Cancha
+
+
+ ${data.courtName} — ${data.sportName}
+
+ |
+
+
+ Cliente
+
+
+ ${data.customerName}
+
+ |
+
+
|
`;
@@ -219,70 +285,101 @@ export function bookingCancelledHtml(data: BookingEmailData): string {
}
export function bookingNoShowHtml(data: BookingEmailData): string {
+ const friendlyDate = formatFriendlyDate(data.date);
+
const content = `
- ${headerSection()}
-
-
- Reserva no concretada
-
-
- ${data.complexName}
-
- |
-
-
-
- |
- ${dateTimeBlock(data.date, data.startTime, data.endTime)}
- |
-
-
- ${divider()}
-
-
- |
-
- La reserva ${data.bookingCode}
- fue registrada como no concretada por falta de asistencia.
-
+ |
+
+
+
+
+
+ |
+ Reserva no concretada
+ |
+
+
+
+ ${data.complexName}
+
+ |
+
+
+
+
+
+ |
+
+ Playzer
+ |
+
+
+ |
+
+
|
-
- ${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
- ${detailRow('Cliente', data.customerName)}
+
+
+ |
+
+ Tu turno
+
+
+ ${friendlyDate}
+
+
+ ${data.startTime} — ${data.endTime}
+
+ |
+
- |
-
- Playzer — Reserva de canchas online
-
- |
- `;
-
- return wrapLayout(content);
-}
-
-function detailRow(label: string, value: string): string {
- return `
-
-
-
+
+
- |
- ${label}
+ |
+
+ La reserva ${data.bookingCode}
+ fue registrada como no concretada por falta de asistencia.
+
|
-
- ${value}
+ |
+
+ |
+
+
+
+
+
+
+ |
+
+ Cancha
+
+
+ ${data.courtName} — ${data.sportName}
+
+ |
+
+
+ Cliente
+
+
+ ${data.customerName}
+
|
|
`;
+
+ return wrapLayout(content);
}
diff --git a/apps/backend/src/lib/auth.ts b/apps/backend/src/lib/auth.ts
index 240ce45..5c9b7e7 100644
--- a/apps/backend/src/lib/auth.ts
+++ b/apps/backend/src/lib/auth.ts
@@ -1,3 +1,4 @@
+import { wrapLayout } from '@/emails/booking-confirmation';
import { dash } from '@better-auth/infra';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
@@ -34,10 +35,73 @@ export const auth = betterAuth({
const verificationUrl = new URL(url);
const appUrl = process.env.APP_BASE_URL ?? 'http://localhost:5173';
verificationUrl.searchParams.set('callbackURL', appUrl);
+
+ const content = `
+
+
+
+
+
+
+
+ |
+ Verificación de email
+ |
+
+
+
+ Verificá tu dirección de correo electrónico
+
+ |
+
+
+
+
+
+ |
+
+ Playzer
+ |
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+ Hacé click en el botón de abajo para verificar tu dirección de correo electrónico y empezar a usar Playzer.
+
+ |
+
+
+
+ |
+
+ |
+
+
+
+ |
+
+ Si no creaste una cuenta en Playzer, ignorá este mensaje.
+
+ |
+ `;
+
await sendMail({
to: user.email,
subject: 'Verificá tu email en Playzer',
- html: `Hacé click para verificar tu email: ${verificationUrl.toString()}`,
+ html: wrapLayout(content),
text: `Hacé click para verificar tu email: ${verificationUrl.toString()}`,
});
},
diff --git a/apps/backend/test/complex/invite-complex-user.test.ts b/apps/backend/test/complex/invite-complex-user.test.ts
index 9d13302..839db55 100644
--- a/apps/backend/test/complex/invite-complex-user.test.ts
+++ b/apps/backend/test/complex/invite-complex-user.test.ts
@@ -80,7 +80,7 @@ test('rejects when the invite target already belongs to the complex', async () =
}
expect(caught).toBeInstanceOf(ComplexMembersError);
- expect((caught as ComplexMembersError).status).toBe(409);
+ expect((caught as InstanceType).status).toBe(409);
expect(transactionMock).not.toHaveBeenCalled();
expect(sendMailMock).not.toHaveBeenCalled();
});
diff --git a/apps/frontend/public/playzer-favicon-512-transparent.png b/apps/frontend/public/playzer-favicon-512-transparent.png
new file mode 100644
index 0000000..c4d6d05
Binary files /dev/null and b/apps/frontend/public/playzer-favicon-512-transparent.png differ
| | | |