Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
cc23637
feat: start on tax compliance
IMB11 Aug 17, 2025
5f60032
feat: avarala1099 composable
IMB11 Aug 17, 2025
c0dd790
fix: shouldShow should be managed on the page itself
IMB11 Aug 17, 2025
983c3d6
refactor: move show logic to revenue page
IMB11 Aug 17, 2025
7429340
feat: security practices rather than info
IMB11 Aug 17, 2025
368d79a
feat: withdraw page lock
IMB11 Aug 18, 2025
c4bfd31
Merge branch 'main' into cal/dev-233
IMB11 Aug 19, 2025
443b777
fix: empty modal bug & lint issues
IMB11 Aug 19, 2025
76d9f62
feat: hide behind feature flag
IMB11 Aug 19, 2025
551c2bd
Use standard admonition components, make casing consistent
Prospector Aug 19, 2025
f9185df
modal title
Prospector Aug 19, 2025
58290aa
lint
Prospector Aug 19, 2025
519a0ce
feat: withdrawal check
IMB11 Aug 19, 2025
77889ec
feat: tax cap on withdrawals warning
IMB11 Aug 19, 2025
ae173ec
Merge branch 'main' into cal/dev-233
IMB11 Aug 19, 2025
745b326
Merge branch 'main' into cal/dev-233
IMB11 Aug 27, 2025
a1b30a7
feat: start on revenue page overhaul
IMB11 Aug 27, 2025
25a5bb3
feat: segment generation for bar
IMB11 Aug 28, 2025
8298364
feat: tooltips and links
IMB11 Aug 28, 2025
ba8bf6a
fix: tooltip border
IMB11 Aug 28, 2025
1852aba
Merge branch 'main' into cal/dev-233
IMB11 Aug 29, 2025
4baec59
feat: finish initial layout, start on withdraw modal
IMB11 Aug 29, 2025
f78fcb0
feat: start on withdrawal limit stage
IMB11 Aug 29, 2025
58abb39
Merge branch 'main' into cal/dev-233
IMB11 Sep 3, 2025
87ad59b
feat: shade support for primary colors
IMB11 Sep 3, 2025
2fa4785
feat: start on withdraw details stage
IMB11 Sep 3, 2025
f10ae6a
fix: convert swatches to hex
IMB11 Sep 4, 2025
5f03e67
feat: payout method/region dropdown temporarily using multiselect
IMB11 Sep 4, 2025
c879cb3
feat: fix modal open issues and use teleport dropdowns
IMB11 Sep 5, 2025
c2faa56
feat: hide transactions section if there are no transactions
IMB11 Sep 5, 2025
1865120
refactor: NavStack surfaces
IMB11 Sep 6, 2025
eee63e4
Merge branch 'main' into cal/dev-233
IMB11 Sep 8, 2025
03ea910
feat: new dropdown component
IMB11 Sep 8, 2025
38ada32
feat: remove teleport dropdown modal in favour of new combobox component
IMB11 Sep 12, 2025
e3a0eae
Merge branch 'main' into cal/dev-233
IMB11 Sep 12, 2025
c4c3a57
fix: lint
IMB11 Sep 12, 2025
c1a9f8e
refactor: dashboard sidebar layout
IMB11 Sep 12, 2025
2efdf21
feat: cleanup
IMB11 Sep 12, 2025
32ff787
fix: niche bugs
IMB11 Sep 12, 2025
63b3fb1
fix: ComboBox styling
IMB11 Sep 12, 2025
20085d1
feat: first part of qa
IMB11 Sep 12, 2025
07c6c4b
feat: animate flash rather than tooltip
IMB11 Sep 12, 2025
ae336bd
fix: lint
IMB11 Sep 12, 2025
7473d0b
feat: qa border gradient
IMB11 Sep 13, 2025
b033be4
fix: seg hover flashes
IMB11 Sep 13, 2025
960a87c
feat: i18n
IMB11 Sep 13, 2025
d1159b5
feat: i18n and final QA
IMB11 Sep 13, 2025
39aced5
fix: lint
IMB11 Sep 13, 2025
6f4423b
feat: QA
IMB11 Sep 14, 2025
8c2e684
fix: lint
IMB11 Sep 14, 2025
bbeabad
Merge branch 'main' into cal/dev-233
IMB11 Sep 15, 2025
ec216c5
fix: merge conflicts
IMB11 Sep 15, 2025
25e33f9
fix: intl
IMB11 Sep 15, 2025
77e1b74
fix: blue hover
IMB11 Sep 15, 2025
9f69a74
fix: transfers page
IMB11 Sep 15, 2025
1abdd3e
Merge remote-tracking branch 'origin/main' into cal/dev-233
IMB11 Sep 24, 2025
8c0e0b7
fix: intl
IMB11 Sep 24, 2025
a1ebbb2
Merge remote-tracking branch 'origin/cal/surfaces-and-shades' into ca…
IMB11 Sep 24, 2025
e408e5f
feat: stages
IMB11 Sep 25, 2025
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
71 changes: 71 additions & 0 deletions .github/instructions/i18n-convert.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
applyTo: '**/*.vue'
---

You are given a Nuxt/Vue single-file component (.vue). Your task is to convert every hard-coded natural-language string in the <template> into our localization system using @vintl/vintl-nuxt (which wraps FormatJS).

Please follow these rules precisely:

1. Identify translatable strings

- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.

2. Create message definitions

- In the <script setup> block, import `defineMessage` or `defineMessages` from `@vintl/vintl`.
- For each extracted string, define a message with a unique `id` (use a descriptive prefix based on the component path, e.g. `auth.welcome.long-title`) and a `defaultMessage` equal to the original English string.
Example:
const messages = defineMessages({
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You’re now part of the community…' },
})

3. Handle variables and ICU formats

- Replace dynamic parts with ICU placeholders: "Hello, ${user.name}!" → `{name}` and defaultMessage: 'Hello, {name}!'
- For numbers/dates/times, use ICU/FormatJS options (e.g., currency): `{price, number, ::currency/USD}`
- For plurals/selects, use ICU: `'{count, plural, one {# message} other {# messages}}'`

4. Rich-text messages (links/markup)

- In `defaultMessage`, wrap link/markup ranges with tags, e.g.:
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
- Render rich-text messages with `<IntlFormatted>` from `@vintl/vintl/components` and map tags via `values`:
<IntlFormatted
:message="messages.tosLabel"
:values="{
'terms-link': (chunks) => <NuxtLink to='/terms'>{chunks}</NuxtLink>,
'privacy-link': (chunks) => <NuxtLink to='/privacy'>{chunks}</NuxtLink>,
}"
/>
- For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` and map `'strong': (c) => <strong>{c}</strong>`

5. Formatting in templates

- Import and use `useVIntl()`; prefer `formatMessage` for simple strings:
`const { formatMessage } = useVIntl()`
`<button>{{ formatMessage(messages.welcomeTitle) }}</button>`
- Vue methods like `$formatMessage`, `$formatNumber`, `$formatDate` are also available if needed.

6. Naming conventions and id stability

- Make `id`s descriptive and stable (e.g., `error.generic.default.title`). Group related messages with `defineMessages`.

7. Avoid Vue/ICU delimiter collisions

- If an ICU placeholder would end right before `}}` in a Vue template, insert a space so it becomes `} }` to avoid parsing issues.

8. Update imports and remove literals

- Ensure imports for `defineMessage`/`defineMessages`, `useVIntl`, and `<IntlFormatted>` are present. Replace all hard-coded strings with `formatMessage(...)` or `<IntlFormatted>` and remove the literals.

9. Preserve functionality

- Do not change logic, layout, reactivity, or bindings—only refactor strings into i18n.

Use existing patterns from our codebase:

- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`

When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
ButtonStyled,
Checkbox,
Chips,
Combobox,
injectNotificationManager,
TeleportDropdownMenu,
} from '@modrinth/ui'
import {
formatCategory,
Expand Down Expand Up @@ -158,6 +158,21 @@ const selectableGameVersionNumbers = computed(() => {
.map((x) => x.version)
})

const gameVersionOptions = computed(() =>
(selectableGameVersionNumbers.value ?? []).map((v) => ({ value: v, label: v })),
)

const loaderVersionOptions = computed(() =>
(selectableLoaderVersions.value ?? []).map((opt, index) => ({ value: index, label: opt.id })),
)

const loaderVersionLabel = computed(() => {
const idx = loaderVersionIndex.value
return idx >= 0 && selectableLoaderVersions.value
? selectableLoaderVersions.value[idx]?.id
: 'Select version'
})

const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
if (gameVersion.value) {
if (loader.value === 'fabric') {
Expand Down Expand Up @@ -647,11 +662,11 @@ const messages = defineMessages({
{{ formatMessage(messages.gameVersion) }}
</h2>
<div class="flex flex-wrap mt-2 gap-2">
<TeleportDropdownMenu
<Combobox
v-if="selectableGameVersionNumbers !== undefined"
v-model="gameVersion"
:options="selectableGameVersionNumbers"
name="Game Version Dropdown"
:options="gameVersionOptions"
:display-value="gameVersion || formatMessage(messages.unknownVersion)"
/>
<Checkbox
v-if="hasSnapshots"
Expand All @@ -663,14 +678,13 @@ const messages = defineMessages({
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
</h2>
<TeleportDropdownMenu
<Combobox
v-if="selectableLoaderVersions"
:model-value="selectableLoaderVersions[loaderVersionIndex]"
:options="selectableLoaderVersions"
:display-name="(option: ManifestLoaderVersion) => option?.id"
v-model="loaderVersionIndex"
:options="loaderVersionOptions"
:display-value="loaderVersionLabel"
name="Version selector"
class="mt-2"
@change="(value) => (loaderVersionIndex = value.index)"
/>
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
<IssuesIcon />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'

import { get, set } from '@/helpers/settings.ts'
Expand Down Expand Up @@ -50,7 +50,7 @@ watch(
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = e
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
Expand Down Expand Up @@ -86,12 +86,13 @@ watch(
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
</div>
<TeleportDropdownMenu
<Combobox
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="w-40"
:options="['Home', 'Library']"
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
:display-value="settings.default_page ?? 'Select an option'"
/>
</div>

Expand Down Expand Up @@ -122,7 +123,7 @@ watch(
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = e
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { TeleportDropdownMenu } from '@modrinth/ui'
import { Combobox } from '@modrinth/ui'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'

import type { ServerPackStatus } from '@/helpers/worlds.ts'
Expand Down Expand Up @@ -74,12 +74,19 @@ defineExpose({ resourcePackOptions })
{{ formatMessage(messages.resourcePack) }}
</h2>
<div>
<TeleportDropdownMenu
<Combobox
v-model="resourcePack"
:options="resourcePackOptions"
:options="
resourcePackOptions.map((o) => ({
value: o,
label: formatMessage(resourcePackOptionMessages[o]),
}))
"
name="Server resource pack"
:display-name="
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
:display-value="
resourcePack
? formatMessage(resourcePackOptionMessages[resourcePack])
: 'Select an option'
"
/>
</div>
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,12 @@ export default defineNuxtConfig({
'Critical-CH': 'Sec-CH-Prefers-Color-Scheme',
},
},
'/dashboard/revenue/withdraw': {
redirect: {
to: '/dashboard/revenue',
statusCode: 410,
},
},
'/email/**': {
prerender: true,
headers: {
Expand Down
136 changes: 117 additions & 19 deletions apps/frontend/src/components/ui/NavStack.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,130 @@
<template>
<nav>
<ul>
<slot />
<nav :aria-label="ariaLabel" class="w-full">
<ul class="m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4">
<slot v-if="hasSlotContent" />

<template v-else>
<li v-for="(item, idx) in items" :key="getKey(item, idx)" class="contents">
<hr v-if="isSeparator(item)" class="my-1 w-full border-t border-solid" />

<div
v-else-if="isHeading(item)"
class="px-4 pb-1 pt-2 text-xs font-bold uppercase tracking-wide text-secondary"
>
{{ item.label }}
</div>

<NuxtLink
v-else-if="item.link ?? item.to"
:to="(item.link ?? item.to) as string"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
<span v-if="item.chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>

<button
v-else-if="item.action"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
:class="{ 'danger-button': item.danger }"
@click="item.action"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
</button>

<span v-else>You frog. 🐸</span>
</li>
</template>
</ul>
</nav>
</template>

<script>
export default {}
</script>
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
import { type Component, computed, useSlots } from 'vue'

<style lang="scss" scoped>
ul {
display: flex;
flex-direction: column;
grid-gap: var(--spacing-card-xs);
flex-wrap: wrap;
list-style-type: none;
margin: 0;
padding: 0;
type NavStackBaseItem = {
label: string
icon?: Component | string
badge?: string | number | null
chevron?: boolean
danger?: boolean
}

> :first-child {
margin-top: 0;
}
type NavStackLinkItem = NavStackBaseItem & {
type?: 'item'
link?: string | null
to?: string | null
action?: (() => void) | null
}

type NavStackSeparator = { type: 'separator' }
type NavStackHeading = { type: 'heading'; label: string }

export type NavStackEntry = NavStackLinkItem | NavStackSeparator | NavStackHeading

const props = defineProps<{
items?: NavStackEntry[]
ariaLabel?: string
}>()

const ariaLabel = computed(() => props.ariaLabel ?? 'Section navigation')

const slots = useSlots()
const hasSlotContent = computed(() => {
const content = slots.default?.()
return !!(content && content.length)
})

function isSeparator(item: NavStackEntry): item is NavStackSeparator {
return (item as any).type === 'separator'
}

function isHeading(item: NavStackEntry): item is NavStackHeading {
return (item as any).type === 'heading'
}

function getKey(item: NavStackEntry, idx: number) {
if (isSeparator(item)) return `sep-${idx}`
if (isHeading(item)) return `head-${item.label}-${idx}`
const link = (item as NavStackLinkItem).link ?? (item as NavStackLinkItem).to
return link ? `link-${link}` : `action-${(item as NavStackLinkItem).label}-${idx}`
}
</script>

<style lang="scss" scoped>
li {
display: unset;
text-align: unset;
}
.router-link-exact-active.nav-item {
background: var(--color-button-bg-selected);
color: var(--color-button-text-selected);
}
.router-link-exact-active.nav-item .text-contrast {
color: var(--color-button-text-selected);
}
</style>
Loading
Loading