fix: various smaller fixes (#5917)
* fix: try fix email templates rendering links for variables * fix: b is not a function * fix: wording on modpack btn on setup type stage * fix: respect launcher-meta info * feat: i18n pass on creation flow modal * fix: prefetch loader manifests * fix: lint
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Card, StyledInput } from '@modrinth/ui'
|
||||
import { ButtonStyled, Card, NewModal, StyledInput } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import emails from '~/templates/emails'
|
||||
@@ -14,7 +14,7 @@ const filtered = computed(() =>
|
||||
function openAll() {
|
||||
let offset = 0
|
||||
for (const id of filtered.value) {
|
||||
openPreview(id, offset)
|
||||
openPopupPreview(id, offset)
|
||||
offset++
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,81 @@ function copy(id: string) {
|
||||
navigator.clipboard?.writeText(`/_internal/templates/email/${id}`).catch(() => {})
|
||||
}
|
||||
|
||||
function openPreview(id: string, offset = 0) {
|
||||
const previewModal = ref<{ hide: () => void; show: () => void } | null>(null)
|
||||
const previewTemplate = ref<string | null>(null)
|
||||
const previewLoading = ref(false)
|
||||
const previewError = ref<string | null>(null)
|
||||
const previewHtml = ref('')
|
||||
const previewVariables = ref<string[]>([])
|
||||
const variableValues = ref<Record<string, string>>({})
|
||||
|
||||
function extractVariables(html: string): string[] {
|
||||
const tokens = new Set<string>()
|
||||
const regex = /\{([a-zA-Z0-9_.-]+)\}/g
|
||||
let match = regex.exec(html)
|
||||
|
||||
while (match !== null) {
|
||||
tokens.add(match[1])
|
||||
match = regex.exec(html)
|
||||
}
|
||||
|
||||
return [...tokens]
|
||||
}
|
||||
|
||||
const renderedPreview = computed(() => {
|
||||
let html = previewHtml.value
|
||||
|
||||
for (const [key, value] of Object.entries(variableValues.value)) {
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`\\{${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}`, 'g')
|
||||
html = html.replace(pattern, value)
|
||||
}
|
||||
|
||||
return html
|
||||
})
|
||||
|
||||
async function openPreview(id: string, event?: MouseEvent) {
|
||||
if (event?.shiftKey) {
|
||||
openPopupPreview(id)
|
||||
return
|
||||
}
|
||||
|
||||
previewTemplate.value = id
|
||||
previewLoading.value = true
|
||||
previewError.value = null
|
||||
previewHtml.value = ''
|
||||
previewVariables.value = []
|
||||
variableValues.value = {}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/_internal/templates/email/${id}`)
|
||||
previewHtml.value = await response.text()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load template ${id}`)
|
||||
}
|
||||
|
||||
const variables = extractVariables(previewHtml.value)
|
||||
previewVariables.value = variables
|
||||
variableValues.value = Object.fromEntries(variables.map((value) => [value, '']))
|
||||
previewModal.value?.show()
|
||||
} catch (error) {
|
||||
previewError.value = 'Failed to load email preview.'
|
||||
console.error(error)
|
||||
previewModal.value?.show()
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
previewModal.value?.hide()
|
||||
}
|
||||
|
||||
function openPopupPreview(id: string, offset = 0) {
|
||||
const width = 600
|
||||
const height = 850
|
||||
const left = window.screenX + (window.outerWidth - width) / 2 + ((offset * 28) % 320)
|
||||
@@ -48,6 +122,69 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="normal-page no-sidebar">
|
||||
<h1 class="mb-4 text-3xl font-extrabold text-heading">Email templates</h1>
|
||||
<NewModal
|
||||
ref="previewModal"
|
||||
header="Preview email"
|
||||
width="min(92vw, 1000px)"
|
||||
:max-content-height="'88vh'"
|
||||
scrollable
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="label__title text-base">Template: {{ previewTemplate }}</p>
|
||||
|
||||
<div
|
||||
v-if="previewError"
|
||||
class="border-danger bg-danger/10 text-danger my-2 rounded border px-3 py-2 text-sm"
|
||||
>
|
||||
{{ previewError }}
|
||||
</div>
|
||||
|
||||
<div v-if="previewLoading" class="my-4 text-sm text-secondary">Loading preview…</div>
|
||||
<div v-else>
|
||||
<div v-if="previewVariables.length" class="mt-2 grid gap-3 md:grid-cols-2">
|
||||
<label
|
||||
v-for="variable in previewVariables"
|
||||
:key="variable"
|
||||
:for="`preview-${variable}`"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<span class="label__title">{{ variable }}</span>
|
||||
<StyledInput
|
||||
:id="`preview-${variable}`"
|
||||
v-model="variableValues[variable]"
|
||||
type="text"
|
||||
:placeholder="`Enter ${variable}`"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p v-else class="mt-2 text-xs text-secondary">
|
||||
No template variables were detected; preview shown using default values.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="label__title mb-2">Rendered template</div>
|
||||
<iframe
|
||||
v-if="!previewError"
|
||||
:srcdoc="renderedPreview"
|
||||
class="h-[60vh] w-full rounded border border-divider bg-white"
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="rounded border border-divider bg-white px-4 py-3 text-sm text-secondary"
|
||||
>
|
||||
Could not render template preview.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-4">
|
||||
<button class="iconified-button transparent" type="button" @click="closePreview">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<div class="normal-page__content">
|
||||
<Card class="mb-6 flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@@ -97,7 +234,7 @@ onMounted(() => {
|
||||
|
||||
<div class="mt-auto flex gap-2">
|
||||
<ButtonStyled color="brand" class="flex-1">
|
||||
<button class="w-full justify-center" @click="openPreview(id)">
|
||||
<button class="w-full justify-center" @click="openPreview(id, $event)">
|
||||
<PlayIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Preview
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> New sign-in method added </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
Your {authprovider.name} account has been connected and you can now use it to sign in to your
|
||||
Modrinth account.
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign-in method removed</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
Your <b>{authprovider.name}</b> account has been disconnected and you can no longer use it to
|
||||
sign in to your Modrinth account.
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your email has been changed </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
At your request, we've successfully updated your Modrinth account's email to
|
||||
{emailchanged.new_email}.
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign in from new device </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
We noticed that your account was just signed into from a new device or location. If this was
|
||||
you, you can safely ignore this email.
|
||||
|
||||
@@ -13,7 +13,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
A new personal access token has been created
|
||||
</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
A new personal access token, <b>{newpat.token_name}</b>, has been added to your account.
|
||||
</Text>
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been changed </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base"> Your password has been changed on your account. </Text>
|
||||
<Text class="text-muted text-base">
|
||||
If you did not make this change, please contact us immediately through our
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been removed </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
At your request, your password has been removed from your account. You must now use a linked
|
||||
authentication provider (such as your {passremoved.provider} account) to log into your
|
||||
|
||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
Payment failed for {paymentfailed.service}
|
||||
</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
Our attempt to collect payment for {paymentfailed.amount} from the payment card on file was
|
||||
unsuccessful. Please update your billing settings to avoid suspension of your service.
|
||||
|
||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold">Revenue available to withdraw!</Heading>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
The {payout.amount} earned during {payout.period} has been processed and is now available to
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Reset your password </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
Please visit the link below to reset your password. If you did not request for your password
|
||||
to be reset, you can safely ignore this email.
|
||||
|
||||
@@ -8,7 +8,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
<StyledEmail title="We’ve added time to your server">
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold">We’ve added time to your server</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">{credit.header_message}</Text>
|
||||
|
||||
<Text class="text-muted text-base">
|
||||
|
||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
Price change for {taxnotification.service}
|
||||
</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
We're writing to let you know about an update to your {taxnotification.service} subscription.
|
||||
</Text>
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Two-factor authentication enabled </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
You've secured your account with two-factor authentication. Now, when signing in, you will
|
||||
need to submit the code generated by your authenticator app.
|
||||
|
||||
@@ -13,7 +13,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
You've disabled two-factor authentication security on your account.
|
||||
</Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
At your request, we've removed two-factor authentication from your Modrinth account.
|
||||
</Text>
|
||||
|
||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Verify your email </Heading>
|
||||
|
||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
||||
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
<Text class="text-muted text-base">
|
||||
Please visit the link below to verify your email. If the button does not work, you can copy
|
||||
the link and paste it into your browser. This link expires in 24 hours.
|
||||
|
||||
@@ -29,7 +29,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>New message from moderators on {project.name}</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
Modrinth's moderation team has left a message on your project,
|
||||
|
||||
@@ -20,7 +20,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>Report of '{report.title}' has been updated</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base"
|
||||
>Your report of {report.title} from {report.date} has been updated by our moderation
|
||||
|
||||
@@ -20,7 +20,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>Report of {report.title} has been submitted</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
We've received your report of {report.title} and our moderation team will review it shortly.
|
||||
|
||||
@@ -27,7 +27,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>You've been invited to an organization</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base"
|
||||
>Modrinth user
|
||||
|
||||
@@ -24,7 +24,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
</Section>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold">You've been invited to a project</Heading>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
Modrinth user
|
||||
|
||||
@@ -26,7 +26,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>Your project, {project.name}, has been approved 🎉</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Congratulations {user.name},</Text>
|
||||
<Text class="text-base">Congratulations <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
Your project
|
||||
|
||||
@@ -29,7 +29,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
>Your project, {project.name}, status has been updated</Heading
|
||||
>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
Your project's status has been changed from <b>{project.oldstatus}</b> to
|
||||
|
||||
@@ -24,7 +24,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
||||
</Section>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold">Project ownership transferred</Heading>
|
||||
|
||||
<Text class="text-base">Hi {user.name},</Text>
|
||||
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
The ownership of
|
||||
|
||||
@@ -67,6 +67,7 @@ const tailwindConfig = {
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Inter:700,400"
|
||||
rel="stylesheet"
|
||||
@@ -78,6 +79,9 @@ const tailwindConfig = {
|
||||
line-height:100%; } table { border-collapse:separate; } a, a:link, a:visited {
|
||||
text-decoration:none; color:#1f68c0; } a:hover { text-decoration:underline; }
|
||||
h1,h2,h3,h4,h5,h6 { color:#000 !important; margin:0; mso-line-height-rule:exactly; }
|
||||
.no-auto-link, .no-auto-link a, .no-auto-link a:link, .no-auto-link a:visited, .no-auto-link
|
||||
a[x-apple-data-detectors] { color:inherit !important; text-decoration:none !important;
|
||||
cursor:default !important; pointer-events:none !important; }
|
||||
</Style>
|
||||
</Head>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user