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:
Calum H.
2026-04-27 17:27:41 +01:00
committed by GitHub
parent e8be67d41f
commit 6afda48e70
45 changed files with 1435 additions and 261 deletions

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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}.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -8,7 +8,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
<StyledEmail title="Weve added time to your server">
<Heading as="h1" class="mb-2 text-2xl font-bold">Weve 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">

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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.

View File

@@ -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,

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>