From 12739718566e4a456004016a970c0777465046f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20P=C3=B6hland?= Date: Tue, 1 Oct 2024 11:27:34 +0200 Subject: [PATCH] make email templates more dynamic --- src/email.tsx | 4 - src/email/email.tsx | 49 +++---- src/email/event-email-2.tsx | 261 ------------------------------------ src/email/event-email.tsx | 129 ++++++++++++++---- src/email/invite-email.tsx | 34 +++-- src/email/utils.tsx | 26 +--- src/utils.ts | 26 ++++ 7 files changed, 179 insertions(+), 350 deletions(-) delete mode 100644 src/email/event-email-2.tsx diff --git a/src/email.tsx b/src/email.tsx index 9ae9429..dd3bce1 100644 --- a/src/email.tsx +++ b/src/email.tsx @@ -1,13 +1,10 @@ export * from './email/email' export * from './email/event-email' -export * from './email/event-email-2' export * from './email/invite-email' export * from './email/types' export { renderGenericEmail, GeneralEmail, - defaultHost, - getHost, MEDIA, Center, ThankYou, @@ -18,5 +15,4 @@ export { bodyStyle, defaultBodyStyle, type WithUnsubscribeToken, - type IHost, } from './email/utils' diff --git a/src/email/email.tsx b/src/email/email.tsx index fc60425..1ca42dc 100644 --- a/src/email/email.tsx +++ b/src/email/email.tsx @@ -4,6 +4,7 @@ import { Body, Container, Img, Section } from '@react-email/components' import { createTranslator } from 'use-intl' import { NftActivityType } from '../types' +import { defaultHost, IEmailConfig } from '../utils' import type { IBaseNotification, IEmailActivityType, @@ -22,16 +23,14 @@ import { type IOfferTypes, type ITradeTypes, } from './types' -import type { IHost, Translations, WithUnsubscribeToken } from './utils' +import type { Translations, WithUnsubscribeToken } from './utils' import { defaultBodyStyle, - defaultHost, FixedButton, FixedHeading, FixedLink, FixedText, GeneralEmail, - getHost, headingStyle, highlightStyle, hintStyle, @@ -49,26 +48,26 @@ const translations = { [NftActivityType.AUCTION_BID]: { title: 'You have a new bid on {nftName}', description: - 'Hi {name}, there is a new bid of {amount} {token} for your {nftName} on XOXNO.', + 'Hi {name}, there is a new bid of {amount} {token} for your {nftName} on {appName}.', action: 'View bids', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, [NftActivityType.AUCTION_OUT_BID]: { title: 'You have been outbid on {nftName}', description: - 'Hi {name}, your previous bid has been outbid by a new bid of {amount} {token} for {nftName} on XOXNO.', + 'Hi {name}, your previous bid has been outbid by a new bid of {amount} {token} for {nftName} on {appName}.', action: 'View bids', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, [NftActivityType.OFFER_CREATE]: { title: 'You have a new offer on {nftName}', description: - 'Hi {name}, you have received a new offer of {amount} {token} for your {nftName} on XOXNO.', + 'Hi {name}, you have received a new offer of {amount} {token} for your {nftName} on {appName}.', action: 'View offer', hint: '', - footer: 'Check your recent offers on XOXNO', + footer: 'Check your recent offers on {appName}', }, [NftActivityType.OFFER_REJECT]: { title: 'Your offer on {nftName} was declined', @@ -76,7 +75,7 @@ const translations = { 'Hi {name}, we regret to inform you that your offer of {amount} {token} was declined by {owner}.', action: 'View offer', hint: '', - footer: 'Check your recent offers on XOXNO', + footer: 'Check your recent offers on {appName}', }, [NftActivityType.TRADE]: { title: 'Congrats, you sold {nftName}!', @@ -84,7 +83,7 @@ const translations = { 'Hi {name}, we are pleased to inform you that your item {nftName} has been sold for {amount} {token}.', action: 'View item', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, [NftActivityType.OFFER_TRADE]: { title: 'Congrats, you bought {nftName}!', @@ -92,7 +91,7 @@ const translations = { 'Hi {name}, we are pleased to inform you that your offer for {nftName} was accepted for {amount} {token}.', action: 'View item', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, deposit: { title: 'Deposit balance updated', @@ -100,7 +99,7 @@ const translations = { 'Hi {name}, your deposit balance {amount, select, 0 {decreased to {amount} {token}. Visit your profile to top it up again} other {was updated to {amount} {token}}}.', action: 'Go to my profile', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, withdrawDeposit: { title: 'Deposit balance updated', @@ -108,15 +107,15 @@ const translations = { 'Hi {name}, your deposit balance {amount, select, 0 {decreased to {amount} {token}. Visit your profile to top it up again} other {was updated to {amount} {token}}}.', action: 'Go to my profile', hint: '', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, verifyEmail: { title: 'Verify your email address', description: - 'Hi {name}, please enter the following code on XOXNO to verify your email address:', + 'Hi {name}, please enter the following code on {appName} to verify your email address:', action: 'Verification code', hint: 'This code is valid for 10 minutes', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, }, }, @@ -162,7 +161,7 @@ function isVerifyEmail(props: IProps) { } type IProps = { - host?: IHost + host?: IEmailConfig } & ( | { activityType: Exclude @@ -207,7 +206,9 @@ const XOXNOEmail = ({ namespace: `emails.${isATrade ? NftActivityType.TRADE : isAOfferTrade ? NftActivityType.OFFER_TRADE : props.activityType}`, }) - const HOST = getHost(host) + const tPayload = { appName: host.appName } + + const HOST = `https://${host.host}` const isUnsuccess = ( [ @@ -242,7 +243,7 @@ const XOXNOEmail = ({ return ( @@ -273,11 +274,12 @@ const XOXNOEmail = ({ )}
- {t('title', payload)} + {t('title', { ...payload, ...tPayload })} {t.rich('description', { ...payload, + ...tPayload, link: (children) => ( {children} @@ -291,24 +293,25 @@ const XOXNOEmail = ({ {isVerifyEmail(props) ? (
- {t('action')} + {t('action', tPayload)} {props.payload.code} - {t('hint')} + {t('hint', tPayload)}
) : ( - {t('action')} + {t('action', tPayload)} )}
( {children} diff --git a/src/email/event-email-2.tsx b/src/email/event-email-2.tsx deleted file mode 100644 index 10bf534..0000000 --- a/src/email/event-email-2.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import React, { createElement, type ComponentProps } from 'react' - -import { Body, Container, Img, Section } from '@react-email/components' -import { createTranslator } from 'use-intl' - -import { getMapsLink, getOnlineLocation } from '../utils' -import type { IEvent } from './types' -import type { IHost, Translations, WithUnsubscribeToken } from './utils' -import { - bodyStyle, - Center, - defaultBodyStyle, - defaultHost, - FixedButton, - FixedHeading, - FixedLink, - FixedText, - GeneralEmail, - getHost, - MEDIA, - MsFix, - renderGenericEmail, - smallHeadingStyle, - ThankYou, -} from './utils' - -const translations = { - namespace: '', - translations: { - en: { - event: { - meta: 'Your {eventName} Ticket is Here – Claim Now!', - title: 'Your {eventName} Ticket is Here', - greeting: 'Dear {name},', - description: - "We're excited to have you join us at the {eventName}! You can use the QR Code attached to this email to pass the event check-in.", - action: 'CLAIM YOUR DIGITAL TICKET HERE', - hint: 'If you want to get the best experience and a unique collectible as a memory of this event, click below to claim your digital ticket:', - qr: 'If you have trouble accessing the ticket on the website, use the QR Code attached to this email to pass the check-in.', - info: 'For more information and updates, visit our website. If you have any questions, feel free to reach out to us via email.', - maps: 'Open in Google Maps', - footer: 'Thank you for using XOXNO!', - }, - }, - }, -} as const satisfies Translations - -type IProps = { - host?: IHost - name: string - event: IEvent - style?: { - background: string - backgroundColor: string - } -} - -const messages = translations.translations.en - -const EventEmail = ({ - host = defaultHost, - event, - name, - style = defaultBodyStyle, - unsubscribeToken, -}: IProps & WithUnsubscribeToken) => { - const t = createTranslator({ - locale: 'en', - messages, - namespace: 'event', - }) - - const HOST = getHost(host) - - const href = `${HOST}/event/${event.eventId}?guest=${event.ticketId}` - - const mapsLink = getMapsLink(event.location) - - return ( - - {({ unsubscribeSection }) => { - return ( - -
- - -
-
- XOXNO Logo -
-
-
-
- - {t('title', { eventName: event.name })} - - - {t('greeting', { name })} - - - {t.rich('description', { - eventName: event.name, - b: (chunks) => {chunks}, - })} - -
-
- - Picture of ticket - -
-
-
-
- {t('hint')} - - {t('action')} - - - - - - - - -
- Info Icon - - - {t.rich('qr', { b: (chunks) => {chunks} })} - -
-
-
-
- - {event.time} - -
- - - - - - - -
- Location pin - - - {event.location.onlineLink - ? getOnlineLocation(event.location.onlineLink) - : event.location.address} - -
- {event.location.address && event.location.placeId && ( - {t('maps')} - )} -
-
-
- - {t.rich('info', { - xoxnolink: (children) => ( - - {children} - - ), - emaillink: (children) => ( - - {children} - - ), - })} - - -
- {unsubscribeSection} -
-
- - ) - }} -
- ) -} - -export const renderEventEmail2 = async ( - props: ComponentProps -) => { - const Email = createElement(EventEmail, props, null) - - return renderGenericEmail(Email) -} diff --git a/src/email/event-email.tsx b/src/email/event-email.tsx index 7f73112..559bbd6 100644 --- a/src/email/event-email.tsx +++ b/src/email/event-email.tsx @@ -3,23 +3,27 @@ import React, { createElement, type ComponentProps } from 'react' import { Body, Container, Img, Section } from '@react-email/components' import { createTranslator } from 'use-intl' -import { getMapsLink, getOnlineLocation } from '../utils' +import { + defaultHost, + getMapsLink, + getOnlineLocation, + IEmailConfig, +} from '../utils' import type { IEvent } from './types' -import type { IHost, Translations, WithUnsubscribeToken } from './utils' +import type { Translations, WithUnsubscribeToken } from './utils' import { + bodyStyle, Center, defaultBodyStyle, - defaultHost, FixedButton, FixedHeading, FixedLink, FixedText, GeneralEmail, - getHost, - headingStyle, MEDIA, MsFix, renderGenericEmail, + smallHeadingStyle, ThankYou, } from './utils' @@ -29,20 +33,23 @@ const translations = { en: { event: { meta: 'Your {eventName} Ticket is Here – Claim Now!', - title: 'Dear {name},', + title: 'Your {eventName} Ticket is Here', + greeting: 'Dear {name},', description: - "We're excited to have you join us at the {eventName}! Please click the button below to claim your ticket and secure your spot:", - action: 'Claim your ticket', + "We're excited to have you join us at the {eventName}! You can use the QR Code attached to this email to pass the event check-in.", + action: 'CLAIM YOUR DIGITAL TICKET HERE', + hint: 'If you want to get the best experience and a unique collectible as a memory of this event, click below to claim your digital ticket:', + qr: 'If you have trouble accessing the ticket on the website, use the QR Code attached to this email to pass the check-in.', info: 'For more information and updates, visit our website. If you have any questions, feel free to reach out to us via email.', maps: 'Open in Google Maps', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', }, }, }, } as const satisfies Translations type IProps = { - host?: IHost + host?: IEmailConfig name: string event: IEvent style?: { @@ -66,7 +73,9 @@ const EventEmail = ({ namespace: 'event', }) - const HOST = getHost(host) + const tPayload = { appName: host.appName } + + const HOST = `https://${host.host}` const href = `${HOST}/event/${event.eventId}?guest=${event.ticketId}` @@ -74,7 +83,7 @@ const EventEmail = ({ return ( @@ -108,25 +117,83 @@ const EventEmail = ({
- {t('title', { name })} + {t('title', { eventName: event.name, ...tPayload })} + + {t('greeting', { name, ...tPayload })} + - {t('description', { eventName: event.name })} + {t.rich('description', { + ...tPayload, + eventName: event.name, + b: (chunks) => {chunks}, + })} - - {t('action')} -
- Picture of ticket + + Picture of ticket + +
+
+
+
+ {t('hint', tPayload)} + + {t('action', tPayload)} + + + + + + + + +
+ Info Icon + + + {t.rich('qr', { + ...tPayload, + b: (chunks) => {chunks}, + })} + +
- + {event.time}
@@ -153,7 +220,7 @@ const EventEmail = ({ /> - + {event.location.onlineLink ? getOnlineLocation(event.location.onlineLink) : event.location.address} @@ -163,26 +230,32 @@ const EventEmail = ({ {event.location.address && event.location.placeId && ( - {t('maps')} + + {t('maps', tPayload)} + )}
-
+
{t.rich('info', { + ...tPayload, xoxnolink: (children) => ( {children} ), emaillink: (children) => ( - + {children} ), })} - +
{unsubscribeSection} diff --git a/src/email/invite-email.tsx b/src/email/invite-email.tsx index c8d5c12..9eb449a 100644 --- a/src/email/invite-email.tsx +++ b/src/email/invite-email.tsx @@ -4,17 +4,16 @@ import { Body, Container, Img, Section } from '@react-email/components' import { createTranslator } from 'use-intl' import { eventRoles, EventUserRoles } from '../types' -import type { IHost, Translations, WithUnsubscribeToken } from './utils' +import { defaultHost, IEmailConfig } from '../utils' +import type { Translations, WithUnsubscribeToken } from './utils' import { Center, defaultBodyStyle, - defaultHost, FixedButton, FixedHeading, FixedLink, FixedText, GeneralEmail, - getHost, MEDIA, MsFix, renderGenericEmail, @@ -33,7 +32,7 @@ const translations = { "You're invited to join the {eventName} team as {roleName}!", action: 'CLAIM YOUR TEAM SPOT HERE', info: 'For more information and updates, visit our website. If you have any questions, feel free to reach out to us via email.', - footer: 'Thank you for using XOXNO!', + footer: 'Thank you for using {appName}!', types: { 'check-in-manager': { label: 'Check-in Manager', @@ -55,7 +54,7 @@ const translations = { } as const satisfies Translations type IProps = { - host?: IHost + host?: IEmailConfig name: string event: { name: string @@ -85,7 +84,9 @@ const InviteEmail = ({ namespace: 'invite', }) - const HOST = getHost(host) + const tPayload = { appName: host.appName } + + const HOST = `https://${host.host}` const href = `${HOST}/event/${event.eventId}/join?guest=${event.inviteId}` @@ -95,7 +96,7 @@ const InviteEmail = ({ return ( @@ -129,40 +130,45 @@ const InviteEmail = ({
- {t('title', { eventName: event.name })} + {t('title', { eventName: event.name, ...tPayload })} - {t('greeting', { name })} + {t('greeting', { name, ...tPayload })} {t.rich('description', { + ...tPayload, eventName: event.name, - roleName: t(`types.${mainRole}.label`), + roleName: t(`types.${mainRole}.label`, tPayload), b: (chunks) => {chunks}, })}{' '} - {t(`types.${mainRole}.description`)} + {t(`types.${mainRole}.description`, tPayload)} - {t('action')} + {t('action', tPayload)}
{t.rich('info', { + ...tPayload, xoxnolink: (children) => ( {children} ), emaillink: (children) => ( - + {children} ), })} - +
{unsubscribeSection} diff --git a/src/email/utils.tsx b/src/email/utils.tsx index e9dc457..4a454ef 100644 --- a/src/email/utils.tsx +++ b/src/email/utils.tsx @@ -241,26 +241,6 @@ export type Translations = { } & Partial<{ [key in Exclude]: Partial }> } -export const defaultHost = 'https://xoxno.com' - -const hosts = [ - defaultHost, - 'https://next.xoxno.com', - 'https://devnet.xoxno.com', -] as const - -export function getHost(propHost: IHost) { - return hosts.includes(propHost) ? propHost : defaultHost -} - -export type IHost = (typeof hosts)[number] - -export const apiMappers: Record = { - 'https://xoxno.com': 'https://api.xoxno.com', - 'https://next.xoxno.com': 'https://api.xoxno.com', - 'https://devnet.xoxno.com': 'https://devnet-api.xoxno.com', -} - export async function renderGenericEmail(Email: ReactElement) { const html = await renderAsync(Email, { pretty: true, @@ -399,3 +379,9 @@ export function ThankYou({ return {text} } + +/* export const apiMappers: Record = { + 'xoxno.com': 'https://api.xoxno.com', + 'devnet.xoxno.com': 'https://devnet-api.xoxno.com', + 'zonatix.com': 'https://api.xoxno.com', +} */ diff --git a/src/utils.ts b/src/utils.ts index f0e9fcc..b36fdf2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,3 +15,29 @@ export function getOnlineLocation(onlineLink: string) { export function getMapsLink(location: IEventDoc['location']) { return `https://www.google.com/maps/search/?api=1&query=${location.address}&query_place_id=${location.placeId}` } + +const hosts = ['xoxno.com', 'devnet.xoxno.com', 'zonatix.com'] as const + +export type IHost = (typeof hosts)[number] + +export interface IEmailConfig { + host: IHost + appName: string + socials: { + discord: string + twitter: string + telegram: string + email: string + } +} + +export const defaultHost: IEmailConfig = { + host: 'xoxno.com', + appName: 'XOXNO', + socials: { + discord: 'https://discord.com/invite/xoxno', + email: 'contact@xoxno.com', + twitter: 'https://twitter.com/XoxnoNetwork', + telegram: 'https://t.me/xoxno', + }, +}