Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const {
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
Expand Down Expand Up @@ -1108,6 +1109,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:fetch-existing-instance-names="fetchExistingInstanceNames"
:search-modpacks="searchModpacks"
:get-project-versions="getProjectVersions"
:get-loader-manifest="getLoaderManifest"
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
Expand Down
12 changes: 12 additions & 0 deletions apps/app-frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@
"app.browse.install-content-to-instance": {
"message": "Install content to instance"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Install"
},
"app.browse.server.installed": {
"message": "Installed"
},
"app.browse.server.installing": {
"message": "Installing"
},
"app.export-modal.description-placeholder": {
"message": "Enter modpack description..."
},
Expand Down
30 changes: 28 additions & 2 deletions apps/app-frontend/src/pages/Browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
import { get_by_profile_path } from '@/helpers/process'
import {
get as getInstance,
Expand Down Expand Up @@ -441,10 +442,26 @@ const messages = defineMessages({
id: 'app.browse.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
installToServer: {
id: 'app.browse.server.install',
defaultMessage: 'Install',
},
installedToServer: {
id: 'app.browse.server.installed',
defaultMessage: 'Installed',
},
installingToServer: {
id: 'app.browse.server.installing',
defaultMessage: 'Installing',
},
modLoaderProvidedByInstance: {
id: 'search.filter.locked.instance-loader.title',
defaultMessage: 'Loader is provided by the instance',
},
modpacksProjectType: {
id: 'app.browse.project-type.modpacks',
defaultMessage: 'Modpacks',
},
modLoaderProvidedByServer: {
id: 'search.filter.locked.server-loader.title',
defaultMessage: 'Loader is provided by the server',
Expand Down Expand Up @@ -550,7 +567,9 @@ const selectableProjectTypes = computed(() => {
const suffix = queryString ? `?${queryString}` : ''

if (isSetupServerContext.value) {
return [{ label: 'Modpacks', href: `/browse/modpack${suffix}` }]
return [
{ label: formatMessage(messages.modpacksProjectType), href: `/browse/modpack${suffix}` },
]
}

if (isFromWorlds.value) {
Expand Down Expand Up @@ -730,7 +749,13 @@ function getCardActions(
return [
{
key: 'install',
label: isInstalling ? 'Installing' : isInstalled ? 'Installed' : 'Install',
label: formatMessage(
isInstalling
? messages.installingToServer
: isInstalled
? messages.installedToServer
: messages.installToServer,
),
icon: isInstalled ? CheckIcon : PlusIcon,
disabled: isInstalled || isInstalling,
color: 'brand',
Expand Down Expand Up @@ -972,6 +997,7 @@ provideBrowseManager({
:on-back="onServerFlowBack"
:search-modpacks="searchServerModpacks"
:get-project-versions="getServerProjectVersions"
:get-loader-manifest="getLoaderManifest"
@hide="() => {}"
@browse-modpacks="() => {}"
@create="handleServerModpackFlowCreate"
Expand Down
2 changes: 2 additions & 0 deletions apps/app-frontend/src/providers/setup/creation-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlre
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
import { import_instance } from '@/helpers/import.js'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack'
import { create, list } from '@/helpers/profile.js'
import type { InstanceLoader } from '@/helpers/types'
Expand Down Expand Up @@ -165,6 +166,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
Expand Down
145 changes: 141 additions & 4 deletions apps/frontend/src/pages/admin/emails.vue
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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++
}
}
Expand All @@ -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)
Expand All @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/templates/emails/account/PATCreated.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Loading
Loading