refactor: introduce PageHeader component (#6629)

* refactor: introduce PageHeader component

* feat: split up according to component structure guide

* fix: lint

* fix: label inconsistencies

* feat: refactor PageHeader (again)

* refactor: old impls

* fix: changes

* fix: rescan

* fix: prepr

* fix: dedupe

* fix: rev

* fix: lint

* fix: lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-07-20 18:25:12 +00:00
committed by GitHub
co-authored by Prospector
parent 8b6f0ec695
commit 932858b5f5
65 changed files with 3131 additions and 2091 deletions
@@ -0,0 +1,294 @@
<template>
<PageHeader :title="instance.name">
<template #leading>
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
</template>
<template #metadata>
<InstanceHeaderServerMetadata
v-if="isServerInstance"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:instance-id="instance.id"
/>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
Minecraft {{ instance.game_version }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
:icon="ServerLoaderIcon"
:icon-props="{ loader: loaderDisplayName }"
tooltip="Mod loader"
>
{{ loaderLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
<template #actions>
<PageHeaderActions>
<ButtonStyled v-if="isInstalling" color="brand" size="large">
<button type="button" disabled>
{{ formatMessage(commonMessages.installingLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="instance.install_stage !== 'installed'" color="brand" size="large">
<button type="button" @click="emit('repair')">
<DownloadIcon />
{{ formatMessage(messages.repair) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing" color="red" size="large">
<button type="button" :disabled="stopping" @click="emit('stop')">
<StopCircleIcon />
{{
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
}}
</button>
</ButtonStyled>
<JoinedButtons
v-else-if="!loading && isServerInstance"
:actions="serverPlayActions"
color="brand"
size="large"
/>
<ButtonStyled v-else-if="!loading" color="brand" size="large">
<button type="button" @click="emit('play')">
<PlayIcon />
{{ formatMessage(commonMessages.playButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button type="button" disabled>{{ formatMessage(messages.starting) }}</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button
v-tooltip="formatMessage(messages.instanceSettings)"
type="button"
:aria-label="formatMessage(messages.instanceSettings)"
@click="emit('settings')"
>
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="moreActions"
:tooltip="formatMessage(messages.moreActions)"
:aria-label="formatMessage(messages.moreActions)"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</PageHeaderActions>
</template>
</PageHeader>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
DownloadIcon,
ExternalIcon,
FolderOpenIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
SettingsIcon,
StopCircleIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
TimerIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
formatLoaderLabel,
type JoinedButtonAction,
JoinedButtons,
LoaderIcon as ServerLoaderIcon,
PageHeader,
PageHeaderActions,
PageHeaderMetadata,
PageHeaderMetadataItem,
type ServerLoader,
TeleportOverflowMenu,
type TeleportOverflowMenuItem,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import type { GameInstance } from '@/helpers/types'
import InstanceHeaderServerMetadata from './instance-page-header-server-metadata.vue'
const messages = defineMessages({
createShortcut: {
id: 'instance.action.create-shortcut',
defaultMessage: 'Create shortcut',
},
exportModpack: {
id: 'instance.action.export-modpack',
defaultMessage: 'Export modpack',
},
instanceSettings: {
id: 'instance.action.settings',
defaultMessage: 'Instance settings',
},
launchInstance: {
id: 'instance.action.launch-instance',
defaultMessage: 'Launch instance',
},
moreActions: {
id: 'instance.action.more-actions',
defaultMessage: 'More actions',
},
neverPlayed: {
id: 'instance.playtime.never-played',
defaultMessage: 'Never played',
},
openFolder: {
id: 'instance.action.open-folder',
defaultMessage: 'Open folder',
},
repair: {
id: 'instance.action.repair',
defaultMessage: 'Repair',
},
starting: {
id: 'instance.action.starting',
defaultMessage: 'Starting...',
},
stopping: {
id: 'instance.action.stopping',
defaultMessage: 'Stopping...',
},
})
const props = withDefaults(
defineProps<{
instance: GameInstance
iconSrc?: string | null
isServerInstance?: boolean
showInstancePlayTime?: boolean
timePlayed?: number
playing?: boolean
loading?: boolean
stopping?: boolean
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
}>(),
{
iconSrc: null,
isServerInstance: false,
showInstancePlayTime: false,
timePlayed: 0,
playing: false,
loading: false,
stopping: false,
loadingServerPing: false,
playersOnline: undefined,
statusOnline: false,
recentPlays: undefined,
ping: undefined,
minecraftServer: undefined,
linkedProjectV3: undefined,
},
)
const emit = defineEmits<{
repair: []
stop: []
play: []
playServer: []
settings: []
openFolder: []
export: []
createShortcut: []
}>()
const installingStages = [
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
]
const { formatMessage } = useVIntl()
const isInstalling = computed(() => installingStages.includes(props.instance.install_stage))
const loaderDisplayName = computed(() => formatLoaderLabel(props.instance.loader) as ServerLoader)
const loaderLabel = computed(() =>
[loaderDisplayName.value, props.instance.loader_version].filter(Boolean).join(' '),
)
const playtimeLabel = computed(() => {
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
const hours = Math.floor(props.timePlayed / 3600)
if (hours >= 1) {
return `${hours} hour${hours > 1 ? 's' : ''}`
}
const minutes = Math.floor(props.timePlayed / 60)
if (minutes >= 1) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`
}
const seconds = Math.floor(props.timePlayed)
return `${seconds} second${seconds > 1 ? 's' : ''}`
})
const serverPlayActions = computed<JoinedButtonAction[]>(() => [
{
id: 'join_server',
label: formatMessage(commonMessages.playButton),
icon: PlayIcon,
action: () => emit('playServer'),
},
{
id: 'launch_instance',
label: formatMessage(messages.launchInstance),
icon: PlayIcon,
action: () => emit('play'),
},
])
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
{
id: 'open-folder',
label: formatMessage(messages.openFolder),
icon: FolderOpenIcon,
action: () => emit('openFolder'),
},
{
id: 'export-mrpack',
label: formatMessage(messages.exportModpack),
icon: PackageIcon,
action: () => emit('export'),
},
{
id: 'create-shortcut',
label: formatMessage(messages.createShortcut),
icon: ExternalIcon,
action: () => emit('createShortcut'),
},
])
</script>
@@ -0,0 +1,63 @@
<template>
<div class="flex items-center flex-wrap gap-2">
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instanceId"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
Avatar,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
} from '@modrinth/ui'
defineProps<{
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
instanceId?: string
}>()
</script>
@@ -788,6 +788,33 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
},
"instance.action.create-shortcut": {
"message": "Create shortcut"
},
"instance.action.export-modpack": {
"message": "Export modpack"
},
"instance.action.launch-instance": {
"message": "Launch instance"
},
"instance.action.more-actions": {
"message": "More actions"
},
"instance.action.open-folder": {
"message": "Open folder"
},
"instance.action.repair": {
"message": "Repair"
},
"instance.action.settings": {
"message": "Instance settings"
},
"instance.action.starting": {
"message": "Starting..."
},
"instance.action.stopping": {
"message": "Stopping..."
},
"instance.add-server.add-and-play": {
"message": "Add and play"
},
@@ -830,6 +857,9 @@
"instance.files.save-as": {
"message": "Save as..."
},
"instance.playtime.never-played": {
"message": "Never played"
},
"instance.server-modal.address": {
"message": "Address"
},
+27 -239
View File
@@ -13,207 +13,31 @@
@unlinked="fetchInstance"
/>
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
<ContentPageHeader>
<template #icon>
<Avatar
:src="icon ? icon : undefined"
:alt="instance.name"
size="64px"
:tint-by="instance.id"
/>
</template>
<template #title>
{{ instance.name }}
</template>
<template #stats>
<div class="flex items-center flex-wrap gap-2">
<template v-if="!isServerInstance">
<div class="flex items-center gap-2 capitalize font-medium">
{{ instance.loader }} {{ instance.game_version }}
</div>
<template v-if="showInstancePlayTime">
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div class="flex items-center gap-2 font-medium">
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
</template>
</template>
<template v-else>
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div
v-if="minecraftServer?.region || ping"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<div
v-if="linkedProjectV3"
class="flex gap-1.5 items-center font-medium text-primary"
>
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instance.id"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</template>
</div>
</template>
<template #actions>
<div class="flex gap-2">
<ButtonStyled
v-if="
[
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
].includes(instance.install_stage)
"
color="brand"
size="large"
>
<button disabled>Installing...</button>
</ButtonStyled>
<ButtonStyled
v-else-if="instance.install_stage !== 'installed'"
color="brand"
size="large"
>
<button @click="repairInstance()">
<DownloadIcon />
Repair
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing === true" color="red" size="large">
<button :disabled="stopping" @click="stopInstance('InstancePage')">
<StopCircleIcon />
{{ stopping ? 'Stopping...' : 'Stop' }}
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="playing === false && loading === false && !isServerInstance"
color="brand"
size="large"
>
<button @click="startInstance('InstancePage')">
<PlayIcon />
Play
</button>
</ButtonStyled>
<div
v-else-if="playing === false && loading === false && isServerInstance"
class="joined-buttons"
>
<ButtonStyled color="brand" size="large">
<button @click="handlePlayServer()">
<PlayIcon />
Play
</button>
</ButtonStyled>
<ButtonStyled color="brand" size="large">
<OverflowMenu
:options="[
{
id: 'join_server',
action: () => handlePlayServer(),
},
{
id: 'launch_instance',
action: () => startInstance('InstancePage'),
},
]"
>
<div class="w-0 text-xl relative top-0.5 right-2.5">
<DropdownIcon />
</div>
<template #join_server>
<PlayIcon />
Join server
</template>
<template #launch_instance>
<PlayIcon />
Launch instance
</template>
</OverflowMenu>
</ButtonStyled>
</div>
<ButtonStyled
v-else-if="loading === true && playing === false"
color="brand"
size="large"
>
<button disabled>Starting...</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled type="transparent" circular size="large">
<OverflowMenu
:options="[
{
id: 'open-folder',
action: () => {
if (instance) showInstanceInFolder(instance.id)
},
},
{
id: 'export-mrpack',
action: () => exportModal?.show(),
},
{
id: 'create-shortcut',
action: () => createShortcut(),
},
]"
>
<MoreVerticalIcon />
<template #share-instance> <UserPlusIcon /> Share instance </template>
<template #host-a-server> <ServerIcon /> Create a server </template>
<template #open-folder> <FolderOpenIcon /> Open folder </template>
<template #export-mrpack> <PackageIcon /> Export modpack </template>
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
</OverflowMenu>
</ButtonStyled>
</div>
</template>
</ContentPageHeader>
<InstancePageHeader
:instance="instance"
:icon-src="icon"
:is-server-instance="isServerInstance"
:show-instance-play-time="showInstancePlayTime"
:time-played="timePlayed"
:playing="playing"
:loading="loading"
:stopping="stopping"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
@repair="() => repairInstance()"
@stop="() => stopInstance('InstancePage')"
@play="() => startInstance('InstancePage')"
@play-server="() => handlePlayServer()"
@settings="() => settingsModal?.show()"
@open-folder="() => instance && showInstanceInFolder(instance.id)"
@export="() => exportModal?.show()"
@create-shortcut="() => createShortcut()"
/>
</div>
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
<NavTabs :links="tabs" />
@@ -272,49 +96,30 @@ import {
BoxesIcon,
CheckCircleIcon,
ClipboardCopyIcon,
DownloadIcon,
DropdownIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
GlobeIcon,
HashIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
PlusIcon,
ServerIcon,
SettingsIcon,
StopCircleIcon,
TerminalSquareIcon,
UpdatedIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
ContentPageHeader,
injectNotificationManager,
NavTabs,
OverflowMenu,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import { injectNotificationManager, NavTabs, useLoadingBarToken } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ExportModal from '@/components/ui/ExportModal.vue'
import InstancePageHeader from '@/components/ui/instance-page-header/index.vue'
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import {
@@ -336,7 +141,6 @@ import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs, useTheming } from '@/store/state'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const { addNotification, handleError } = injectNotificationManager()
@@ -758,22 +562,6 @@ const timePlayed = computed(() => {
: 0
})
const timePlayedHumanized = computed(() => {
const duration = dayjs.duration(timePlayed.value, 'seconds')
const hours = Math.floor(duration.asHours())
if (hours >= 1) {
return hours + ' hour' + (hours > 1 ? 's' : '')
}
const minutes = Math.floor(duration.asMinutes())
if (minutes >= 1) {
return minutes + ' minute' + (minutes > 1 ? 's' : '')
}
const seconds = Math.floor(duration.asSeconds())
return seconds + ' second' + (seconds > 1 ? 's' : '')
})
onUnmounted(() => {
unlistenProcesses()
unlistenInstances()
+170 -146
View File
@@ -59,142 +59,100 @@
>
<ProjectBackgroundGradient :project="data" />
</Teleport>
<ProjectHeader
<ProjectPageHeader
v-else
:project="data"
:project-v3="projectV3"
:ping="serverPing"
:show-status-badge="data.status !== 'approved'"
@contextmenu.prevent.stop="handleRightClick"
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
>
<template v-if="isServerProject" #actions>
<ButtonStyled v-if="serverPlaying" size="large" color="red">
<button @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
:disabled="data && installingServerProjects.includes(data.id)"
@click="handleClickPlay"
>
<PlayIcon />
{{
data && installingServerProjects.includes(data.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'open-in-browser',
link: `https://modrinth.com/project/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
<template #actions>
<template v-if="isServerProject">
<ButtonStyled v-if="serverPlaying" color="red" size="large">
<button type="button" @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button type="button" :disabled="serverInstallLoading" @click="handleClickPlay">
<PlayIcon />
{{
serverInstallLoading
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
type="button"
:aria-label="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="serverProjectHeaderMoreActions"
tooltip="More options"
aria-label="More options"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</template>
<template v-else>
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
<button v-tooltip="formatMessage(messages.alreadyInstalled)" type="button" disabled>
<CheckIcon />
{{ formatMessage(commonMessages.installedLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="showSwitchVersion" size="large">
<button type="button" @click="goToVersions">
<SwapIcon />
{{ formatMessage(messages.switchVersion) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button
v-tooltip="
installButtonInstalled ? formatMessage(messages.alreadyInstalled) : undefined
"
type="button"
:disabled="installButtonDisabled"
@click="install(null)"
>
<component :is="installButtonIcon" :class="installButtonIconClass" />
{{
installButtonInstalled
? formatMessage(commonMessages.installedLabel)
: installButtonValidating
? formatMessage(commonMessages.validatingLabel)
: installButtonLoading
? formatMessage(commonMessages.installingLabel)
: serverProjectSelected
? formatMessage(commonMessages.selectedLabel)
: formatMessage(commonMessages.installButton)
}}
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="projectHeaderMoreActions"
tooltip="More options"
aria-label="More options"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</template>
</template>
<template v-else #actions>
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
<button v-tooltip="installButtonTooltip" disabled>
<CheckIcon />
{{ formatMessage(commonMessages.installedLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="showSwitchVersion" size="large">
<button @click="goToVersions">
<SwapIcon />
{{ formatMessage(messages.switchVersion) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
v-tooltip="installButtonTooltip"
:disabled="installButtonDisabled"
@click="install(null)"
>
<SpinnerIcon
v-if="installButtonLoading && !installButtonInstalled"
class="animate-spin"
/>
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
<CheckIcon v-else />
{{ installButtonLabel }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'follow',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #follow> <HeartIcon /> Follow </template>
<template #save> <BookmarkIcon /> Save </template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
</template>
</ProjectHeader>
</ProjectPageHeader>
<NavTabs
:links="[
{
@@ -289,9 +247,8 @@ import {
getTargetInstallPreferences,
injectNotificationManager,
NavTabs,
OverflowMenu,
ProjectBackgroundGradient,
ProjectHeader,
ProjectPageHeader,
ProjectSidebarCompatibility,
ProjectSidebarCreators,
ProjectSidebarDetails,
@@ -300,6 +257,7 @@ import {
ProjectSidebarTags,
requestInstall,
SelectedProjectsFloatingBar,
TeleportOverflowMenu,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -358,14 +316,14 @@ const messages = defineMessages({
id: 'app.project.install-context.back-to-browse',
defaultMessage: 'Back to discover',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
alreadyInstalled: {
id: 'app.project.install-button.already-installed',
defaultMessage: 'This project is already installed',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
switchVersion: {
id: 'app.project.install-button.switch-version',
defaultMessage: 'Switch version',
@@ -529,17 +487,72 @@ const installButtonInstalled = computed(() =>
const installButtonDisabled = computed(
() => installButtonInstalled.value || installButtonLoading.value,
)
const installButtonLabel = computed(() => {
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
return formatMessage(commonMessages.installButton)
})
const installButtonTooltip = computed(() => {
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
return null
const serverInstallLoading = computed(
() => !!data.value && installingServerProjects.value.includes(data.value.id),
)
const installButtonIcon = computed(() => {
if (installButtonLoading.value && !installButtonInstalled.value) return SpinnerIcon
if (!installButtonInstalled.value && !serverProjectSelected.value) return DownloadIcon
return CheckIcon
})
const installButtonIconClass = computed(() =>
installButtonLoading.value && !installButtonInstalled.value ? 'animate-spin' : undefined,
)
const serverProjectHeaderMoreActions = computed(() => [
{
id: 'open-in-browser',
label: formatMessage(commonMessages.openInModrinthButton),
icon: ExternalIcon,
action: openProjectInBrowser,
},
{
divider: true,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
color: 'red',
action: reportProject,
},
])
const projectHeaderMoreActions = computed(() => [
{
id: 'follow',
label: formatMessage(commonMessages.followButton),
icon: HeartIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
label: formatMessage(commonMessages.saveButton),
icon: BookmarkIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
label: formatMessage(commonMessages.openInModrinthButton),
icon: ExternalIcon,
action: openProjectInBrowser,
},
{
divider: true,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
color: 'red',
action: reportProject,
},
])
const projectSearchUrl = computed(
() => `/browse/${isServerProject.value ? 'server' : data.value?.project_type}`,
)
const showSwitchVersion = computed(() => !!instance.value && installed.value)
const onVersionsPage = computed(() => route.name === 'Versions')
@@ -585,6 +598,17 @@ function handleAddServerToInstance() {
showAddServerToInstanceModal(data.value.title, address)
}
function openProjectInBrowser() {
if (!data.value) return
const type = isServerProject.value ? 'project' : data.value.project_type
void openUrl(`https://modrinth.com/${type}/${data.value.slug}`)
}
function reportProject() {
if (!data.value) return
void openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`)
}
async function fetchProjectData() {
const [project, projectV3Result] = await Promise.all([
get_project(route.params.id, 'must_revalidate').catch(handleError),
@@ -0,0 +1,163 @@
<template>
<PageHeader :title="organization.name" :summary="organization.description">
<template #leading>
<Avatar
:src="organization.icon_url"
:alt="organization.name"
:tint-by="organization.id"
size="96px"
/>
</template>
<template #badges>
<PageHeaderBadgeItem :icon="OrganizationIcon" class="px-0 text-primary">
{{ formatMessage(messages.organizationLabel) }}
</PageHeaderBadgeItem>
</template>
<template #metadata>
<PageHeaderMetadata>
<PageHeaderMetadataNumberItem
:icon="UsersIcon"
:value="membersCount"
:label="formatMessage(messages.membersLabel)"
/>
<PageHeaderMetadataNumberItem
:icon="BoxIcon"
:value="projectsCount"
:label="formatMessage(messages.projectsLabel)"
/>
<PageHeaderMetadataNumberItem
:icon="DownloadIcon"
:value="downloads"
:label="formatMessage(messages.downloadsLabel)"
:tooltip="formatNumber(downloads)"
/>
</PageHeaderMetadata>
</template>
<template #actions>
<PageHeaderActions>
<ButtonStyled v-if="canManage" size="large">
<nuxt-link :to="`/organization/${organization.slug}/settings`">
<SettingsIcon />
{{ formatMessage(messages.manage) }}
</nuxt-link>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="moreActions"
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</PageHeaderActions>
</template>
</PageHeader>
</template>
<script setup lang="ts">
import {
BoxIcon,
ClipboardCopyIcon,
DownloadIcon,
MoreVerticalIcon,
OrganizationIcon,
SettingsIcon,
UsersIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
PageHeader,
PageHeaderActions,
PageHeaderBadgeItem,
PageHeaderMetadata,
PageHeaderMetadataNumberItem,
TeleportOverflowMenu,
type TeleportOverflowMenuItem,
useFormatNumber,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
const messages = defineMessages({
downloadsLabel: {
id: 'organization.label.downloads',
defaultMessage: 'downloads',
},
manage: {
id: 'organization.button.manage',
defaultMessage: 'Manage',
},
manageProjects: {
id: 'organization.button.manage-projects',
defaultMessage: 'Manage projects',
},
membersLabel: {
id: 'organization.label.members',
defaultMessage: 'members',
},
organizationLabel: {
id: 'organization.label.organization',
defaultMessage: 'Organization',
},
projectsLabel: {
id: 'organization.label.projects',
defaultMessage: 'projects',
},
})
const props = defineProps<{
organization: {
id: string
name: string
slug: string
description?: string | null
icon_url?: string | null
}
membersCount: number
projectsCount: number
downloads: number
canManage?: boolean
}>()
const emit = defineEmits<{
manageProjects: []
copyId: []
copyPermalink: []
}>()
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
{
id: 'manage-projects',
label: formatMessage(messages.manageProjects),
icon: BoxIcon,
action: () => emit('manageProjects'),
shown: props.canManage,
},
{
divider: true,
shown: props.canManage,
},
{
id: 'copy-id',
label: formatMessage(commonMessages.copyIdButton),
icon: ClipboardCopyIcon,
action: () => emit('copyId'),
},
{
id: 'copy-permalink',
label: formatMessage(commonMessages.copyPermalinkButton),
icon: ClipboardCopyIcon,
action: () => emit('copyPermalink'),
},
])
</script>
@@ -0,0 +1,127 @@
<template>
<ButtonStyled size="large" circular>
<PopoutMenu
v-if="authUser"
:tooltip="
saved ? formatMessage(commonMessages.savedLabel) : formatMessage(commonMessages.saveButton)
"
from="top-right"
:aria-label="formatMessage(commonMessages.saveButton)"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
<template #menu>
<StyledInput
v-model="displayCollectionsSearch"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
wrapper-class="menu-search"
/>
<div v-if="filteredCollections.length > 0" class="collections-list text-primary">
<Checkbox
v-for="option in filteredCollections"
:key="option.id"
:model-value="option.projects.includes(projectId)"
class="popout-checkbox"
@update:model-value="() => collectProject(option, projectId)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">{{ noCollectionsLabel }}</p>
</div>
<ButtonStyled>
<button class="mx-3 mb-3" @click="createCollection">
<PlusIcon aria-hidden="true" />
{{ createNewCollectionLabel }}
</button>
</ButtonStyled>
</template>
</PopoutMenu>
<nuxt-link
v-else
v-tooltip="formatMessage(commonMessages.saveButton)"
:to="signInRoute"
:aria-label="formatMessage(commonMessages.saveButton)"
>
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</template>
<script setup lang="ts">
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
commonMessages,
PopoutMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
type CollectionOption = {
id: string
name: string
projects: string[]
}
const props = defineProps<{
authUser?: unknown
signInRoute: RouteLocationRaw
projectId: string
collections: CollectionOption[]
saved: boolean
baseId: string
noCollectionsLabel: string
createNewCollectionLabel: string
collectProject: (option: CollectionOption, projectId: string) => void | Promise<void>
createCollection: (event: MouseEvent) => void
}>()
const { formatMessage } = useVIntl()
const displayCollectionsSearch = ref('')
const filteredCollections = computed(() =>
props.collections
.filter((collection) =>
collection.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
)
.slice()
.sort((a, b) => a.name.localeCompare(b.name)),
)
</script>
<style scoped lang="scss">
.popout-checkbox {
padding: var(--gap-sm) var(--gap-md);
white-space: nowrap;
&:hover {
filter: brightness(0.95);
}
}
.menu-text {
padding: 0 var(--gap-md);
font-size: var(--font-size-nm);
color: var(--color-secondary);
}
.menu-search {
margin: var(--gap-sm) var(--gap-md);
width: calc(100% - var(--gap-md) * 2);
}
.collections-list {
max-height: 40rem;
overflow-y: auto;
background-color: var(--color-bg);
border-radius: var(--radius-md);
margin: var(--gap-sm) var(--gap-md);
padding: var(--gap-sm);
}
</style>
@@ -0,0 +1,294 @@
<template>
<PageHeader :title="user.username" :summary="summary">
<template #leading>
<Avatar
:src="user.avatar_url"
:alt="user.username"
:size="isModrinthUser ? '64px' : '96px'"
:tint-by="user.username"
circle
/>
</template>
<template v-if="isOfficialAccount || showAffiliateBadge" #badges>
<PageHeaderBadgeItem
v-if="isOfficialAccount"
:icon="BadgeCheckIcon"
:icon-props="{ fill: 'var(--color-brand-highlight)' }"
:tooltip="formatMessage(messages.officialAccount)"
class="border-brand-highlight bg-brand-highlight text-brand"
>
{{ formatMessage(messages.officialAccount) }}
</PageHeaderBadgeItem>
<PageHeaderBadgeItem
v-if="showAffiliateBadge"
:icon="AffiliateIcon"
class="border-brand-highlight bg-brand-highlight text-brand"
>
{{ formatMessage(messages.affiliateLabel) }}
</PageHeaderBadgeItem>
</template>
<template v-if="$slots.summary" #summary>
<slot name="summary" />
</template>
<template v-if="!isModrinthUser" #metadata>
<PageHeaderMetadata>
<PageHeaderMetadataNumberItem
:icon="BoxIcon"
:value="projectsCount"
:label="formatMessage(messages.profileProjectCountLabel, { count: projectsCount })"
/>
<PageHeaderMetadataNumberItem
:icon="DownloadIcon"
:value="downloads"
:label="formatMessage(messages.profileDownloadCountLabel, { count: downloads })"
:tooltip="downloadsTooltip"
/>
<PageHeaderMetadataTimeItem
:icon="CalendarIcon"
:date="user.created"
:label="formatMessage(messages.profileJoinedLabel)"
:tooltip="joinedTooltip"
/>
</PageHeaderMetadata>
</template>
<template #actions>
<PageHeaderActions>
<ButtonStyled v-if="isSelf" size="large">
<nuxt-link to="/settings/profile">
<EditIcon />
{{ formatMessage(commonMessages.editButton) }}
</nuxt-link>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="moreActions"
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</PageHeaderActions>
</template>
</PageHeader>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
AffiliateIcon,
BadgeCheckIcon,
BoxIcon,
CalendarIcon,
ChartIcon,
ClipboardCopyIcon,
CurrencyIcon,
DownloadIcon,
EditIcon,
InfoIcon,
MoreVerticalIcon,
ReportIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
PageHeader,
PageHeaderActions,
PageHeaderBadgeItem,
PageHeaderMetadata,
PageHeaderMetadataNumberItem,
PageHeaderMetadataTimeItem,
TeleportOverflowMenu,
type TeleportOverflowMenuItem,
useFormatDateTime,
useFormatNumber,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
const messages = defineMessages({
affiliateLabel: {
id: 'profile.label.affiliate',
defaultMessage: 'Affiliate',
},
analyticsButton: {
id: 'profile.button.analytics',
defaultMessage: 'View user analytics',
},
billingButton: {
id: 'profile.button.billing',
defaultMessage: 'Manage user billing',
},
editRoleButton: {
id: 'profile.button.edit-role',
defaultMessage: 'Edit role',
},
infoButton: {
id: 'profile.button.info',
defaultMessage: 'View user details',
},
officialAccount: {
id: 'profile.official-account',
defaultMessage: 'Official Modrinth account',
},
profileJoinedLabel: {
id: 'profile.label.joined',
defaultMessage: 'Joined',
},
profileProjectCountLabel: {
id: 'profile.label.project-count',
defaultMessage: '{count, plural, one {project} other {projects}}',
},
profileDownloadCountLabel: {
id: 'profile.label.download-count',
defaultMessage: '{count, plural, one {download} other {downloads}}',
},
profileManageProjectsButton: {
id: 'profile.button.manage-projects',
defaultMessage: 'Manage projects',
},
removeAffiliateButton: {
id: 'profile.button.remove-affiliate',
defaultMessage: 'Remove as affiliate',
},
setAffiliateButton: {
id: 'profile.button.set-affiliate',
defaultMessage: 'Set as affiliate',
},
})
const props = withDefaults(
defineProps<{
user: Labrinth.Users.v3.User
summary?: string | null
authUser?: Labrinth.Users.v3.User | null
isModrinthUser?: boolean
isOfficialAccount?: boolean
showAffiliateBadge?: boolean
isAffiliate?: boolean
isSelf?: boolean
isAdmin?: boolean
isStaff?: boolean
projectsCount?: number
downloads?: number
}>(),
{
summary: null,
authUser: null,
isModrinthUser: false,
isOfficialAccount: false,
showAffiliateBadge: false,
isAffiliate: false,
isSelf: false,
isAdmin: false,
isStaff: false,
projectsCount: 0,
downloads: 0,
},
)
const emit = defineEmits<{
manageProjects: []
report: []
copyId: []
copyPermalink: []
openBilling: []
toggleAffiliate: []
openInfo: []
openAnalytics: []
editRole: []
}>()
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const downloadsTooltip = computed(() => formatNumber(props.downloads))
const joinedTooltip = computed(() => formatDateTime(props.user.created))
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
{
id: 'manage-projects',
label: formatMessage(messages.profileManageProjectsButton),
icon: BoxIcon,
action: () => emit('manageProjects'),
shown: props.isSelf,
},
{
divider: true,
shown: props.isSelf,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
action: () => emit('report'),
color: 'red',
shown: props.authUser?.id !== props.user.id,
},
{
id: 'copy-id',
label: formatMessage(commonMessages.copyIdButton),
icon: ClipboardCopyIcon,
action: () => emit('copyId'),
},
{
id: 'copy-permalink',
label: formatMessage(commonMessages.copyPermalinkButton),
icon: ClipboardCopyIcon,
action: () => emit('copyPermalink'),
},
{
divider: true,
shown: props.isAdmin,
},
{
id: 'open-billing',
label: formatMessage(messages.billingButton),
icon: CurrencyIcon,
action: () => emit('openBilling'),
shown: props.isStaff,
},
{
id: 'toggle-affiliate',
label: props.isAffiliate
? formatMessage(messages.removeAffiliateButton)
: formatMessage(messages.setAffiliateButton),
icon: AffiliateIcon,
action: () => emit('toggleAffiliate'),
shown: props.isAdmin,
remainOnClick: true,
color: props.isAffiliate ? 'red' : 'orange',
},
{
id: 'open-info',
label: formatMessage(messages.infoButton),
icon: InfoIcon,
action: () => emit('openInfo'),
shown: props.isStaff,
},
{
id: 'open-analytics',
label: formatMessage(messages.analyticsButton),
icon: ChartIcon,
action: () => emit('openAnalytics'),
shown: props.isAdmin,
},
{
id: 'edit-role',
label: formatMessage(messages.editRoleButton),
icon: EditIcon,
action: () => emit('editRole'),
shown: props.isAdmin,
},
])
</script>
@@ -2171,9 +2171,6 @@
"profile.label.collection": {
"message": "Kolekce"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {stažení} few {stažení} other {stažení}}"
},
"profile.label.joined": {
"message": "Členem od"
},
@@ -2192,9 +2189,6 @@
"profile.label.organizations": {
"message": "Organizace"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektů}}"
},
"profile.label.saving": {
"message": "Ukládání..."
},
@@ -2528,12 +2522,6 @@
"project.settings.visit-dashboard": {
"message": "Přejít na přehled projektů"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {stažení} few {stažení} other {stažení}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {# sledující} few {# sledující} other {# sledujících}}"
},
"project.versions.title": {
"message": "Verze"
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "Kollektion"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
},
"profile.label.joined": {
"message": "Beigetreten"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "Organisationen"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
},
"profile.label.saving": {
"message": "Speichert..."
},
@@ -3656,12 +3650,6 @@
"project.settings.visit-dashboard": {
"message": "Projekt-Dashboard besuchen"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {Download} other {Downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {Follower} other {Follower}}"
},
"project.status.archived.message": {
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheided sich, das Projekt zu Dearchivieren."
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "Kollektion"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
},
"profile.label.joined": {
"message": "Beigetreten"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "Organisationen"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
},
"profile.label.saving": {
"message": "Speichert..."
},
@@ -3656,12 +3650,6 @@
"project.settings.visit-dashboard": {
"message": "Projekt-Dashboard besuchen"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {Download} other {Downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {Follower} other {Follower}}"
},
"project.status.archived.message": {
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheidet sich das Projekt zu dearchivieren."
},
+28 -10
View File
@@ -3005,6 +3005,24 @@
"muralpay.warning.wallet-address": {
"message": "Double-check your wallet address. Funds sent to an incorrect address cannot be recovered."
},
"organization.button.manage": {
"message": "Manage"
},
"organization.button.manage-projects": {
"message": "Manage projects"
},
"organization.label.downloads": {
"message": "downloads"
},
"organization.label.members": {
"message": "members"
},
"organization.label.organization": {
"message": "Organization"
},
"organization.label.projects": {
"message": "projects"
},
"organization.settings.projects.edit-links.affected-projects": {
"message": "Changes will be applied to {count, plural, one {# project} other {# projects}}."
},
@@ -3074,8 +3092,8 @@
"profile.label.collection": {
"message": "Collection"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
"profile.label.download-count": {
"message": "{count, plural, one {download} other {downloads}}"
},
"profile.label.joined": {
"message": "Joined"
@@ -3095,8 +3113,8 @@
"profile.label.organizations": {
"message": "Organizations"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {project} other {projects}}"
"profile.label.project-count": {
"message": "{count, plural, one {project} other {projects}}"
},
"profile.label.saving": {
"message": "Saving..."
@@ -3260,6 +3278,12 @@
"project.actions.dont-show-again": {
"message": "Don't show again"
},
"project.actions.edit-project": {
"message": "Edit project"
},
"project.actions.rescan-modpack": {
"message": "Rescan modpack"
},
"project.actions.review-project": {
"message": "Review project"
},
@@ -3662,12 +3686,6 @@
"project.settings.visit-dashboard": {
"message": "Visit projects dashboard"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {download} other {downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {follower} other {followers}}"
},
"project.status.archived.message": {
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "Colección"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
},
"profile.label.joined": {
"message": "Se unió"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "Organizaciones"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
},
"profile.label.saving": {
"message": "Guardando..."
},
@@ -3656,12 +3650,6 @@
"project.settings.visit-dashboard": {
"message": "Ver el panel de control de proyectos"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {descarga} other {descargas}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {seguidor} other {seguidores}}"
},
"project.status.archived.message": {
"message": "{title} se ha archivado. {title} no recibirá más actualizaciones hasta que el autor decida desarchivar el proyecto."
},
@@ -2891,9 +2891,6 @@
"profile.label.collection": {
"message": "Colección"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
},
"profile.label.joined": {
"message": "Se unió hace"
},
@@ -2912,9 +2909,6 @@
"profile.label.organizations": {
"message": "Organizaciones"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
},
"profile.label.saving": {
"message": "Guardando..."
},
@@ -3377,12 +3371,6 @@
"project.settings.visit-dashboard": {
"message": "Visita el panel de proyectos"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {descarga} other {descargas}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {seguidor} other {seguidores}}"
},
"project.status.archived.message": {
"message": "{title} ha sido archivado. {title} no recibirá ninguna futura actualización excepto que el autor decida desarchivar el proyecto."
},
@@ -2213,9 +2213,6 @@
"profile.label.collection": {
"message": "Koleksiyon"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {download} other {na download}}"
},
"profile.label.joined": {
"message": "Sumali"
},
@@ -2234,9 +2231,6 @@
"profile.label.organizations": {
"message": "Mga organisasyon"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {proyekto} other {na proyekto}}"
},
"profile.label.saving": {
"message": "Sine-save..."
},
@@ -3074,9 +3074,6 @@
"profile.label.collection": {
"message": "Collection"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {téléchargement} other {téléchargements}}"
},
"profile.label.joined": {
"message": "Rejoint"
},
@@ -3095,9 +3092,6 @@
"profile.label.organizations": {
"message": "Organisations"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {projet} other {projets}}"
},
"profile.label.saving": {
"message": "Sauvegarde en cours..."
},
@@ -3662,12 +3656,6 @@
"project.settings.visit-dashboard": {
"message": "Visiter le tableau de contrôle des projets"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {# téléchargement} other {# téléchargements}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {# suivi} other {# suivis}}"
},
"project.status.archived.message": {
"message": "{title} a été archivé. {title} ne recevra plus de mises à jour jusqu'à ce que l'auteur du projet décide de désarchiver le projet."
},
@@ -1838,9 +1838,6 @@
"profile.label.collection": {
"message": "אוסף"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
},
"profile.label.joined": {
"message": "הצטרף ב-"
},
@@ -1859,9 +1856,6 @@
"profile.label.organizations": {
"message": "ארגונים"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {project} other {projects}}"
},
"profile.label.saving": {
"message": "שומר..."
},
@@ -2966,9 +2966,6 @@
"profile.label.collection": {
"message": "Gyűjtemény"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {letöltés} other {letöltések}}"
},
"profile.label.joined": {
"message": "Csatlakozott"
},
@@ -2987,9 +2984,6 @@
"profile.label.organizations": {
"message": "Szervezetek"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {projekt} other {projektek}}"
},
"profile.label.saving": {
"message": "Mentés..."
},
@@ -3476,12 +3470,6 @@
"project.settings.visit-dashboard": {
"message": "Projektek irányítópult megnyitása"
},
"project.stats.downloads-label": {
"message": "{count} letöltés"
},
"project.stats.followers-label": {
"message": "{count} követő"
},
"project.status.archived.message": {
"message": "A(z) {title} archiválásra került. A(z) {title} nem kap további frissítéseket, kivéve, ha a fejlesztő úgy dönt, hogy visszavonja a projekt archiválását."
},
@@ -2219,9 +2219,6 @@
"profile.label.collection": {
"message": "Koleksi"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, other {pengunduhan}}"
},
"profile.label.joined": {
"message": "Telah bergabung"
},
@@ -2240,9 +2237,6 @@
"profile.label.organizations": {
"message": "Organisasi"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, other {proyek}}"
},
"profile.label.saving": {
"message": "Menyimpan..."
},
@@ -3059,9 +3059,6 @@
"profile.label.collection": {
"message": "Raccolta"
},
"profile.label.downloads": {
"message": "{count} download"
},
"profile.label.joined": {
"message": "Iscrizione"
},
@@ -3080,9 +3077,6 @@
"profile.label.organizations": {
"message": "Organizzazioni"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {progetto} other {progetti}}"
},
"profile.label.saving": {
"message": "Salvando..."
},
@@ -3647,12 +3641,6 @@
"project.settings.visit-dashboard": {
"message": "Visita bacheca del progetto"
},
"project.stats.downloads-label": {
"message": "{count} download"
},
"project.stats.followers-label": {
"message": "{count} follower"
},
"project.status.archived.message": {
"message": "{title} è stato archiviato. {title} non riceverà più aggiornamenti a meno che l'autore decida di rimuoverlo dall'archivio."
},
@@ -2597,9 +2597,6 @@
"profile.label.collection": {
"message": "コレクション"
},
"profile.label.downloads": {
"message": "{count}件のダウンロード"
},
"profile.label.joined": {
"message": "参加: "
},
@@ -2618,9 +2615,6 @@
"profile.label.organizations": {
"message": "組織"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {プロジェクト} other {プロジェクト}}"
},
"profile.label.saving": {
"message": "保存中…"
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "컬렉션"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {다운로드} other {다운로드}}"
},
"profile.label.joined": {
"message": "가입"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "조직"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {프로젝트} other {프로젝트}}"
},
"profile.label.saving": {
"message": "저장..."
},
@@ -3656,12 +3650,6 @@
"project.settings.visit-dashboard": {
"message": "프로젝트 대시보드 방문"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {{count}회 다운로드} other {{count}회 다운로드}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {팔로워} other {팔로워}}"
},
"project.status.archived.message": {
"message": "{title} 은(는) 보관되었습니다. 작성자가 프로젝트 보관 해제를 결정하지 않는 한 더 이상 업데이트가 제공되지 않습니다."
},
@@ -2750,9 +2750,6 @@
"profile.label.collection": {
"message": "Koleksi"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, other {muat turun}}"
},
"profile.label.joined": {
"message": "Telah menyertai"
},
@@ -2771,9 +2768,6 @@
"profile.label.organizations": {
"message": "Organisasi"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, other {projek}}"
},
"profile.label.saving": {
"message": "Sedang menyimpan..."
},
@@ -3188,9 +3182,6 @@
"project.settings.visit-dashboard": {
"message": "Kunjungi papan pemuka projek"
},
"project.stats.downloads-label": {
"message": "{count, plural, other {muat turun}}"
},
"project.status.archived.message": {
"message": "{title} telah diarkibkan. {title} tidak akan menerima sebarang kemas kini lanjut melainkan pengarang memutuskan untuk menyaharkibkan projek."
},
@@ -3065,9 +3065,6 @@
"profile.label.collection": {
"message": "Collectie"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
},
"profile.label.joined": {
"message": "Lid geworden"
},
@@ -3086,9 +3083,6 @@
"profile.label.organizations": {
"message": "Organisaties"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {project} other {projecten}}"
},
"profile.label.saving": {
"message": "Opslaan..."
},
@@ -2678,9 +2678,6 @@
"profile.label.collection": {
"message": "Samling"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {nedlasting} other {nedlastinger}}"
},
"profile.label.joined": {
"message": "Blei med"
},
@@ -2699,9 +2696,6 @@
"profile.label.organizations": {
"message": "Organisasjoner"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {projekt} other {projekt}}"
},
"profile.label.saving": {
"message": "Lagrer..."
},
@@ -3038,12 +3032,6 @@
"project.settings.visit-dashboard": {
"message": "Besøk prosjekt-dashbordet"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {nedlasting} other {nedlastinger}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {følger} other {følgere}}"
},
"project.status.archived.message": {
"message": "{title} har blitt arkivert. {title} kommer ikke til å få noen nye oppdateringer, hvis ikke forfatteren bestemmer seg for å dearkivere prosjektet."
},
@@ -3062,9 +3062,6 @@
"profile.label.collection": {
"message": "Kolekcja"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {pobranie} few {pobrania} other {pobrań}}"
},
"profile.label.joined": {
"message": "Dołączył(-a)"
},
@@ -3083,9 +3080,6 @@
"profile.label.organizations": {
"message": "Organizacje"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektów}}"
},
"profile.label.saving": {
"message": "Zapisywanie..."
},
@@ -3647,12 +3641,6 @@
"project.settings.visit-dashboard": {
"message": "Odwiedź pulpit projektów"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {pobranie} few {pobrania} other {pobrań}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {obserwujący} other {obserwujących}}"
},
"project.status.archived.message": {
"message": "{title} zostało zarchiwizowane. {title} nie będzie otrzymywać żadnych dalszych aktualizacji, chyba że autor zdecyduje się przywrócić projekt."
},
@@ -3656,12 +3656,6 @@
"project.settings.visit-dashboard": {
"message": "Visitar painel de projetos"
},
"project.stats.downloads-label": {
"message": "{count, plural, =0 {Nenhum download} one {# download} other {# downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, =0 {Nenhum seguidor} one {# seguidor} other {# seguidores}}"
},
"project.status.archived.message": {
"message": "{title} foi arquivado. {title} não receberá atualizações a menos que o autor decida desarquivar o projeto."
},
@@ -2561,9 +2561,6 @@
"profile.label.collection": {
"message": "Coleção"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {transferência} other {transferências}}"
},
"profile.label.joined": {
"message": "Entrou"
},
@@ -2582,9 +2579,6 @@
"profile.label.organizations": {
"message": "Organizações"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural,one {projeto} other {projetos}}"
},
"profile.label.saving": {
"message": "A guardar..."
},
@@ -3065,9 +3065,6 @@
"profile.label.collection": {
"message": "Коллекция"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {скачивание} few {скачивания} other {скачиваний}}"
},
"profile.label.joined": {
"message": "Регистрация:"
},
@@ -3086,9 +3083,6 @@
"profile.label.organizations": {
"message": "Организации"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {проект} few {проекта} other {проектов}}"
},
"profile.label.saving": {
"message": "Сохранение..."
},
@@ -3653,12 +3647,6 @@
"project.settings.visit-dashboard": {
"message": "Управление проектами"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {загрузка} few {загрузки} other {загрузок}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {подписчик} few {подписчика} other {подписчиков}}"
},
"project.status.archived.message": {
"message": "{title} помещён в архив. {title} больше не будет получать обновления, если только автор не решит разархивировать проект."
},
@@ -2738,9 +2738,6 @@
"profile.label.collection": {
"message": "Samling"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {nedladdning} other {nedladdningar}}"
},
"profile.label.joined": {
"message": "Gick med"
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "Koleksiyon"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {indirme} other {indirmeler}}"
},
"profile.label.joined": {
"message": "Katılma:"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "Organizasyonlar"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {proje} other {projeler}}"
},
"profile.label.saving": {
"message": "Kaydediliyor..."
},
@@ -3602,12 +3596,6 @@
"project.settings.visit-dashboard": {
"message": "Projeler panelini ziyaret et"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {download} other {downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {follower} other {followers}}"
},
"project.status.archived.message": {
"message": "{title} arşivlenmiş. {title}, yapımcı fikrini değiştirmediği sürece daha fazla güncelleme almayacak."
},
@@ -3071,9 +3071,6 @@
"profile.label.collection": {
"message": "Добірка"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {завантаження} few {завантаження} many {завантажень} other {завантажень}}"
},
"profile.label.joined": {
"message": "Приєднався"
},
@@ -3092,9 +3089,6 @@
"profile.label.organizations": {
"message": "Організації"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {проєкт} few {проєкти} many {проєктів} other {проєктів}}"
},
"profile.label.saving": {
"message": "Збереження…"
},
@@ -3659,12 +3653,6 @@
"project.settings.visit-dashboard": {
"message": "Панель керування проєктами"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {завантаження} few {завантаження} many {завантажень} other {завантаження}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {стежить} few {стежать} many {стежать} other {стежать}}"
},
"project.status.archived.message": {
"message": "«{title}» було архівовано. «{title}» не отримуватиме подальших оновлень допоки автор не вирішить розархівувати проєкт."
},
@@ -2861,9 +2861,6 @@
"profile.label.collection": {
"message": "Bộ sưu tập"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, one {lượt tải xuống} other {lượt tải xuống}}"
},
"profile.label.joined": {
"message": "Đã tham gia"
},
@@ -2882,9 +2879,6 @@
"profile.label.organizations": {
"message": "Tổ chức"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, one {dự án} other {dự án}}"
},
"profile.label.saving": {
"message": "Đang lưu..."
},
@@ -3284,12 +3278,6 @@
"project.settings.visit-dashboard": {
"message": "Truy cập bảng điều khiển dự án"
},
"project.stats.downloads-label": {
"message": "{count, plural, other {lượt tải}}"
},
"project.stats.followers-label": {
"message": "{count, plural, other {lượt theo dõi}}"
},
"project.status.archived.message": {
"message": "{title} đã được lưu trữ. {title} sẽ không nhận được bất kỳ bản cập nhật nào trong tương lai trừ khi tác giả quyết định hủy lưu trữ dự án."
},
@@ -3068,9 +3068,6 @@
"profile.label.collection": {
"message": "收藏夹"
},
"profile.label.downloads": {
"message": "{count} {countPlural, plural, other {次下载}}"
},
"profile.label.joined": {
"message": "加入于"
},
@@ -3089,9 +3086,6 @@
"profile.label.organizations": {
"message": "组织"
},
"profile.label.projects": {
"message": "{count} {countPlural, plural, other {个项目}}"
},
"profile.label.saving": {
"message": "正在保存…"
},
@@ -3656,12 +3650,6 @@
"project.settings.visit-dashboard": {
"message": "访问项目的控制面板"
},
"project.stats.downloads-label": {
"message": "{count, plural, other {下载}}"
},
"project.stats.followers-label": {
"message": "{count, plural, other {关注者}}"
},
"project.status.archived.message": {
"message": "{title} 已归档。除非作者决定取消归档,否则 {title} 将不再更新。"
},
@@ -3074,9 +3074,6 @@
"profile.label.collection": {
"message": "收藏"
},
"profile.label.downloads": {
"message": "下載次數:{count}"
},
"profile.label.joined": {
"message": "加入時間:"
},
@@ -3095,9 +3092,6 @@
"profile.label.organizations": {
"message": "組織"
},
"profile.label.projects": {
"message": "{count} 個{countPlural, plural, other {專案}}"
},
"profile.label.saving": {
"message": "儲存中..."
},
@@ -3662,12 +3656,6 @@
"project.settings.visit-dashboard": {
"message": "前往專案資訊主頁"
},
"project.stats.downloads-label": {
"message": "{count, plural, other {下載}}"
},
"project.stats.followers-label": {
"message": "{count, plural, other {追蹤者}}"
},
"project.status.archived.message": {
"message": "{title} 已封存。{title} 將不會再收到任何後續更新,除非作者決定解除封存該專案。"
},
+226 -258
View File
@@ -130,29 +130,27 @@
@set-processing="setProcessing"
/>
</div>
<ProjectHeader
<ProjectPageHeader
v-if="projectV3Loaded"
:project="project"
:project-v3="projectV3"
:member="!!currentMember"
:show-status-badge="!!currentMember || project.status !== 'approved'"
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
>
<template #actions>
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand" circular>
<nuxt-link
v-tooltip="'Edit project'"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
v-tooltip="formatMessage(messages.editProject)"
:to="`${projectPath}/settings`"
class="!font-bold lg:!hidden"
>
<SettingsIcon aria-hidden="true" />
<SettingsIcon />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
<nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
class="!font-bold max-lg:!hidden"
>
<SettingsIcon aria-hidden="true" />
Edit project
<nuxt-link :to="`${projectPath}/settings`" class="!font-bold max-lg:!hidden">
<SettingsIcon />
{{ formatMessage(messages.editProject) }}
</nuxt-link>
</ButtonStyled>
@@ -160,20 +158,20 @@
<ButtonStyled
v-if="!isServerProject"
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
:color="projectHeaderPrimaryColor"
:circular="!!auth.user && !!currentMember"
>
<button
v-tooltip="
auth.user && currentMember ? formatMessage(commonMessages.downloadButton) : ''
auth.user && currentMember
? formatMessage(commonMessages.downloadButton)
: undefined
"
@click="(event) => downloadModal.show(event)"
type="button"
:aria-label="formatMessage(commonMessages.downloadButton)"
@click="handleProjectHeaderPrimary"
>
<DownloadIcon aria-hidden="true" />
<DownloadIcon />
{{
auth.user && currentMember ? '' : formatMessage(commonMessages.downloadButton)
}}
@@ -182,19 +180,21 @@
<ButtonStyled
v-else
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
:color="projectHeaderPrimaryColor"
:circular="!!auth.user && !!currentMember"
>
<button
v-tooltip="auth.user && currentMember && !openInAppModal?.open ? 'Play' : ''"
@click="handlePlayServerProject"
v-tooltip="
auth.user && currentMember
? formatMessage(commonMessages.playButton)
: undefined
"
type="button"
:aria-label="formatMessage(commonMessages.playButton)"
@click="handleProjectHeaderPrimary"
>
<PlayIcon aria-hidden="true" />
{{ auth.user && currentMember ? '' : 'Play' }}
<PlayIcon />
{{ auth.user && currentMember ? '' : formatMessage(commonMessages.playButton) }}
</button>
</ButtonStyled>
</div>
@@ -204,109 +204,100 @@
v-if="!isServerProject"
size="large"
circular
:color="
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
:color="projectHeaderPrimaryColor"
>
<button
type="button"
:aria-label="formatMessage(commonMessages.downloadButton)"
class="flex sm:hidden"
@click="(event) => downloadModal.show(event)"
@click="handleProjectHeaderPrimary"
>
<DownloadIcon aria-hidden="true" />
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled
v-else
size="large"
circular
:color="
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
>
<button aria-label="Play" class="flex sm:hidden" @click="handlePlayServerProject">
<PlayIcon aria-hidden="true" />
<ButtonStyled v-else size="large" circular :color="projectHeaderPrimaryColor">
<button
type="button"
:aria-label="formatMessage(commonMessages.playButton)"
class="flex sm:hidden"
@click="handleProjectHeaderPrimary"
>
<PlayIcon />
</button>
</ButtonStyled>
</div>
<Tooltip
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
v-if="
showProjectHeaderCreateServerAction && flags.showProjectPageCreateServersTooltip
"
theme="dismissable-prompt"
class="inline-flex"
:triggers="[]"
:shown="flags.showProjectPageCreateServersTooltip"
:auto-hide="false"
placement="bottom-start"
>
<ButtonStyled size="large" circular>
<ButtonStyled circular size="large">
<nuxt-link
v-tooltip="formatMessage(messages.createServerTooltip)"
:to="`/hosting?project=${project.id}#plan`"
@click="
() => {
flags.showProjectPageCreateServersTooltip = false
saveFeatureFlags()
}
"
:to="projectHeaderCreateServerTo"
:aria-label="formatMessage(messages.serversPromoTitle)"
@click="dismissProjectHeaderCreateServerPrompt"
>
<ServerPlusIcon aria-hidden="true" />
<ServerPlusIcon />
</nuxt-link>
</ButtonStyled>
<template #popper>
<div class="grid grid-cols-[min-content] gap-1">
<div class="flex min-w-60 items-center justify-between gap-4">
<h3
class="m-0 flex items-center gap-2 whitespace-nowrap text-base font-bold text-contrast"
>
{{ formatMessage(messages.serversPromoTitle) }}
<TagItem
:style="{
'--_color': 'var(--color-brand)',
'--_bg-color': 'var(--color-brand-highlight)',
}"
>{{ formatMessage(commonMessages.newBadge) }}</TagItem
<div class="grid max-w-[18rem] gap-2">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
<h3 class="m-0 text-base font-bold text-contrast">
{{ formatMessage(messages.serversPromoTitle) }}
</h3>
<span
class="rounded-full bg-brand-highlight px-2 py-0.5 text-xs font-bold text-brand"
>
</h3>
{{ formatMessage(commonMessages.newBadge) }}
</span>
</div>
<ButtonStyled size="small" circular>
<button
v-tooltip="formatMessage(messages.dontShowAgain)"
@click="
() => {
flags.showProjectPageCreateServersTooltip = false
saveFeatureFlags()
}
"
@click="dismissProjectHeaderCreateServerPrompt"
>
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
<p class="m-0 text-sm font-medium leading-tight text-secondary">
{{ formatMessage(messages.serversPromoDescription) }}
</p>
<p class="m-0 text-wrap text-sm font-bold text-primary">
<p class="m-0 text-sm font-semibold text-contrast">
<IntlFormatted
:message-id="messages.serversPromoPricing"
:values="{
price: formatPrice(500, 'USD', true),
}"
:values="{ price: formatPrice(500, 'USD', true) }"
>
<template #small="{ children }">
<span class="text-xs">
<component :is="() => children" />
</span>
<small><component :is="() => children" /></small>
</template>
</IntlFormatted>
</p>
</div>
</template>
</Tooltip>
<ButtonStyled size="large" circular>
<ButtonStyled v-else-if="showProjectHeaderCreateServerAction" circular size="large">
<nuxt-link
v-tooltip="formatMessage(messages.createServerTooltip)"
:to="projectHeaderCreateServerTo"
:aria-label="formatMessage(messages.serversPromoTitle)"
@click="dismissProjectHeaderCreateServerPrompt"
>
<ServerPlusIcon />
</nuxt-link>
</ButtonStyled>
<ButtonStyled circular size="large">
<ClientOnly>
<button
v-if="auth.user"
@@ -315,14 +306,15 @@
? formatMessage(commonMessages.unfollowButton)
: formatMessage(commonMessages.followButton)
"
type="button"
:aria-label="
following
? formatMessage(commonMessages.unfollowButton)
: formatMessage(commonMessages.followButton)
"
@click="userFollowProject(project)"
@click="followProjectFromHeader"
>
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
<HeartIcon :fill="following ? 'currentColor' : 'none'" />
</button>
<nuxt-link
v-else
@@ -343,156 +335,31 @@
</template>
</ClientOnly>
</ButtonStyled>
<ButtonStyled size="large" circular>
<PopoutMenu
v-if="auth.user"
:tooltip="
collections.some((x) => x.projects.includes(project.id))
? formatMessage(commonMessages.savedLabel)
: formatMessage(commonMessages.saveButton)
"
from="top-right"
:aria-label="formatMessage(commonMessages.saveButton)"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<StyledInput
v-model="displayCollectionsSearch"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
wrapper-class="menu-search"
/>
<div v-if="collections.length > 0" class="collections-list text-primary">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">{{ formatMessage(messages.noCollectionsFound) }}</p>
</div>
<ButtonStyled>
<button
class="mx-3 mb-3"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.createNewCollection) }}
</button>
</ButtonStyled>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" :to="signInRouteObj" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ProjectCollectionSaveButton
:auth-user="auth.user"
:sign-in-route="signInRouteObj"
:project-id="project.id"
:collections="collections"
:saved="collections.some((x) => x.projects.includes(project.id))"
:base-id="baseId"
:no-collections-label="formatMessage(messages.noCollectionsFound)"
:create-new-collection-label="formatMessage(messages.createNewCollection)"
:collect-project="onUserCollectProject"
:create-collection="(event) => modalCollection?.show(event)"
/>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="projectHeaderMoreActions"
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
:options="[
{
id: 'analytics',
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
hoverOnly: true,
shown: auth.user && !!currentMember,
},
{
divider: true,
shown: auth.user && !!currentMember,
},
{
id: 'moderation-checklist',
action: openModerationChecklistFromMenu,
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'tech-review',
link: `/moderation/technical-review/${project.id}`,
color: 'orange',
hoverOnly: true,
shown: auth.user && tags.staffRoles.includes(auth.user.role),
},
{
id: 'moderation-modpack-rescan',
action: () => scanModal.show(),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
project.actualProjectType === 'modpack',
},
{
divider: true,
shown: auth.user && tags.staffRoles.includes(auth.user.role),
},
{
id: 'report',
action: () =>
auth.user
? reportProject(project.id)
: navigateTo(
getSignInRouteObj(route, getReportPath('project', project.id)),
),
color: 'red',
hoverOnly: true,
shown: !isMember,
},
{ id: 'copy-id', action: () => copyId() },
{ id: 'copy-permalink', action: () => copyPermalink() },
]"
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
:dropdown-id="`${baseId}-more-options`"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
<ChartIcon aria-hidden="true" />
{{ formatMessage(commonMessages.analyticsButton) }}
</template>
<template #moderation-checklist>
<ScaleIcon aria-hidden="true" /> {{ formatMessage(messages.reviewProject) }}
</template>
<template #tech-review> <ScanEyeIcon aria-hidden="true" /> Tech review </template>
<template #moderation-modpack-rescan>
<FolderSearchIcon aria-hidden="true" /> Rescan modpack
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
<template #copy-permalink>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyPermalinkButton) }}
</template>
</OverflowMenu>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</template>
</ProjectHeader>
</ProjectPageHeader>
<ProjectMemberHeader
v-if="currentMember"
:project="project"
@@ -726,7 +593,6 @@
<script setup>
import {
BookmarkIcon,
BookTextIcon,
CalendarIcon,
ChartIcon,
@@ -739,7 +605,6 @@ import {
ListIcon,
MoreVerticalIcon,
PlayIcon,
PlusIcon,
ReportIcon,
ScaleIcon,
ScanEyeIcon,
@@ -752,7 +617,6 @@ import {
Admonition,
Avatar,
ButtonStyled,
Checkbox,
commonMessages,
defineMessages,
injectModrinthClient,
@@ -761,12 +625,10 @@ import {
NavTabs,
NewModal,
OpenInAppModal,
OverflowMenu,
PopoutMenu,
PROJECT_DEP_MARKER_QUERY,
ProjectBackgroundGradient,
ProjectEnvironmentModal,
ProjectHeader,
ProjectPageHeader,
ProjectSidebarCompatibility,
ProjectSidebarCreators,
ProjectSidebarDetails,
@@ -774,8 +636,7 @@ import {
ProjectSidebarServerInfo,
ProjectSidebarTags,
provideProjectPageContext,
StyledInput,
TagItem,
TeleportOverflowMenu,
useDebugLogger,
useFormatDateTime,
useFormatPrice,
@@ -795,6 +656,7 @@ import MessageBanner from '~/components/ui/MessageBanner.vue'
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
import ModpackScanModal from '~/components/ui/moderation/ModpackScanModal.vue'
import ProjectCollectionSaveButton from '~/components/ui/ProjectCollectionSaveButton.vue'
import ProjectDownloadModal from '~/components/ui/ProjectDownloadModal/index.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import { getSignInRouteObj } from '~/composables/auth.ts'
@@ -851,11 +713,11 @@ const flags = useFeatureFlags()
const cosmetics = useCosmetics()
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatPrice = useFormatPrice()
const debug = useDebugLogger('DownloadModal')
@@ -925,10 +787,6 @@ const messages = defineMessages({
id: 'project.navigation.changelog',
defaultMessage: 'Changelog',
},
createNewCollection: {
id: 'project.collections.create-new',
defaultMessage: 'Create new collection',
},
createServer: {
id: 'project.actions.create-server',
defaultMessage: 'Create a server',
@@ -937,6 +795,10 @@ const messages = defineMessages({
id: 'project.actions.create-server-tooltip',
defaultMessage: 'Create a server',
},
createNewCollection: {
id: 'project.collections.create-new',
defaultMessage: 'Create new collection',
},
descriptionTab: {
id: 'project.description.title',
defaultMessage: 'Description',
@@ -945,9 +807,9 @@ const messages = defineMessages({
id: 'project.actions.dont-show-again',
defaultMessage: "Don't show again",
},
downloadsStat: {
id: 'project.stats.downloads-label',
defaultMessage: '{count, plural, one {download} other {downloads}}',
editProject: {
id: 'project.actions.edit-project',
defaultMessage: 'Edit project',
},
errorLoadingProject: {
id: 'project.error.loading',
@@ -975,10 +837,6 @@ const messages = defineMessages({
id: 'project.environment.migration.learn-more',
defaultMessage: 'Learn more about this change',
},
followersStat: {
id: 'project.stats.followers-label',
defaultMessage: '{count, plural, one {follower} other {followers}}',
},
galleryTab: {
id: 'project.gallery.title',
defaultMessage: 'Gallery',
@@ -1039,6 +897,10 @@ const messages = defineMessages({
id: 'project.actions.review-project',
defaultMessage: 'Review project',
},
rescanModpack: {
id: 'project.actions.rescan-modpack',
defaultMessage: 'Rescan modpack',
},
serversPromoDescription: {
id: 'project.actions.servers-promo.description',
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
@@ -1066,6 +928,7 @@ const messages = defineMessages({
})
const modalLicense = ref(null)
const modalCollection = useTemplateRef('modal_collection')
const licenseText = ref('')
const createdDate = computed(() =>
@@ -1104,13 +967,8 @@ async function getLicenseData(event) {
}
}
const displayCollectionsSearch = ref('')
const collections = computed(() =>
user.value && user.value.collections
? user.value.collections.filter((x) =>
x.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
)
: [],
user.value && user.value.collections ? user.value.collections : [],
)
if (
@@ -1844,6 +1702,89 @@ const canCreateServerFrom = computed(() => {
return project.value.project_type === 'modpack' && project.value.server_side !== 'unsupported'
})
const projectSearchUrl = computed(
() => `/discover/${isServerProject.value ? 'servers' : `${project.value?.project_type}s`}`,
)
const projectPath = computed(() =>
project.value
? `/${project.value.project_type}/${project.value.slug ? project.value.slug : project.value.id}`
: '',
)
const projectHeaderPrimaryColor = computed(() =>
currentMember.value || route.name === 'type-project-version-version' ? 'standard' : 'brand',
)
const showProjectHeaderCreateServerAction = computed(
() => canCreateServerFrom.value && flags.value.showProjectPageQuickServerButton,
)
const projectHeaderCreateServerTo = computed(() =>
project.value ? `/hosting?project=${project.value.id}#plan` : '/hosting',
)
const projectHeaderMoreActions = computed(() => {
const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role))
return [
{
id: 'analytics',
label: formatMessage(commonMessages.analyticsButton),
icon: ChartIcon,
link: `${projectPath.value}/settings/analytics`,
shown: !!auth.value.user && !!currentMember.value,
},
{
divider: true,
shown: !!auth.value.user && !!currentMember.value,
},
{
id: 'moderation-checklist',
label: formatMessage(messages.reviewProject),
icon: ScaleIcon,
action: openModerationChecklistFromMenu,
color: 'orange',
shown: !!auth.value.user && isStaff && !showModerationChecklist.value,
},
{
id: 'tech-review',
label: 'Tech review',
icon: ScanEyeIcon,
link: `/moderation/technical-review/${project.value?.id}`,
color: 'orange',
shown: !!auth.value.user && isStaff,
},
{
id: 'moderation-modpack-rescan',
label: formatMessage(messages.rescanModpack),
icon: FolderSearchIcon,
action: () => scanModal.value?.show(),
color: 'orange',
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
},
{
divider: true,
shown: !!auth.value.user && isStaff,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
action: reportProjectFromHeader,
color: 'red',
shown: !isMember.value,
},
{
id: 'copy-id',
label: formatMessage(commonMessages.copyIdButton),
icon: ClipboardCopyIcon,
action: copyId,
},
{
id: 'copy-permalink',
label: formatMessage(commonMessages.copyPermalinkButton),
icon: ClipboardCopyIcon,
action: copyPermalink,
},
]
})
const createCanonicalUrl = () =>
project.value ? `https://modrinth.com/project/${project.value.id}` : undefined
@@ -1878,6 +1819,33 @@ if (!route.name.startsWith('type-project-settings')) {
const onUserCollectProject = useClientTry(userCollectProject)
function handleProjectHeaderPrimary(event) {
if (isServerProject.value) {
handlePlayServerProject()
} else {
downloadModal.value?.show(event)
}
}
function dismissProjectHeaderCreateServerPrompt() {
flags.value.showProjectPageCreateServersTooltip = false
saveFeatureFlags()
}
function followProjectFromHeader() {
if (!project.value) return
userFollowProject(project.value)
}
function reportProjectFromHeader() {
if (!project.value) return
if (auth.value.user) {
reportProject(project.value.id)
} else {
navigateTo(getSignInRouteObj(route, getReportPath('project', project.value.id)))
}
}
watch(
[versionsV3, _versionsV3Error],
([data, error]) => {
@@ -62,86 +62,16 @@
</template>
<template v-else>
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
</template>
<template #title>
{{ organization.name }}
</template>
<template #title-suffix>
<div class="ml-1 flex items-center gap-2 font-semibold">
<OrganizationIcon />
Organization
</div>
</template>
<template #summary>
{{ organization.description }}
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<UsersIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
members
</div>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div>
<div
v-tooltip="formatNumber(sumDownloads)"
class="flex items-center gap-2 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
</div>
</template>
<template #actions>
<ButtonStyled v-if="auth.user && currentMember" size="large">
<NuxtLink :to="`/organization/${organization.slug}/settings`">
<SettingsIcon aria-hidden="true" />
Manage
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () =>
router.push('/organization/' + organization?.slug + '/settings/projects'),
hoverFilledOnly: true,
shown: !!(auth.user && currentMember),
},
{ divider: true, shown: !!(auth?.user && currentMember) },
{ id: 'copy-id', action: () => copyId() },
{ id: 'copy-permalink', action: () => copyPermalink() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
Manage projects
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
<template #copy-permalink>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyPermalinkButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
<OrganizationPageHeader
:organization="organization"
:members-count="acceptedMembers?.length || 0"
:projects-count="projects?.length || 0"
:downloads="sumDownloads"
:can-manage="!!(auth.user && currentMember)"
@manage-projects="router.push(`/organization/${organization.slug}/settings/projects`)"
@copy-id="copyId"
@copy-permalink="copyPermalink"
/>
</div>
<div class="normal-page__sidebar">
<AdPlaceholder v-if="!auth.user" />
@@ -293,11 +223,7 @@ import {
BoxIcon,
ChartIcon,
CheckIcon,
ClipboardCopyIcon,
CrownIcon,
DownloadIcon,
MoreVerticalIcon,
OrganizationIcon,
SettingsIcon,
SpinnerIcon,
UsersIcon,
@@ -307,16 +233,13 @@ import {
Avatar,
ButtonStyled,
commonMessages,
ContentPageHeader,
injectModrinthClient,
NavTabs,
OverflowMenu,
PROJECT_DEP_MARKER_QUERY,
ProjectCard,
ProjectCardList,
SidebarCard,
useCompactNumber,
useFormatNumber,
useVIntl,
} from '@modrinth/ui'
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
@@ -326,6 +249,7 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import NavStack from '~/components/ui/NavStack.vue'
import OrganizationPageHeader from '~/components/ui/OrganizationPageHeader.vue'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
import {
OrganizationContext,
@@ -342,7 +266,6 @@ type ProjectV3 = Labrinth.Projects.v3.Project & {
const vintl = useVIntl()
const { formatMessage } = vintl
const formatNumber = useFormatNumber()
const { formatCompactNumber } = useCompactNumber()
const auth: { user: any } & any = await useAuth()
+49 -258
View File
@@ -120,35 +120,34 @@
</NewModal>
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<Avatar
:src="user.avatar_url"
:alt="user.username"
:size="isModrinthUser ? '64px' : '96px'"
circle
/>
</template>
<template #title>
<span class="flex items-center gap-2">
{{ user.username }}
<BadgeCheckIcon
v-if="isOfficialAccount"
v-tooltip="formatMessage(messages.officialAccount)"
class="size-5 text-brand"
fill="var(--color-brand-highlight)"
/>
<TagItem
v-if="isAdminViewing && isAffiliate"
:style="{
'--_color': 'var(--color-brand)',
'--_bg-color': 'var(--color-brand-highlight)',
}"
>
<AffiliateIcon /> Affiliate
</TagItem>
</span>
</template>
<UserPageHeader
:user="user"
:summary="isModrinthUser ? null : profileHeaderSummary"
:auth-user="auth.user"
:is-modrinth-user="isModrinthUser"
:is-official-account="isOfficialAccount"
:show-affiliate-badge="isAdminViewing && !!isAffiliate"
:is-affiliate="!!isAffiliate"
:is-self="auth.user?.id === user.id"
:is-admin="isAdminViewing"
:is-staff="!!(auth.user && isStaff(auth.user))"
:projects-count="projects?.length || 0"
:downloads="sumDownloads"
@manage-projects="navigateTo('/dashboard/projects')"
@report="reportProfileFromHeader"
@copy-id="copyId"
@copy-permalink="copyPermalink"
@open-billing="navigateTo(`/admin/billing/${user.id}`)"
@toggle-affiliate="toggleAffiliate(user.id)"
@open-info="userDetailsModal?.show()"
@open-analytics="
navigateTo({
path: '/dashboard/analytics',
query: { user: user.username || user.id },
})
"
@edit-role="openRoleEditModal"
>
<template v-if="isModrinthUser" #summary>
<IntlFormatted :message-id="messages.officialAccountBio">
<template #support-link>
@@ -173,159 +172,7 @@
</template>
</IntlFormatted>
</template>
<template v-else #summary>
{{
user.bio
? user.bio
: projects?.length > 0
? formatMessage(messages.bioFallbackCreator)
: formatMessage(messages.bioFallbackUser)
}}
</template>
<template v-if="!isModrinthUser" #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{
formatMessage(messages.profileProjectsLabel, {
count: formatCompactNumber(projects?.length || 0),
countPlural: formatCompactNumberPlural(projects?.length || 0),
})
}}
</div>
<div
v-tooltip="formatNumber(sumDownloads)"
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{
formatMessage(messages.profileDownloadsLabel, {
count: formatCompactNumber(sumDownloads),
countPlural: formatCompactNumberPlural(sumDownloads),
})
}}
</div>
<div
v-tooltip="formatDateTime(user.created)"
class="flex items-center gap-2 font-semibold"
>
<CalendarIcon class="h-6 w-6 text-secondary" />
{{ formatMessage(messages.profileJoinedLabel) }}
{{ formatRelativeTime(user.created) }}
</div>
</template>
<template #actions>
<ButtonStyled size="large">
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
<EditIcon aria-hidden="true" />
{{ formatMessage(commonMessages.editButton) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () => navigateTo('/dashboard/projects'),
hoverOnly: true,
shown: auth.user && auth.user.id === user.id,
},
{ divider: true, shown: auth.user && auth.user.id === user.id },
{
id: 'report',
action: () =>
auth.user ? reportUser(user.id) : navigateTo(getSignInRouteObj(route)),
color: 'red',
hoverOnly: true,
shown: auth.user?.id !== user.id,
},
{ id: 'copy-id', action: () => copyId() },
{ id: 'copy-permalink', action: () => copyPermalink() },
{
divider: true,
shown: auth.user && isAdmin(auth.user),
},
{
id: 'open-billing',
action: () => navigateTo(`/admin/billing/${user.id}`),
shown: auth.user && isStaff(auth.user),
},
{
id: 'toggle-affiliate',
action: () => toggleAffiliate(user.id),
shown: isAdminViewing,
remainOnClick: true,
color: isAffiliate ? 'red' : 'orange',
},
{
id: 'open-info',
action: () => $refs.userDetailsModal.show(),
shown: auth.user && isStaff(auth.user),
},
{
id: 'open-analytics',
action: () =>
navigateTo({
path: '/dashboard/analytics',
query: { user: user.username || user.id },
}),
shown: auth.user && isAdmin(auth.user),
},
{
id: 'edit-role',
action: () => openRoleEditModal(),
shown: auth.user && isAdmin(auth.user),
},
]"
aria-label="More options"
:dropdown-id="`${baseId}-more-options`"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
{{ formatMessage(messages.profileManageProjectsButton) }}
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
<template #copy-permalink>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyPermalinkButton) }}
</template>
<template #open-billing>
<CurrencyIcon aria-hidden="true" />
{{ formatMessage(messages.billingButton) }}
</template>
<template #open-info>
<InfoIcon aria-hidden="true" />
{{ formatMessage(messages.infoButton) }}
</template>
<template #open-analytics>
<ChartIcon aria-hidden="true" />
{{ formatMessage(messages.analyticsButton) }}
</template>
<template #toggle-affiliate>
<AffiliateIcon aria-hidden="true" />
{{
formatMessage(
isAffiliate ? messages.removeAffiliateButton : messages.setAffiliateButton,
)
}}
</template>
<template #edit-role>
<EditIcon aria-hidden="true" />
{{ formatMessage(messages.editRoleButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</UserPageHeader>
</div>
<div class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -509,23 +356,12 @@
</template>
<script setup>
import {
AffiliateIcon,
BadgeCheckIcon,
BoxIcon,
CalendarIcon,
ChartIcon,
CheckIcon,
ClipboardCopyIcon,
CurrencyIcon,
DownloadIcon,
EditIcon,
GlobeIcon,
InfoIcon,
LibraryIcon,
LinkIcon,
LockIcon,
MoreVerticalIcon,
ReportIcon,
SaveIcon,
SpinnerIcon,
XIcon,
@@ -535,22 +371,15 @@ import {
ButtonStyled,
Combobox,
commonMessages,
ContentPageHeader,
defineMessages,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NavTabs,
NewModal,
OverflowMenu,
ProjectCard,
ProjectCardList,
TagItem,
useCompactNumber,
useFormatDateTime,
useFormatNumber,
UserBadges,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
@@ -561,6 +390,7 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import UserPageHeader from '~/components/ui/UserPageHeader.vue'
import { getSignInRouteObj } from '~/composables/auth.ts'
import { projectUserSorting } from '~/utils/projects.ts'
import { reportUser } from '~/utils/report-helpers.ts'
@@ -575,35 +405,14 @@ const config = useRuntimeConfig()
const queryClient = useQueryClient()
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const { addNotification } = injectNotificationManager()
const baseId = useId()
const messages = defineMessages({
profileProjectsLabel: {
id: 'profile.label.projects',
defaultMessage: '{count} {countPlural, plural, one {project} other {projects}}',
},
profileDownloadsLabel: {
id: 'profile.label.downloads',
defaultMessage: '{count} {countPlural, plural, one {download} other {downloads}}',
},
collectionProjectsCount: {
id: 'profile.collection.projects-count',
defaultMessage: '{count, plural, one {# project} other {# projects}}',
},
profileJoinedLabel: {
id: 'profile.label.joined',
defaultMessage: 'Joined',
},
savingLabel: {
id: 'profile.label.saving',
defaultMessage: 'Saving...',
@@ -669,10 +478,6 @@ const messages = defineMessages({
id: 'profile.label.badges',
defaultMessage: 'Badges',
},
profileManageProjectsButton: {
id: 'profile.button.manage-projects',
defaultMessage: 'Manage projects',
},
profileMetaDescription: {
id: 'profile.meta.description',
defaultMessage: "Download {username}'s projects on Modrinth",
@@ -699,42 +504,10 @@ const messages = defineMessages({
defaultMessage:
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
},
billingButton: {
id: 'profile.button.billing',
defaultMessage: 'Manage user billing',
},
infoButton: {
id: 'profile.button.info',
defaultMessage: 'View user details',
},
analyticsButton: {
id: 'profile.button.analytics',
defaultMessage: 'View user analytics',
},
setAffiliateButton: {
id: 'profile.button.set-affiliate',
defaultMessage: 'Set as affiliate',
},
removeAffiliateButton: {
id: 'profile.button.remove-affiliate',
defaultMessage: 'Remove as affiliate',
},
affiliateLabel: {
id: 'profile.label.affiliate',
defaultMessage: 'Affiliate',
},
editRoleButton: {
id: 'profile.button.edit-role',
defaultMessage: 'Edit role',
},
userNotFoundError: {
id: 'profile.error.not-found',
defaultMessage: 'User not found',
},
officialAccount: {
id: 'profile.official-account',
defaultMessage: 'Official Modrinth account',
},
officialAccountBio: {
id: 'profile.official-account.bio',
defaultMessage:
@@ -891,14 +664,32 @@ async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
}
function reportProfileFromHeader() {
if (!user.value) return
if (auth.value.user) {
reportUser(user.value.id)
} else {
navigateTo(getSignInRouteObj(route))
}
}
const isAffiliate = computed(() => user.value?.badges & UserBadge.AFFILIATE)
const isAdminViewing = computed(() => isAdmin(auth.value.user))
const userDetailsModal = useTemplateRef('userDetailsModal')
async function toggleAffiliate(id) {
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
queryClient.invalidateQueries({ queryKey: ['user', userId] })
}
const profileHeaderSummary = computed(() =>
user.value?.bio
? user.value.bio
: (projects.value?.length ?? 0) === 0
? formatMessage(messages.bioFallbackUser)
: formatMessage(messages.bioFallbackCreator),
)
const navLinks = computed(() => [
{
label: formatMessage(commonMessages.allProjectType),
@@ -950,7 +741,7 @@ function saveRoleEdit() {
editRoleModal.value?.hide()
})
.catch(() => {
.catch((error) => {
console.error('Failed to update user role:', error)
addNotification({