mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 13:16:38 +00:00
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:
@@ -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>
|
||||||
+63
@@ -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": {
|
"friends.sign-in-to-add-friends": {
|
||||||
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
|
"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": {
|
"instance.add-server.add-and-play": {
|
||||||
"message": "Add and play"
|
"message": "Add and play"
|
||||||
},
|
},
|
||||||
@@ -830,6 +857,9 @@
|
|||||||
"instance.files.save-as": {
|
"instance.files.save-as": {
|
||||||
"message": "Save as..."
|
"message": "Save as..."
|
||||||
},
|
},
|
||||||
|
"instance.playtime.never-played": {
|
||||||
|
"message": "Never played"
|
||||||
|
},
|
||||||
"instance.server-modal.address": {
|
"instance.server-modal.address": {
|
||||||
"message": "Address"
|
"message": "Address"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,207 +13,31 @@
|
|||||||
@unlinked="fetchInstance"
|
@unlinked="fetchInstance"
|
||||||
/>
|
/>
|
||||||
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
||||||
<ContentPageHeader>
|
<InstancePageHeader
|
||||||
<template #icon>
|
:instance="instance"
|
||||||
<Avatar
|
:icon-src="icon"
|
||||||
:src="icon ? icon : undefined"
|
:is-server-instance="isServerInstance"
|
||||||
:alt="instance.name"
|
:show-instance-play-time="showInstancePlayTime"
|
||||||
size="64px"
|
:time-played="timePlayed"
|
||||||
:tint-by="instance.id"
|
:playing="playing"
|
||||||
/>
|
:loading="loading"
|
||||||
</template>
|
:stopping="stopping"
|
||||||
<template #title>
|
:loading-server-ping="loadingServerPing"
|
||||||
{{ instance.name }}
|
:players-online="playersOnline"
|
||||||
</template>
|
:status-online="statusOnline"
|
||||||
<template #stats>
|
:recent-plays="recentPlays"
|
||||||
<div class="flex items-center flex-wrap gap-2">
|
:ping="ping"
|
||||||
<template v-if="!isServerInstance">
|
:minecraft-server="minecraftServer"
|
||||||
<div class="flex items-center gap-2 capitalize font-medium">
|
:linked-project-v3="linkedProjectV3"
|
||||||
{{ instance.loader }} {{ instance.game_version }}
|
@repair="() => repairInstance()"
|
||||||
</div>
|
@stop="() => stopInstance('InstancePage')"
|
||||||
|
@play="() => startInstance('InstancePage')"
|
||||||
<template v-if="showInstancePlayTime">
|
@play-server="() => handlePlayServer()"
|
||||||
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
@settings="() => settingsModal?.show()"
|
||||||
|
@open-folder="() => instance && showInstanceInFolder(instance.id)"
|
||||||
<div class="flex items-center gap-2 font-medium">
|
@export="() => exportModal?.show()"
|
||||||
<template v-if="timePlayed > 0">
|
@create-shortcut="() => createShortcut()"
|
||||||
{{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
|
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
|
||||||
<NavTabs :links="tabs" />
|
<NavTabs :links="tabs" />
|
||||||
@@ -272,49 +96,30 @@ import {
|
|||||||
BoxesIcon,
|
BoxesIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
DownloadIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
EditIcon,
|
EditIcon,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
HashIcon,
|
HashIcon,
|
||||||
MoreVerticalIcon,
|
|
||||||
PackageIcon,
|
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ServerIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
UserPlusIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { injectNotificationManager, NavTabs, useLoadingBarToken } from '@modrinth/ui'
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
ContentPageHeader,
|
|
||||||
injectNotificationManager,
|
|
||||||
NavTabs,
|
|
||||||
OverflowMenu,
|
|
||||||
ServerOnlinePlayers,
|
|
||||||
ServerPing,
|
|
||||||
ServerRecentPlays,
|
|
||||||
ServerRegion,
|
|
||||||
useLoadingBarToken,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import duration from 'dayjs/plugin/duration'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import ExportModal from '@/components/ui/ExportModal.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 InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||||
import {
|
import {
|
||||||
@@ -336,7 +141,6 @@ import { injectServerInstall } from '@/providers/server-install'
|
|||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { useBreadcrumbs, useTheming } from '@/store/state'
|
import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const { addNotification, handleError } = injectNotificationManager()
|
const { addNotification, handleError } = injectNotificationManager()
|
||||||
@@ -758,22 +562,6 @@ const timePlayed = computed(() => {
|
|||||||
: 0
|
: 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(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenInstances()
|
unlistenInstances()
|
||||||
|
|||||||
@@ -59,142 +59,100 @@
|
|||||||
>
|
>
|
||||||
<ProjectBackgroundGradient :project="data" />
|
<ProjectBackgroundGradient :project="data" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<ProjectHeader
|
<ProjectPageHeader
|
||||||
v-else
|
v-else
|
||||||
:project="data"
|
:project="data"
|
||||||
:project-v3="projectV3"
|
:project-v3="projectV3"
|
||||||
:ping="serverPing"
|
:show-status-badge="data.status !== 'approved'"
|
||||||
@contextmenu.prevent.stop="handleRightClick"
|
@contextmenu.prevent.stop="handleRightClick"
|
||||||
|
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
|
||||||
>
|
>
|
||||||
<template v-if="isServerProject" #actions>
|
<template #actions>
|
||||||
<ButtonStyled v-if="serverPlaying" size="large" color="red">
|
<template v-if="isServerProject">
|
||||||
<button @click="handleStopServer">
|
<ButtonStyled v-if="serverPlaying" color="red" size="large">
|
||||||
<StopCircleIcon />
|
<button type="button" @click="handleStopServer">
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
<StopCircleIcon />
|
||||||
</button>
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
</ButtonStyled>
|
</button>
|
||||||
<ButtonStyled v-else size="large" color="brand">
|
</ButtonStyled>
|
||||||
<button
|
<ButtonStyled v-else color="brand" size="large">
|
||||||
:disabled="data && installingServerProjects.includes(data.id)"
|
<button type="button" :disabled="serverInstallLoading" @click="handleClickPlay">
|
||||||
@click="handleClickPlay"
|
<PlayIcon />
|
||||||
>
|
{{
|
||||||
<PlayIcon />
|
serverInstallLoading
|
||||||
{{
|
? formatMessage(commonMessages.installingLabel)
|
||||||
data && installingServerProjects.includes(data.id)
|
: formatMessage(commonMessages.playButton)
|
||||||
? formatMessage(commonMessages.installingLabel)
|
}}
|
||||||
: formatMessage(commonMessages.playButton)
|
</button>
|
||||||
}}
|
</ButtonStyled>
|
||||||
</button>
|
<ButtonStyled circular size="large">
|
||||||
</ButtonStyled>
|
<button
|
||||||
<ButtonStyled size="large" circular>
|
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||||
<button
|
type="button"
|
||||||
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
:aria-label="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||||
@click="handleAddServerToInstance"
|
@click="handleAddServerToInstance"
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled size="large" circular type="transparent">
|
<ButtonStyled circular size="large" type="transparent">
|
||||||
<OverflowMenu
|
<TeleportOverflowMenu
|
||||||
:tooltip="`More options`"
|
:options="serverProjectHeaderMoreActions"
|
||||||
:options="[
|
tooltip="More options"
|
||||||
{
|
aria-label="More options"
|
||||||
id: 'open-in-browser',
|
>
|
||||||
link: `https://modrinth.com/project/${data.slug}`,
|
<MoreVerticalIcon />
|
||||||
external: true,
|
</TeleportOverflowMenu>
|
||||||
},
|
</ButtonStyled>
|
||||||
{
|
</template>
|
||||||
divider: true,
|
<template v-else>
|
||||||
},
|
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
|
||||||
{
|
<button v-tooltip="formatMessage(messages.alreadyInstalled)" type="button" disabled>
|
||||||
id: 'report',
|
<CheckIcon />
|
||||||
color: 'red',
|
{{ formatMessage(commonMessages.installedLabel) }}
|
||||||
hoverFilled: true,
|
</button>
|
||||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
</ButtonStyled>
|
||||||
},
|
<ButtonStyled v-else-if="showSwitchVersion" size="large">
|
||||||
]"
|
<button type="button" @click="goToVersions">
|
||||||
aria-label="More options"
|
<SwapIcon />
|
||||||
>
|
{{ formatMessage(messages.switchVersion) }}
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
</button>
|
||||||
<template #open-in-browser>
|
</ButtonStyled>
|
||||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
<ButtonStyled v-else color="brand" size="large">
|
||||||
</template>
|
<button
|
||||||
<template #report> <ReportIcon /> Report </template>
|
v-tooltip="
|
||||||
</OverflowMenu>
|
installButtonInstalled ? formatMessage(messages.alreadyInstalled) : undefined
|
||||||
</ButtonStyled>
|
"
|
||||||
|
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>
|
||||||
<template v-else #actions>
|
</ProjectPageHeader>
|
||||||
<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>
|
|
||||||
<NavTabs
|
<NavTabs
|
||||||
:links="[
|
:links="[
|
||||||
{
|
{
|
||||||
@@ -289,9 +247,8 @@ import {
|
|||||||
getTargetInstallPreferences,
|
getTargetInstallPreferences,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
NavTabs,
|
NavTabs,
|
||||||
OverflowMenu,
|
|
||||||
ProjectBackgroundGradient,
|
ProjectBackgroundGradient,
|
||||||
ProjectHeader,
|
ProjectPageHeader,
|
||||||
ProjectSidebarCompatibility,
|
ProjectSidebarCompatibility,
|
||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
@@ -300,6 +257,7 @@ import {
|
|||||||
ProjectSidebarTags,
|
ProjectSidebarTags,
|
||||||
requestInstall,
|
requestInstall,
|
||||||
SelectedProjectsFloatingBar,
|
SelectedProjectsFloatingBar,
|
||||||
|
TeleportOverflowMenu,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
@@ -358,14 +316,14 @@ const messages = defineMessages({
|
|||||||
id: 'app.project.install-context.back-to-browse',
|
id: 'app.project.install-context.back-to-browse',
|
||||||
defaultMessage: 'Back to discover',
|
defaultMessage: 'Back to discover',
|
||||||
},
|
},
|
||||||
installContentToInstance: {
|
|
||||||
id: 'app.project.install-context.install-content-to-instance',
|
|
||||||
defaultMessage: 'Install content to instance',
|
|
||||||
},
|
|
||||||
alreadyInstalled: {
|
alreadyInstalled: {
|
||||||
id: 'app.project.install-button.already-installed',
|
id: 'app.project.install-button.already-installed',
|
||||||
defaultMessage: 'This project is already installed',
|
defaultMessage: 'This project is already installed',
|
||||||
},
|
},
|
||||||
|
installContentToInstance: {
|
||||||
|
id: 'app.project.install-context.install-content-to-instance',
|
||||||
|
defaultMessage: 'Install content to instance',
|
||||||
|
},
|
||||||
switchVersion: {
|
switchVersion: {
|
||||||
id: 'app.project.install-button.switch-version',
|
id: 'app.project.install-button.switch-version',
|
||||||
defaultMessage: 'Switch version',
|
defaultMessage: 'Switch version',
|
||||||
@@ -529,17 +487,72 @@ const installButtonInstalled = computed(() =>
|
|||||||
const installButtonDisabled = computed(
|
const installButtonDisabled = computed(
|
||||||
() => installButtonInstalled.value || installButtonLoading.value,
|
() => installButtonInstalled.value || installButtonLoading.value,
|
||||||
)
|
)
|
||||||
const installButtonLabel = computed(() => {
|
const serverInstallLoading = computed(
|
||||||
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
|
() => !!data.value && installingServerProjects.value.includes(data.value.id),
|
||||||
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
|
)
|
||||||
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
|
const installButtonIcon = computed(() => {
|
||||||
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
|
if (installButtonLoading.value && !installButtonInstalled.value) return SpinnerIcon
|
||||||
return formatMessage(commonMessages.installButton)
|
if (!installButtonInstalled.value && !serverProjectSelected.value) return DownloadIcon
|
||||||
})
|
return CheckIcon
|
||||||
const installButtonTooltip = computed(() => {
|
|
||||||
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
|
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
|
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 showSwitchVersion = computed(() => !!instance.value && installed.value)
|
||||||
const onVersionsPage = computed(() => route.name === 'Versions')
|
const onVersionsPage = computed(() => route.name === 'Versions')
|
||||||
@@ -585,6 +598,17 @@ function handleAddServerToInstance() {
|
|||||||
showAddServerToInstanceModal(data.value.title, address)
|
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() {
|
async function fetchProjectData() {
|
||||||
const [project, projectV3Result] = await Promise.all([
|
const [project, projectV3Result] = await Promise.all([
|
||||||
get_project(route.params.id, 'must_revalidate').catch(handleError),
|
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": {
|
"profile.label.collection": {
|
||||||
"message": "Kolekce"
|
"message": "Kolekce"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {stažení} few {stažení} other {stažení}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Členem od"
|
"message": "Členem od"
|
||||||
},
|
},
|
||||||
@@ -2192,9 +2189,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizace"
|
"message": "Organizace"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektů}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Ukládání..."
|
"message": "Ukládání..."
|
||||||
},
|
},
|
||||||
@@ -2528,12 +2522,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Přejít na přehled projektů"
|
"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": {
|
"project.versions.title": {
|
||||||
"message": "Verze"
|
"message": "Verze"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3068,9 +3068,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Kollektion"
|
"message": "Kollektion"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Beigetreten"
|
"message": "Beigetreten"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisationen"
|
"message": "Organisationen"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Speichert..."
|
"message": "Speichert..."
|
||||||
},
|
},
|
||||||
@@ -3656,12 +3650,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Projekt-Dashboard besuchen"
|
"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": {
|
"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."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Kollektion"
|
"message": "Kollektion"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Beigetreten"
|
"message": "Beigetreten"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisationen"
|
"message": "Organisationen"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Speichert..."
|
"message": "Speichert..."
|
||||||
},
|
},
|
||||||
@@ -3656,12 +3650,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Projekt-Dashboard besuchen"
|
"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": {
|
"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."
|
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheidet sich das Projekt zu dearchivieren."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3005,6 +3005,24 @@
|
|||||||
"muralpay.warning.wallet-address": {
|
"muralpay.warning.wallet-address": {
|
||||||
"message": "Double-check your wallet address. Funds sent to an incorrect address cannot be recovered."
|
"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": {
|
"organization.settings.projects.edit-links.affected-projects": {
|
||||||
"message": "Changes will be applied to {count, plural, one {# project} other {# projects}}."
|
"message": "Changes will be applied to {count, plural, one {# project} other {# projects}}."
|
||||||
},
|
},
|
||||||
@@ -3074,8 +3092,8 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Collection"
|
"message": "Collection"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
"profile.label.download-count": {
|
||||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
"message": "{count, plural, one {download} other {downloads}}"
|
||||||
},
|
},
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Joined"
|
"message": "Joined"
|
||||||
@@ -3095,8 +3113,8 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizations"
|
"message": "Organizations"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
"profile.label.project-count": {
|
||||||
"message": "{count} {countPlural, plural, one {project} other {projects}}"
|
"message": "{count, plural, one {project} other {projects}}"
|
||||||
},
|
},
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Saving..."
|
"message": "Saving..."
|
||||||
@@ -3260,6 +3278,12 @@
|
|||||||
"project.actions.dont-show-again": {
|
"project.actions.dont-show-again": {
|
||||||
"message": "Don't 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": {
|
"project.actions.review-project": {
|
||||||
"message": "Review project"
|
"message": "Review project"
|
||||||
},
|
},
|
||||||
@@ -3662,12 +3686,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Visit projects 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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Colección"
|
"message": "Colección"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Se unió"
|
"message": "Se unió"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizaciones"
|
"message": "Organizaciones"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Guardando..."
|
"message": "Guardando..."
|
||||||
},
|
},
|
||||||
@@ -3656,12 +3650,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Ver el panel de control de proyectos"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} se ha archivado. {title} no recibirá más actualizaciones hasta que el autor decida desarchivar el proyecto."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Colección"
|
"message": "Colección"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Se unió hace"
|
"message": "Se unió hace"
|
||||||
},
|
},
|
||||||
@@ -2912,9 +2909,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizaciones"
|
"message": "Organizaciones"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Guardando..."
|
"message": "Guardando..."
|
||||||
},
|
},
|
||||||
@@ -3377,12 +3371,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Visita el panel de proyectos"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} ha sido archivado. {title} no recibirá ninguna futura actualización excepto que el autor decida desarchivar el proyecto."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Koleksiyon"
|
"message": "Koleksiyon"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {download} other {na download}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Sumali"
|
"message": "Sumali"
|
||||||
},
|
},
|
||||||
@@ -2234,9 +2231,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Mga organisasyon"
|
"message": "Mga organisasyon"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {proyekto} other {na proyekto}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Sine-save..."
|
"message": "Sine-save..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3074,9 +3074,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Collection"
|
"message": "Collection"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {téléchargement} other {téléchargements}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Rejoint"
|
"message": "Rejoint"
|
||||||
},
|
},
|
||||||
@@ -3095,9 +3092,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisations"
|
"message": "Organisations"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {projet} other {projets}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Sauvegarde en cours..."
|
"message": "Sauvegarde en cours..."
|
||||||
},
|
},
|
||||||
@@ -3662,12 +3656,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Visiter le tableau de contrôle des projets"
|
"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": {
|
"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."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "אוסף"
|
"message": "אוסף"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "הצטרף ב-"
|
"message": "הצטרף ב-"
|
||||||
},
|
},
|
||||||
@@ -1859,9 +1856,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "ארגונים"
|
"message": "ארגונים"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {project} other {projects}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "שומר..."
|
"message": "שומר..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2966,9 +2966,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Gyűjtemény"
|
"message": "Gyűjtemény"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {letöltés} other {letöltések}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Csatlakozott"
|
"message": "Csatlakozott"
|
||||||
},
|
},
|
||||||
@@ -2987,9 +2984,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Szervezetek"
|
"message": "Szervezetek"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {projekt} other {projektek}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Mentés..."
|
"message": "Mentés..."
|
||||||
},
|
},
|
||||||
@@ -3476,12 +3470,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Projektek irányítópult megnyitása"
|
"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": {
|
"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."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Koleksi"
|
"message": "Koleksi"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, other {pengunduhan}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Telah bergabung"
|
"message": "Telah bergabung"
|
||||||
},
|
},
|
||||||
@@ -2240,9 +2237,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisasi"
|
"message": "Organisasi"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, other {proyek}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Menyimpan..."
|
"message": "Menyimpan..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3059,9 +3059,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Raccolta"
|
"message": "Raccolta"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} download"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Iscrizione"
|
"message": "Iscrizione"
|
||||||
},
|
},
|
||||||
@@ -3080,9 +3077,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizzazioni"
|
"message": "Organizzazioni"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {progetto} other {progetti}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Salvando..."
|
"message": "Salvando..."
|
||||||
},
|
},
|
||||||
@@ -3647,12 +3641,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Visita bacheca del progetto"
|
"message": "Visita bacheca del progetto"
|
||||||
},
|
},
|
||||||
"project.stats.downloads-label": {
|
|
||||||
"message": "{count} download"
|
|
||||||
},
|
|
||||||
"project.stats.followers-label": {
|
|
||||||
"message": "{count} follower"
|
|
||||||
},
|
|
||||||
"project.status.archived.message": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} è stato archiviato. {title} non riceverà più aggiornamenti a meno che l'autore decida di rimuoverlo dall'archivio."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "コレクション"
|
"message": "コレクション"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count}件のダウンロード"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "参加: "
|
"message": "参加: "
|
||||||
},
|
},
|
||||||
@@ -2618,9 +2615,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "組織"
|
"message": "組織"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {プロジェクト} other {プロジェクト}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "保存中…"
|
"message": "保存中…"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3068,9 +3068,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "컬렉션"
|
"message": "컬렉션"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {다운로드} other {다운로드}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "가입"
|
"message": "가입"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "조직"
|
"message": "조직"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {프로젝트} other {프로젝트}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "저장..."
|
"message": "저장..."
|
||||||
},
|
},
|
||||||
@@ -3656,12 +3650,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "프로젝트 대시보드 방문"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} 은(는) 보관되었습니다. 작성자가 프로젝트 보관 해제를 결정하지 않는 한 더 이상 업데이트가 제공되지 않습니다."
|
"message": "{title} 은(는) 보관되었습니다. 작성자가 프로젝트 보관 해제를 결정하지 않는 한 더 이상 업데이트가 제공되지 않습니다."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2750,9 +2750,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Koleksi"
|
"message": "Koleksi"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, other {muat turun}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Telah menyertai"
|
"message": "Telah menyertai"
|
||||||
},
|
},
|
||||||
@@ -2771,9 +2768,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisasi"
|
"message": "Organisasi"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, other {projek}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Sedang menyimpan..."
|
"message": "Sedang menyimpan..."
|
||||||
},
|
},
|
||||||
@@ -3188,9 +3182,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Kunjungi papan pemuka projek"
|
"message": "Kunjungi papan pemuka projek"
|
||||||
},
|
},
|
||||||
"project.stats.downloads-label": {
|
|
||||||
"message": "{count, plural, other {muat turun}}"
|
|
||||||
},
|
|
||||||
"project.status.archived.message": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} telah diarkibkan. {title} tidak akan menerima sebarang kemas kini lanjut melainkan pengarang memutuskan untuk menyaharkibkan projek."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Collectie"
|
"message": "Collectie"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Lid geworden"
|
"message": "Lid geworden"
|
||||||
},
|
},
|
||||||
@@ -3086,9 +3083,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisaties"
|
"message": "Organisaties"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {project} other {projecten}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Opslaan..."
|
"message": "Opslaan..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2678,9 +2678,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Samling"
|
"message": "Samling"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {nedlasting} other {nedlastinger}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Blei med"
|
"message": "Blei med"
|
||||||
},
|
},
|
||||||
@@ -2699,9 +2696,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organisasjoner"
|
"message": "Organisasjoner"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {projekt} other {projekt}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Lagrer..."
|
"message": "Lagrer..."
|
||||||
},
|
},
|
||||||
@@ -3038,12 +3032,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Besøk prosjekt-dashbordet"
|
"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": {
|
"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."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Kolekcja"
|
"message": "Kolekcja"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {pobranie} few {pobrania} other {pobrań}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Dołączył(-a)"
|
"message": "Dołączył(-a)"
|
||||||
},
|
},
|
||||||
@@ -3083,9 +3080,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizacje"
|
"message": "Organizacje"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektów}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Zapisywanie..."
|
"message": "Zapisywanie..."
|
||||||
},
|
},
|
||||||
@@ -3647,12 +3641,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Odwiedź pulpit projektów"
|
"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": {
|
"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."
|
"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": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Visitar painel de projetos"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} foi arquivado. {title} não receberá atualizações a menos que o autor decida desarquivar o projeto."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Coleção"
|
"message": "Coleção"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {transferência} other {transferências}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Entrou"
|
"message": "Entrou"
|
||||||
},
|
},
|
||||||
@@ -2582,9 +2579,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizações"
|
"message": "Organizações"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural,one {projeto} other {projetos}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "A guardar..."
|
"message": "A guardar..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3065,9 +3065,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Коллекция"
|
"message": "Коллекция"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {скачивание} few {скачивания} other {скачиваний}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Регистрация:"
|
"message": "Регистрация:"
|
||||||
},
|
},
|
||||||
@@ -3086,9 +3083,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Организации"
|
"message": "Организации"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {проект} few {проекта} other {проектов}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Сохранение..."
|
"message": "Сохранение..."
|
||||||
},
|
},
|
||||||
@@ -3653,12 +3647,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Управление проектами"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} помещён в архив. {title} больше не будет получать обновления, если только автор не решит разархивировать проект."
|
"message": "{title} помещён в архив. {title} больше не будет получать обновления, если только автор не решит разархивировать проект."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2738,9 +2738,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Samling"
|
"message": "Samling"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {nedladdning} other {nedladdningar}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Gick med"
|
"message": "Gick med"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3068,9 +3068,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Koleksiyon"
|
"message": "Koleksiyon"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {indirme} other {indirmeler}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Katılma:"
|
"message": "Katılma:"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Organizasyonlar"
|
"message": "Organizasyonlar"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {proje} other {projeler}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Kaydediliyor..."
|
"message": "Kaydediliyor..."
|
||||||
},
|
},
|
||||||
@@ -3602,12 +3596,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Projeler panelini ziyaret et"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} arşivlenmiş. {title}, yapımcı fikrini değiştirmediği sürece daha fazla güncelleme almayacak."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "Добірка"
|
"message": "Добірка"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, one {завантаження} few {завантаження} many {завантажень} other {завантажень}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "Приєднався"
|
"message": "Приєднався"
|
||||||
},
|
},
|
||||||
@@ -3092,9 +3089,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Організації"
|
"message": "Організації"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {проєкт} few {проєкти} many {проєктів} other {проєктів}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Збереження…"
|
"message": "Збереження…"
|
||||||
},
|
},
|
||||||
@@ -3659,12 +3653,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Панель керування проєктами"
|
"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": {
|
"project.status.archived.message": {
|
||||||
"message": "«{title}» було архівовано. «{title}» не отримуватиме подальших оновлень допоки автор не вирішить розархівувати проєкт."
|
"message": "«{title}» було архівовано. «{title}» не отримуватиме подальших оновлень допоки автор не вирішить розархівувати проєкт."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2861,9 +2861,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "Bộ sưu tập"
|
"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": {
|
"profile.label.joined": {
|
||||||
"message": "Đã tham gia"
|
"message": "Đã tham gia"
|
||||||
},
|
},
|
||||||
@@ -2882,9 +2879,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "Tổ chức"
|
"message": "Tổ chức"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, one {dự án} other {dự án}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "Đang lưu..."
|
"message": "Đang lưu..."
|
||||||
},
|
},
|
||||||
@@ -3284,12 +3278,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "Truy cập bảng điều khiển dự án"
|
"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": {
|
"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."
|
"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": {
|
"profile.label.collection": {
|
||||||
"message": "收藏夹"
|
"message": "收藏夹"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "{count} {countPlural, plural, other {次下载}}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "加入于"
|
"message": "加入于"
|
||||||
},
|
},
|
||||||
@@ -3089,9 +3086,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "组织"
|
"message": "组织"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} {countPlural, plural, other {个项目}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "正在保存…"
|
"message": "正在保存…"
|
||||||
},
|
},
|
||||||
@@ -3656,12 +3650,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "访问项目的控制面板"
|
"message": "访问项目的控制面板"
|
||||||
},
|
},
|
||||||
"project.stats.downloads-label": {
|
|
||||||
"message": "{count, plural, other {下载}}"
|
|
||||||
},
|
|
||||||
"project.stats.followers-label": {
|
|
||||||
"message": "{count, plural, other {关注者}}"
|
|
||||||
},
|
|
||||||
"project.status.archived.message": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} 已归档。除非作者决定取消归档,否则 {title} 将不再更新。"
|
"message": "{title} 已归档。除非作者决定取消归档,否则 {title} 将不再更新。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3074,9 +3074,6 @@
|
|||||||
"profile.label.collection": {
|
"profile.label.collection": {
|
||||||
"message": "收藏"
|
"message": "收藏"
|
||||||
},
|
},
|
||||||
"profile.label.downloads": {
|
|
||||||
"message": "下載次數:{count}"
|
|
||||||
},
|
|
||||||
"profile.label.joined": {
|
"profile.label.joined": {
|
||||||
"message": "加入時間:"
|
"message": "加入時間:"
|
||||||
},
|
},
|
||||||
@@ -3095,9 +3092,6 @@
|
|||||||
"profile.label.organizations": {
|
"profile.label.organizations": {
|
||||||
"message": "組織"
|
"message": "組織"
|
||||||
},
|
},
|
||||||
"profile.label.projects": {
|
|
||||||
"message": "{count} 個{countPlural, plural, other {專案}}"
|
|
||||||
},
|
|
||||||
"profile.label.saving": {
|
"profile.label.saving": {
|
||||||
"message": "儲存中..."
|
"message": "儲存中..."
|
||||||
},
|
},
|
||||||
@@ -3662,12 +3656,6 @@
|
|||||||
"project.settings.visit-dashboard": {
|
"project.settings.visit-dashboard": {
|
||||||
"message": "前往專案資訊主頁"
|
"message": "前往專案資訊主頁"
|
||||||
},
|
},
|
||||||
"project.stats.downloads-label": {
|
|
||||||
"message": "{count, plural, other {下載}}"
|
|
||||||
},
|
|
||||||
"project.stats.followers-label": {
|
|
||||||
"message": "{count, plural, other {追蹤者}}"
|
|
||||||
},
|
|
||||||
"project.status.archived.message": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} 已封存。{title} 將不會再收到任何後續更新,除非作者決定解除封存該專案。"
|
"message": "{title} 已封存。{title} 將不會再收到任何後續更新,除非作者決定解除封存該專案。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,29 +130,27 @@
|
|||||||
@set-processing="setProcessing"
|
@set-processing="setProcessing"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ProjectHeader
|
<ProjectPageHeader
|
||||||
v-if="projectV3Loaded"
|
v-if="projectV3Loaded"
|
||||||
:project="project"
|
:project="project"
|
||||||
:project-v3="projectV3"
|
:project-v3="projectV3"
|
||||||
:member="!!currentMember"
|
:show-status-badge="!!currentMember || project.status !== 'approved'"
|
||||||
|
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand" circular>
|
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand" circular>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-tooltip="'Edit project'"
|
v-tooltip="formatMessage(messages.editProject)"
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
:to="`${projectPath}/settings`"
|
||||||
class="!font-bold lg:!hidden"
|
class="!font-bold lg:!hidden"
|
||||||
>
|
>
|
||||||
<SettingsIcon aria-hidden="true" />
|
<SettingsIcon />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
|
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
|
||||||
<nuxt-link
|
<nuxt-link :to="`${projectPath}/settings`" class="!font-bold max-lg:!hidden">
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
<SettingsIcon />
|
||||||
class="!font-bold max-lg:!hidden"
|
{{ formatMessage(messages.editProject) }}
|
||||||
>
|
|
||||||
<SettingsIcon aria-hidden="true" />
|
|
||||||
Edit project
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
@@ -160,20 +158,20 @@
|
|||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="!isServerProject"
|
v-if="!isServerProject"
|
||||||
size="large"
|
size="large"
|
||||||
:color="
|
:color="projectHeaderPrimaryColor"
|
||||||
(auth.user && currentMember) || route.name === 'type-project-version-version'
|
|
||||||
? `standard`
|
|
||||||
: `brand`
|
|
||||||
"
|
|
||||||
:circular="!!auth.user && !!currentMember"
|
:circular="!!auth.user && !!currentMember"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-tooltip="
|
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)
|
auth.user && currentMember ? '' : formatMessage(commonMessages.downloadButton)
|
||||||
}}
|
}}
|
||||||
@@ -182,19 +180,21 @@
|
|||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-else
|
v-else
|
||||||
size="large"
|
size="large"
|
||||||
:color="
|
:color="projectHeaderPrimaryColor"
|
||||||
(auth.user && currentMember) || route.name === 'type-project-version-version'
|
|
||||||
? `standard`
|
|
||||||
: `brand`
|
|
||||||
"
|
|
||||||
:circular="!!auth.user && !!currentMember"
|
:circular="!!auth.user && !!currentMember"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-tooltip="auth.user && currentMember && !openInAppModal?.open ? 'Play' : ''"
|
v-tooltip="
|
||||||
@click="handlePlayServerProject"
|
auth.user && currentMember
|
||||||
|
? formatMessage(commonMessages.playButton)
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
:aria-label="formatMessage(commonMessages.playButton)"
|
||||||
|
@click="handleProjectHeaderPrimary"
|
||||||
>
|
>
|
||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon />
|
||||||
{{ auth.user && currentMember ? '' : 'Play' }}
|
{{ auth.user && currentMember ? '' : formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,109 +204,100 @@
|
|||||||
v-if="!isServerProject"
|
v-if="!isServerProject"
|
||||||
size="large"
|
size="large"
|
||||||
circular
|
circular
|
||||||
:color="
|
:color="projectHeaderPrimaryColor"
|
||||||
route.name === 'type-project-version-version' || (auth.user && currentMember)
|
|
||||||
? `standard`
|
|
||||||
: `brand`
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
:aria-label="formatMessage(commonMessages.downloadButton)"
|
:aria-label="formatMessage(commonMessages.downloadButton)"
|
||||||
class="flex sm:hidden"
|
class="flex sm:hidden"
|
||||||
@click="(event) => downloadModal.show(event)"
|
@click="handleProjectHeaderPrimary"
|
||||||
>
|
>
|
||||||
<DownloadIcon aria-hidden="true" />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled
|
<ButtonStyled v-else size="large" circular :color="projectHeaderPrimaryColor">
|
||||||
v-else
|
<button
|
||||||
size="large"
|
type="button"
|
||||||
circular
|
:aria-label="formatMessage(commonMessages.playButton)"
|
||||||
:color="
|
class="flex sm:hidden"
|
||||||
route.name === 'type-project-version-version' || (auth.user && currentMember)
|
@click="handleProjectHeaderPrimary"
|
||||||
? `standard`
|
>
|
||||||
: `brand`
|
<PlayIcon />
|
||||||
"
|
|
||||||
>
|
|
||||||
<button aria-label="Play" class="flex sm:hidden" @click="handlePlayServerProject">
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
|
v-if="
|
||||||
|
showProjectHeaderCreateServerAction && flags.showProjectPageCreateServersTooltip
|
||||||
|
"
|
||||||
theme="dismissable-prompt"
|
theme="dismissable-prompt"
|
||||||
|
class="inline-flex"
|
||||||
:triggers="[]"
|
:triggers="[]"
|
||||||
:shown="flags.showProjectPageCreateServersTooltip"
|
:shown="flags.showProjectPageCreateServersTooltip"
|
||||||
:auto-hide="false"
|
:auto-hide="false"
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
<ButtonStyled size="large" circular>
|
<ButtonStyled circular size="large">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-tooltip="formatMessage(messages.createServerTooltip)"
|
v-tooltip="formatMessage(messages.createServerTooltip)"
|
||||||
:to="`/hosting?project=${project.id}#plan`"
|
:to="projectHeaderCreateServerTo"
|
||||||
@click="
|
:aria-label="formatMessage(messages.serversPromoTitle)"
|
||||||
() => {
|
@click="dismissProjectHeaderCreateServerPrompt"
|
||||||
flags.showProjectPageCreateServersTooltip = false
|
|
||||||
saveFeatureFlags()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<ServerPlusIcon aria-hidden="true" />
|
<ServerPlusIcon />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="grid grid-cols-[min-content] gap-1">
|
<div class="grid max-w-[18rem] gap-2">
|
||||||
<div class="flex min-w-60 items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<h3
|
<div class="flex items-center gap-2">
|
||||||
class="m-0 flex items-center gap-2 whitespace-nowrap text-base font-bold text-contrast"
|
<h3 class="m-0 text-base font-bold text-contrast">
|
||||||
>
|
{{ formatMessage(messages.serversPromoTitle) }}
|
||||||
{{ formatMessage(messages.serversPromoTitle) }}
|
</h3>
|
||||||
<TagItem
|
<span
|
||||||
:style="{
|
class="rounded-full bg-brand-highlight px-2 py-0.5 text-xs font-bold text-brand"
|
||||||
'--_color': 'var(--color-brand)',
|
|
||||||
'--_bg-color': 'var(--color-brand-highlight)',
|
|
||||||
}"
|
|
||||||
>{{ formatMessage(commonMessages.newBadge) }}</TagItem
|
|
||||||
>
|
>
|
||||||
</h3>
|
{{ formatMessage(commonMessages.newBadge) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<ButtonStyled size="small" circular>
|
<ButtonStyled size="small" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="formatMessage(messages.dontShowAgain)"
|
v-tooltip="formatMessage(messages.dontShowAgain)"
|
||||||
@click="
|
@click="dismissProjectHeaderCreateServerPrompt"
|
||||||
() => {
|
|
||||||
flags.showProjectPageCreateServersTooltip = false
|
|
||||||
saveFeatureFlags()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="m-0 text-sm font-medium leading-tight text-secondary">
|
||||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
|
||||||
{{ formatMessage(messages.serversPromoDescription) }}
|
{{ formatMessage(messages.serversPromoDescription) }}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="m-0 text-sm font-semibold text-contrast">
|
||||||
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
|
||||||
<IntlFormatted
|
<IntlFormatted
|
||||||
:message-id="messages.serversPromoPricing"
|
:message-id="messages.serversPromoPricing"
|
||||||
:values="{
|
:values="{ price: formatPrice(500, 'USD', true) }"
|
||||||
price: formatPrice(500, 'USD', true),
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #small="{ children }">
|
<template #small="{ children }">
|
||||||
<span class="text-xs">
|
<small><component :is="() => children" /></small>
|
||||||
<component :is="() => children" />
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</IntlFormatted>
|
</IntlFormatted>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Tooltip>
|
</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>
|
<ClientOnly>
|
||||||
<button
|
<button
|
||||||
v-if="auth.user"
|
v-if="auth.user"
|
||||||
@@ -315,14 +306,15 @@
|
|||||||
? formatMessage(commonMessages.unfollowButton)
|
? formatMessage(commonMessages.unfollowButton)
|
||||||
: formatMessage(commonMessages.followButton)
|
: formatMessage(commonMessages.followButton)
|
||||||
"
|
"
|
||||||
|
type="button"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
following
|
following
|
||||||
? formatMessage(commonMessages.unfollowButton)
|
? formatMessage(commonMessages.unfollowButton)
|
||||||
: formatMessage(commonMessages.followButton)
|
: formatMessage(commonMessages.followButton)
|
||||||
"
|
"
|
||||||
@click="userFollowProject(project)"
|
@click="followProjectFromHeader"
|
||||||
>
|
>
|
||||||
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
|
<HeartIcon :fill="following ? 'currentColor' : 'none'" />
|
||||||
</button>
|
</button>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-else
|
v-else
|
||||||
@@ -343,156 +335,31 @@
|
|||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</ButtonStyled>
|
</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">
|
<ProjectCollectionSaveButton
|
||||||
<p class="popout-text">{{ formatMessage(messages.noCollectionsFound) }}</p>
|
:auth-user="auth.user"
|
||||||
</div>
|
:sign-in-route="signInRouteObj"
|
||||||
<ButtonStyled>
|
:project-id="project.id"
|
||||||
<button
|
:collections="collections"
|
||||||
class="mx-3 mb-3"
|
:saved="collections.some((x) => x.projects.includes(project.id))"
|
||||||
@click="(event) => $refs.modal_collection.show(event)"
|
:base-id="baseId"
|
||||||
>
|
:no-collections-label="formatMessage(messages.noCollectionsFound)"
|
||||||
<PlusIcon aria-hidden="true" />
|
:create-new-collection-label="formatMessage(messages.createNewCollection)"
|
||||||
{{ formatMessage(messages.createNewCollection) }}
|
:collect-project="onUserCollectProject"
|
||||||
</button>
|
:create-collection="(event) => modalCollection?.show(event)"
|
||||||
</ButtonStyled>
|
/>
|
||||||
</template>
|
|
||||||
</PopoutMenu>
|
|
||||||
<nuxt-link v-else v-tooltip="'Save'" :to="signInRouteObj" aria-label="Save">
|
|
||||||
<BookmarkIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
|
|
||||||
<ButtonStyled size="large" circular type="transparent">
|
<ButtonStyled circular size="large" type="transparent">
|
||||||
<OverflowMenu
|
<TeleportOverflowMenu
|
||||||
|
:options="projectHeaderMoreActions"
|
||||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
: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)"
|
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||||
:dropdown-id="`${baseId}-more-options`"
|
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon />
|
||||||
<template #analytics>
|
</TeleportOverflowMenu>
|
||||||
<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>
|
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
</ProjectHeader>
|
</ProjectPageHeader>
|
||||||
<ProjectMemberHeader
|
<ProjectMemberHeader
|
||||||
v-if="currentMember"
|
v-if="currentMember"
|
||||||
:project="project"
|
:project="project"
|
||||||
@@ -726,7 +593,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
BookmarkIcon,
|
|
||||||
BookTextIcon,
|
BookTextIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
@@ -739,7 +605,6 @@ import {
|
|||||||
ListIcon,
|
ListIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
|
||||||
ReportIcon,
|
ReportIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
ScanEyeIcon,
|
ScanEyeIcon,
|
||||||
@@ -752,7 +617,6 @@ import {
|
|||||||
Admonition,
|
Admonition,
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Checkbox,
|
|
||||||
commonMessages,
|
commonMessages,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
@@ -761,12 +625,10 @@ import {
|
|||||||
NavTabs,
|
NavTabs,
|
||||||
NewModal,
|
NewModal,
|
||||||
OpenInAppModal,
|
OpenInAppModal,
|
||||||
OverflowMenu,
|
|
||||||
PopoutMenu,
|
|
||||||
PROJECT_DEP_MARKER_QUERY,
|
PROJECT_DEP_MARKER_QUERY,
|
||||||
ProjectBackgroundGradient,
|
ProjectBackgroundGradient,
|
||||||
ProjectEnvironmentModal,
|
ProjectEnvironmentModal,
|
||||||
ProjectHeader,
|
ProjectPageHeader,
|
||||||
ProjectSidebarCompatibility,
|
ProjectSidebarCompatibility,
|
||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
@@ -774,8 +636,7 @@ import {
|
|||||||
ProjectSidebarServerInfo,
|
ProjectSidebarServerInfo,
|
||||||
ProjectSidebarTags,
|
ProjectSidebarTags,
|
||||||
provideProjectPageContext,
|
provideProjectPageContext,
|
||||||
StyledInput,
|
TeleportOverflowMenu,
|
||||||
TagItem,
|
|
||||||
useDebugLogger,
|
useDebugLogger,
|
||||||
useFormatDateTime,
|
useFormatDateTime,
|
||||||
useFormatPrice,
|
useFormatPrice,
|
||||||
@@ -795,6 +656,7 @@ import MessageBanner from '~/components/ui/MessageBanner.vue'
|
|||||||
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
||||||
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
||||||
import ModpackScanModal from '~/components/ui/moderation/ModpackScanModal.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 ProjectDownloadModal from '~/components/ui/ProjectDownloadModal/index.vue'
|
||||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||||
@@ -851,11 +713,11 @@ const flags = useFeatureFlags()
|
|||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatPrice = useFormatPrice()
|
|
||||||
const formatDateTime = useFormatDateTime({
|
const formatDateTime = useFormatDateTime({
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
dateStyle: 'long',
|
dateStyle: 'long',
|
||||||
})
|
})
|
||||||
|
const formatPrice = useFormatPrice()
|
||||||
|
|
||||||
const debug = useDebugLogger('DownloadModal')
|
const debug = useDebugLogger('DownloadModal')
|
||||||
|
|
||||||
@@ -925,10 +787,6 @@ const messages = defineMessages({
|
|||||||
id: 'project.navigation.changelog',
|
id: 'project.navigation.changelog',
|
||||||
defaultMessage: 'Changelog',
|
defaultMessage: 'Changelog',
|
||||||
},
|
},
|
||||||
createNewCollection: {
|
|
||||||
id: 'project.collections.create-new',
|
|
||||||
defaultMessage: 'Create new collection',
|
|
||||||
},
|
|
||||||
createServer: {
|
createServer: {
|
||||||
id: 'project.actions.create-server',
|
id: 'project.actions.create-server',
|
||||||
defaultMessage: 'Create a server',
|
defaultMessage: 'Create a server',
|
||||||
@@ -937,6 +795,10 @@ const messages = defineMessages({
|
|||||||
id: 'project.actions.create-server-tooltip',
|
id: 'project.actions.create-server-tooltip',
|
||||||
defaultMessage: 'Create a server',
|
defaultMessage: 'Create a server',
|
||||||
},
|
},
|
||||||
|
createNewCollection: {
|
||||||
|
id: 'project.collections.create-new',
|
||||||
|
defaultMessage: 'Create new collection',
|
||||||
|
},
|
||||||
descriptionTab: {
|
descriptionTab: {
|
||||||
id: 'project.description.title',
|
id: 'project.description.title',
|
||||||
defaultMessage: 'Description',
|
defaultMessage: 'Description',
|
||||||
@@ -945,9 +807,9 @@ const messages = defineMessages({
|
|||||||
id: 'project.actions.dont-show-again',
|
id: 'project.actions.dont-show-again',
|
||||||
defaultMessage: "Don't show again",
|
defaultMessage: "Don't show again",
|
||||||
},
|
},
|
||||||
downloadsStat: {
|
editProject: {
|
||||||
id: 'project.stats.downloads-label',
|
id: 'project.actions.edit-project',
|
||||||
defaultMessage: '{count, plural, one {download} other {downloads}}',
|
defaultMessage: 'Edit project',
|
||||||
},
|
},
|
||||||
errorLoadingProject: {
|
errorLoadingProject: {
|
||||||
id: 'project.error.loading',
|
id: 'project.error.loading',
|
||||||
@@ -975,10 +837,6 @@ const messages = defineMessages({
|
|||||||
id: 'project.environment.migration.learn-more',
|
id: 'project.environment.migration.learn-more',
|
||||||
defaultMessage: 'Learn more about this change',
|
defaultMessage: 'Learn more about this change',
|
||||||
},
|
},
|
||||||
followersStat: {
|
|
||||||
id: 'project.stats.followers-label',
|
|
||||||
defaultMessage: '{count, plural, one {follower} other {followers}}',
|
|
||||||
},
|
|
||||||
galleryTab: {
|
galleryTab: {
|
||||||
id: 'project.gallery.title',
|
id: 'project.gallery.title',
|
||||||
defaultMessage: 'Gallery',
|
defaultMessage: 'Gallery',
|
||||||
@@ -1039,6 +897,10 @@ const messages = defineMessages({
|
|||||||
id: 'project.actions.review-project',
|
id: 'project.actions.review-project',
|
||||||
defaultMessage: 'Review project',
|
defaultMessage: 'Review project',
|
||||||
},
|
},
|
||||||
|
rescanModpack: {
|
||||||
|
id: 'project.actions.rescan-modpack',
|
||||||
|
defaultMessage: 'Rescan modpack',
|
||||||
|
},
|
||||||
serversPromoDescription: {
|
serversPromoDescription: {
|
||||||
id: 'project.actions.servers-promo.description',
|
id: 'project.actions.servers-promo.description',
|
||||||
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
|
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 modalLicense = ref(null)
|
||||||
|
const modalCollection = useTemplateRef('modal_collection')
|
||||||
const licenseText = ref('')
|
const licenseText = ref('')
|
||||||
|
|
||||||
const createdDate = computed(() =>
|
const createdDate = computed(() =>
|
||||||
@@ -1104,13 +967,8 @@ async function getLicenseData(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayCollectionsSearch = ref('')
|
|
||||||
const collections = computed(() =>
|
const collections = computed(() =>
|
||||||
user.value && user.value.collections
|
user.value && user.value.collections ? user.value.collections : [],
|
||||||
? user.value.collections.filter((x) =>
|
|
||||||
x.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -1844,6 +1702,89 @@ const canCreateServerFrom = computed(() => {
|
|||||||
return project.value.project_type === 'modpack' && project.value.server_side !== 'unsupported'
|
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 = () =>
|
const createCanonicalUrl = () =>
|
||||||
project.value ? `https://modrinth.com/project/${project.value.id}` : undefined
|
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)
|
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(
|
watch(
|
||||||
[versionsV3, _versionsV3Error],
|
[versionsV3, _versionsV3Error],
|
||||||
([data, error]) => {
|
([data, error]) => {
|
||||||
|
|||||||
@@ -62,86 +62,16 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="normal-page__header py-4">
|
<div class="normal-page__header py-4">
|
||||||
<ContentPageHeader>
|
<OrganizationPageHeader
|
||||||
<template #icon>
|
:organization="organization"
|
||||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
|
:members-count="acceptedMembers?.length || 0"
|
||||||
</template>
|
:projects-count="projects?.length || 0"
|
||||||
<template #title>
|
:downloads="sumDownloads"
|
||||||
{{ organization.name }}
|
:can-manage="!!(auth.user && currentMember)"
|
||||||
</template>
|
@manage-projects="router.push(`/organization/${organization.slug}/settings/projects`)"
|
||||||
<template #title-suffix>
|
@copy-id="copyId"
|
||||||
<div class="ml-1 flex items-center gap-2 font-semibold">
|
@copy-permalink="copyPermalink"
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__sidebar">
|
<div class="normal-page__sidebar">
|
||||||
<AdPlaceholder v-if="!auth.user" />
|
<AdPlaceholder v-if="!auth.user" />
|
||||||
@@ -293,11 +223,7 @@ import {
|
|||||||
BoxIcon,
|
BoxIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ClipboardCopyIcon,
|
|
||||||
CrownIcon,
|
CrownIcon,
|
||||||
DownloadIcon,
|
|
||||||
MoreVerticalIcon,
|
|
||||||
OrganizationIcon,
|
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
@@ -307,16 +233,13 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
ContentPageHeader,
|
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
NavTabs,
|
NavTabs,
|
||||||
OverflowMenu,
|
|
||||||
PROJECT_DEP_MARKER_QUERY,
|
PROJECT_DEP_MARKER_QUERY,
|
||||||
ProjectCard,
|
ProjectCard,
|
||||||
ProjectCardList,
|
ProjectCardList,
|
||||||
SidebarCard,
|
SidebarCard,
|
||||||
useCompactNumber,
|
useCompactNumber,
|
||||||
useFormatNumber,
|
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
|
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 AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||||
import NavStack from '~/components/ui/NavStack.vue'
|
import NavStack from '~/components/ui/NavStack.vue'
|
||||||
|
import OrganizationPageHeader from '~/components/ui/OrganizationPageHeader.vue'
|
||||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||||
import {
|
import {
|
||||||
OrganizationContext,
|
OrganizationContext,
|
||||||
@@ -342,7 +266,6 @@ type ProjectV3 = Labrinth.Projects.v3.Project & {
|
|||||||
const vintl = useVIntl()
|
const vintl = useVIntl()
|
||||||
const { formatMessage } = vintl
|
const { formatMessage } = vintl
|
||||||
|
|
||||||
const formatNumber = useFormatNumber()
|
|
||||||
const { formatCompactNumber } = useCompactNumber()
|
const { formatCompactNumber } = useCompactNumber()
|
||||||
|
|
||||||
const auth: { user: any } & any = await useAuth()
|
const auth: { user: any } & any = await useAuth()
|
||||||
|
|||||||
@@ -120,35 +120,34 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||||
<div class="normal-page__header py-4">
|
<div class="normal-page__header py-4">
|
||||||
<ContentPageHeader>
|
<UserPageHeader
|
||||||
<template #icon>
|
:user="user"
|
||||||
<Avatar
|
:summary="isModrinthUser ? null : profileHeaderSummary"
|
||||||
:src="user.avatar_url"
|
:auth-user="auth.user"
|
||||||
:alt="user.username"
|
:is-modrinth-user="isModrinthUser"
|
||||||
:size="isModrinthUser ? '64px' : '96px'"
|
:is-official-account="isOfficialAccount"
|
||||||
circle
|
:show-affiliate-badge="isAdminViewing && !!isAffiliate"
|
||||||
/>
|
:is-affiliate="!!isAffiliate"
|
||||||
</template>
|
:is-self="auth.user?.id === user.id"
|
||||||
<template #title>
|
:is-admin="isAdminViewing"
|
||||||
<span class="flex items-center gap-2">
|
:is-staff="!!(auth.user && isStaff(auth.user))"
|
||||||
{{ user.username }}
|
:projects-count="projects?.length || 0"
|
||||||
<BadgeCheckIcon
|
:downloads="sumDownloads"
|
||||||
v-if="isOfficialAccount"
|
@manage-projects="navigateTo('/dashboard/projects')"
|
||||||
v-tooltip="formatMessage(messages.officialAccount)"
|
@report="reportProfileFromHeader"
|
||||||
class="size-5 text-brand"
|
@copy-id="copyId"
|
||||||
fill="var(--color-brand-highlight)"
|
@copy-permalink="copyPermalink"
|
||||||
/>
|
@open-billing="navigateTo(`/admin/billing/${user.id}`)"
|
||||||
<TagItem
|
@toggle-affiliate="toggleAffiliate(user.id)"
|
||||||
v-if="isAdminViewing && isAffiliate"
|
@open-info="userDetailsModal?.show()"
|
||||||
:style="{
|
@open-analytics="
|
||||||
'--_color': 'var(--color-brand)',
|
navigateTo({
|
||||||
'--_bg-color': 'var(--color-brand-highlight)',
|
path: '/dashboard/analytics',
|
||||||
}"
|
query: { user: user.username || user.id },
|
||||||
>
|
})
|
||||||
<AffiliateIcon /> Affiliate
|
"
|
||||||
</TagItem>
|
@edit-role="openRoleEditModal"
|
||||||
</span>
|
>
|
||||||
</template>
|
|
||||||
<template v-if="isModrinthUser" #summary>
|
<template v-if="isModrinthUser" #summary>
|
||||||
<IntlFormatted :message-id="messages.officialAccountBio">
|
<IntlFormatted :message-id="messages.officialAccountBio">
|
||||||
<template #support-link>
|
<template #support-link>
|
||||||
@@ -173,159 +172,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</IntlFormatted>
|
</IntlFormatted>
|
||||||
</template>
|
</template>
|
||||||
<template v-else #summary>
|
</UserPageHeader>
|
||||||
{{
|
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||||
@@ -509,23 +356,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
AffiliateIcon,
|
|
||||||
BadgeCheckIcon,
|
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
CalendarIcon,
|
|
||||||
ChartIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ClipboardCopyIcon,
|
|
||||||
CurrencyIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
EditIcon,
|
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
InfoIcon,
|
|
||||||
LibraryIcon,
|
LibraryIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
MoreVerticalIcon,
|
|
||||||
ReportIcon,
|
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
@@ -535,22 +371,15 @@ import {
|
|||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Combobox,
|
Combobox,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
ContentPageHeader,
|
|
||||||
defineMessages,
|
defineMessages,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
IntlFormatted,
|
IntlFormatted,
|
||||||
NavTabs,
|
NavTabs,
|
||||||
NewModal,
|
NewModal,
|
||||||
OverflowMenu,
|
|
||||||
ProjectCard,
|
ProjectCard,
|
||||||
ProjectCardList,
|
ProjectCardList,
|
||||||
TagItem,
|
|
||||||
useCompactNumber,
|
|
||||||
useFormatDateTime,
|
|
||||||
useFormatNumber,
|
|
||||||
UserBadges,
|
UserBadges,
|
||||||
useRelativeTime,
|
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
|
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 AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||||
|
import UserPageHeader from '~/components/ui/UserPageHeader.vue'
|
||||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||||
import { projectUserSorting } from '~/utils/projects.ts'
|
import { projectUserSorting } from '~/utils/projects.ts'
|
||||||
import { reportUser } from '~/utils/report-helpers.ts'
|
import { reportUser } from '~/utils/report-helpers.ts'
|
||||||
@@ -575,35 +405,14 @@ const config = useRuntimeConfig()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatNumber = useFormatNumber()
|
|
||||||
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
|
|
||||||
const formatRelativeTime = useRelativeTime()
|
|
||||||
const formatDateTime = useFormatDateTime({
|
|
||||||
timeStyle: 'short',
|
|
||||||
dateStyle: 'long',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const baseId = useId()
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
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: {
|
collectionProjectsCount: {
|
||||||
id: 'profile.collection.projects-count',
|
id: 'profile.collection.projects-count',
|
||||||
defaultMessage: '{count, plural, one {# project} other {# projects}}',
|
defaultMessage: '{count, plural, one {# project} other {# projects}}',
|
||||||
},
|
},
|
||||||
profileJoinedLabel: {
|
|
||||||
id: 'profile.label.joined',
|
|
||||||
defaultMessage: 'Joined',
|
|
||||||
},
|
|
||||||
savingLabel: {
|
savingLabel: {
|
||||||
id: 'profile.label.saving',
|
id: 'profile.label.saving',
|
||||||
defaultMessage: 'Saving...',
|
defaultMessage: 'Saving...',
|
||||||
@@ -669,10 +478,6 @@ const messages = defineMessages({
|
|||||||
id: 'profile.label.badges',
|
id: 'profile.label.badges',
|
||||||
defaultMessage: 'Badges',
|
defaultMessage: 'Badges',
|
||||||
},
|
},
|
||||||
profileManageProjectsButton: {
|
|
||||||
id: 'profile.button.manage-projects',
|
|
||||||
defaultMessage: 'Manage projects',
|
|
||||||
},
|
|
||||||
profileMetaDescription: {
|
profileMetaDescription: {
|
||||||
id: 'profile.meta.description',
|
id: 'profile.meta.description',
|
||||||
defaultMessage: "Download {username}'s projects on Modrinth",
|
defaultMessage: "Download {username}'s projects on Modrinth",
|
||||||
@@ -699,42 +504,10 @@ const messages = defineMessages({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
|
"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: {
|
userNotFoundError: {
|
||||||
id: 'profile.error.not-found',
|
id: 'profile.error.not-found',
|
||||||
defaultMessage: 'User not found',
|
defaultMessage: 'User not found',
|
||||||
},
|
},
|
||||||
officialAccount: {
|
|
||||||
id: 'profile.official-account',
|
|
||||||
defaultMessage: 'Official Modrinth account',
|
|
||||||
},
|
|
||||||
officialAccountBio: {
|
officialAccountBio: {
|
||||||
id: 'profile.official-account.bio',
|
id: 'profile.official-account.bio',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
@@ -891,14 +664,32 @@ async function copyPermalink() {
|
|||||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
|
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 isAffiliate = computed(() => user.value?.badges & UserBadge.AFFILIATE)
|
||||||
const isAdminViewing = computed(() => isAdmin(auth.value.user))
|
const isAdminViewing = computed(() => isAdmin(auth.value.user))
|
||||||
|
const userDetailsModal = useTemplateRef('userDetailsModal')
|
||||||
|
|
||||||
async function toggleAffiliate(id) {
|
async function toggleAffiliate(id) {
|
||||||
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
|
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
|
||||||
queryClient.invalidateQueries({ queryKey: ['user', userId] })
|
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(() => [
|
const navLinks = computed(() => [
|
||||||
{
|
{
|
||||||
label: formatMessage(commonMessages.allProjectType),
|
label: formatMessage(commonMessages.allProjectType),
|
||||||
@@ -950,7 +741,7 @@ function saveRoleEdit() {
|
|||||||
|
|
||||||
editRoleModal.value?.hide()
|
editRoleModal.value?.hide()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
console.error('Failed to update user role:', error)
|
console.error('Failed to update user role:', error)
|
||||||
|
|
||||||
addNotification({
|
addNotification({
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col gap-2 border-0 border-b border-solid border-divider pb-4">
|
|
||||||
<div class="flex flex-wrap items-start gap-4 max-md:flex-col">
|
|
||||||
<div class="flex min-w-0 flex-1 gap-4">
|
|
||||||
<slot name="icon" />
|
|
||||||
<div class="flex min-w-0 flex-col gap-2 justify-center">
|
|
||||||
<div class="flex flex-col gap-1.5 justify-center">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<h1 class="m-0 text-2xl font-semibold leading-none text-contrast">
|
|
||||||
<slot name="title" />
|
|
||||||
</h1>
|
|
||||||
<slot name="title-suffix" />
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="$slots.summary"
|
|
||||||
class="m-0 max-w-[44rem] empty:hidden"
|
|
||||||
:class="[disableLineClamp ? '' : 'line-clamp-2']"
|
|
||||||
>
|
|
||||||
<slot name="summary" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="$slots.stats" class="flex flex-wrap gap-3 empty:hidden max-md:hidden">
|
|
||||||
<slot name="stats" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
|
||||||
<slot name="actions" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="$slots.stats" class="flex justify-between md:hidden">
|
|
||||||
<div class="flex flex-wrap gap-3 empty:hidden">
|
|
||||||
<slot name="stats" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Props {
|
|
||||||
disableLineClamp?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const { disableLineClamp } = defineProps<Props>()
|
|
||||||
</script>
|
|
||||||
@@ -33,7 +33,8 @@ import { DropdownIcon } from '@modrinth/assets'
|
|||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { ButtonStyled, OverflowMenu } from '../index'
|
import ButtonStyled from './ButtonStyled.vue'
|
||||||
|
import OverflowMenu from './OverflowMenu.vue'
|
||||||
|
|
||||||
// TODO: This should be moved to a shared types file.
|
// TODO: This should be moved to a shared types file.
|
||||||
type Colors = 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
type Colors = 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grid grid-cols-[min-content_1fr_auto] gap-4">
|
|
||||||
<slot name="icon" />
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<slot name="title" />
|
|
||||||
<slot name="summary" />
|
|
||||||
<slot name="stats" />
|
|
||||||
</div>
|
|
||||||
<slot name="actions" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
<div data-pyro-telepopover-wrapper class="relative">
|
<div data-pyro-telepopover-wrapper class="relative">
|
||||||
<button
|
<button
|
||||||
ref="triggerRef"
|
ref="triggerRef"
|
||||||
|
v-tooltip="tooltip"
|
||||||
class="teleport-overflow-menu-trigger"
|
class="teleport-overflow-menu-trigger"
|
||||||
:class="btnClass"
|
:class="btnClass"
|
||||||
:aria-expanded="isOpen"
|
:aria-expanded="isOpen"
|
||||||
:aria-haspopup="true"
|
:aria-haspopup="true"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:disabled="disabled"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mouseenter="handleMouseEnter"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
@@ -38,9 +41,14 @@
|
|||||||
:key="isDivider(option) ? `divider-${index}` : option.id"
|
:key="isDivider(option) ? `divider-${index}` : option.id"
|
||||||
>
|
>
|
||||||
<div v-if="isDivider(option)" class="h-px w-full bg-surface-5"></div>
|
<div v-if="isDivider(option)" class="h-px w-full bg-surface-5"></div>
|
||||||
<ButtonStyled v-else type="transparent" role="menuitem" :color="option.color">
|
<ButtonStyled
|
||||||
|
v-else
|
||||||
|
type="transparent"
|
||||||
|
role="menuitem"
|
||||||
|
:color="optionButtonColor(option)"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-if="typeof option.action === 'function'"
|
v-if="typeof option.action === 'function' || option.disabled"
|
||||||
:ref="
|
:ref="
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) menuItemsRef[index] = el as HTMLElement
|
if (el) menuItemsRef[index] = el as HTMLElement
|
||||||
@@ -49,41 +57,45 @@
|
|||||||
v-tooltip="option.tooltip"
|
v-tooltip="option.tooltip"
|
||||||
:disabled="option.disabled"
|
:disabled="option.disabled"
|
||||||
class="w-full !justify-start !whitespace-nowrap focus-visible:!outline-none"
|
class="w-full !justify-start !whitespace-nowrap focus-visible:!outline-none"
|
||||||
|
:aria-label="option.ariaLabel ?? option.label ?? option.id"
|
||||||
:aria-selected="index === selectedIndex"
|
:aria-selected="index === selectedIndex"
|
||||||
:style="index === selectedIndex ? { background: 'var(--color-button-bg)' } : {}"
|
:style="index === selectedIndex ? { background: 'var(--color-button-bg)' } : {}"
|
||||||
@click="handleItemClick(option, index)"
|
@click="(event) => handleItemClick(option, index, event)"
|
||||||
@focus="selectedIndex = index"
|
@focus="selectedIndex = index"
|
||||||
@mouseover="handleMouseOver(index)"
|
@mouseover="handleMouseOver(index)"
|
||||||
>
|
>
|
||||||
<slot :name="option.id">
|
<slot :name="option.id">
|
||||||
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
{{ option.id }}
|
{{ option.label ?? option.id }}
|
||||||
</slot>
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
<AutoLink
|
<AutoLink
|
||||||
v-else-if="typeof option.action === 'string'"
|
v-else-if="optionLink(option)"
|
||||||
:ref="
|
:ref="
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) menuItemsRef[index] = el as HTMLElement
|
if (el) menuItemsRef[index] = el as HTMLElement
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:to="option.action"
|
:to="optionLink(option)"
|
||||||
|
:target="option.external ? '_blank' : undefined"
|
||||||
|
:rel="option.external ? 'noopener noreferrer' : undefined"
|
||||||
class="w-full !justify-start !whitespace-nowrap focus-visible:!outline-none"
|
class="w-full !justify-start !whitespace-nowrap focus-visible:!outline-none"
|
||||||
|
:aria-label="option.ariaLabel ?? option.label ?? option.id"
|
||||||
:aria-selected="index === selectedIndex"
|
:aria-selected="index === selectedIndex"
|
||||||
:style="index === selectedIndex ? { background: 'var(--color-button-bg)' } : {}"
|
:style="index === selectedIndex ? { background: 'var(--color-button-bg)' } : {}"
|
||||||
@click="handleItemClick(option, index)"
|
@click="(event) => handleItemClick(option, index, event)"
|
||||||
@focus="selectedIndex = index"
|
@focus="selectedIndex = index"
|
||||||
@mouseover="handleMouseOver(index)"
|
@mouseover="handleMouseOver(index)"
|
||||||
>
|
>
|
||||||
<slot :name="option.id">
|
<slot :name="option.id">
|
||||||
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
{{ option.id }}
|
{{ option.label ?? option.id }}
|
||||||
</slot>
|
</slot>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<slot :name="option.id">
|
<slot :name="option.id">
|
||||||
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
{{ option.id }}
|
{{ option.label ?? option.id }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -95,29 +107,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AutoLink, ButtonStyled } from '@modrinth/ui'
|
|
||||||
import { onClickOutside, useElementHover } from '@vueuse/core'
|
import { onClickOutside, useElementHover } from '@vueuse/core'
|
||||||
import { type Component, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { type Component, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
interface Option {
|
import AutoLink from './AutoLink.vue'
|
||||||
|
import ButtonStyled from './ButtonStyled.vue'
|
||||||
|
|
||||||
|
type OptionColor =
|
||||||
|
| 'standard'
|
||||||
|
| 'brand'
|
||||||
|
| 'primary'
|
||||||
|
| 'danger'
|
||||||
|
| 'secondary'
|
||||||
|
| 'highlight'
|
||||||
|
| 'red'
|
||||||
|
| 'orange'
|
||||||
|
| 'green'
|
||||||
|
| 'blue'
|
||||||
|
| 'purple'
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
id: string
|
id: string
|
||||||
|
label?: string
|
||||||
icon?: Component
|
icon?: Component
|
||||||
action?: (() => void) | string
|
action?: ((event?: MouseEvent) => void) | string
|
||||||
|
link?: string
|
||||||
|
external?: boolean
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
color?: OptionColor
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
remainOnClick?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Divider = {
|
export type Divider = {
|
||||||
divider?: boolean
|
divider?: boolean
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item = Option | Divider
|
export type Item = Option | Divider
|
||||||
|
|
||||||
function isDivider(item: Item): item is Divider {
|
function isDivider(item: Item): item is Divider {
|
||||||
return (item as Divider).divider
|
return !!(item as Divider).divider
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -125,10 +157,16 @@ const props = withDefaults(
|
|||||||
options: Item[]
|
options: Item[]
|
||||||
hoverable?: boolean
|
hoverable?: boolean
|
||||||
btnClass?: string | string[] | Record<string, boolean>
|
btnClass?: string | string[] | Record<string, boolean>
|
||||||
|
disabled?: boolean
|
||||||
|
tooltip?: string
|
||||||
|
ariaLabel?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
btnClass: undefined,
|
btnClass: undefined,
|
||||||
|
disabled: false,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -191,6 +229,7 @@ const calculateMenuPosition = () => {
|
|||||||
|
|
||||||
const toggleMenu = (event: MouseEvent) => {
|
const toggleMenu = (event: MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
if (props.disabled) return
|
||||||
if (!props.hoverable) {
|
if (!props.hoverable) {
|
||||||
if (isOpen.value) {
|
if (isOpen.value) {
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -201,6 +240,7 @@ const toggleMenu = (event: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
|
if (props.disabled) return
|
||||||
isOpen.value = true
|
isOpen.value = true
|
||||||
emit('open')
|
emit('open')
|
||||||
disableBodyScroll()
|
disableBodyScroll()
|
||||||
@@ -218,15 +258,18 @@ const closeMenu = () => {
|
|||||||
document.removeEventListener('mousemove', handleMouseMove)
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectOption = (option: Option) => {
|
const selectOption = (option: Option, event?: MouseEvent) => {
|
||||||
emit('select', option)
|
emit('select', option)
|
||||||
if (typeof option.action === 'function') {
|
if (typeof option.action === 'function') {
|
||||||
option.action()
|
option.action(event)
|
||||||
|
}
|
||||||
|
if (!option.remainOnClick) {
|
||||||
|
closeMenu()
|
||||||
}
|
}
|
||||||
closeMenu()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent) => {
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
|
if (props.disabled) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
isMouseDown.value = true
|
isMouseDown.value = true
|
||||||
}
|
}
|
||||||
@@ -270,10 +313,10 @@ const handleMouseMove = (event: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemClick = (option: Option, index: number) => {
|
const handleItemClick = (option: Option, index: number, event?: MouseEvent) => {
|
||||||
if (option.disabled) return
|
if (option.disabled) return
|
||||||
selectedIndex.value = index
|
selectedIndex.value = index
|
||||||
selectOption(option)
|
selectOption(option, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseOver = (index: number) => {
|
const handleMouseOver = (index: number) => {
|
||||||
@@ -432,4 +475,23 @@ onClickOutside(menuRef, (event) => {
|
|||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function optionLink(option: Option) {
|
||||||
|
if (typeof option.action === 'string') return option.action
|
||||||
|
return option.link
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionButtonColor(option: Option) {
|
||||||
|
switch (option.color) {
|
||||||
|
case 'primary':
|
||||||
|
return 'brand'
|
||||||
|
case 'danger':
|
||||||
|
return 'red'
|
||||||
|
case 'secondary':
|
||||||
|
case 'highlight':
|
||||||
|
return 'standard'
|
||||||
|
default:
|
||||||
|
return option.color ?? 'standard'
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export { default as CollapsibleAdmonition } from './CollapsibleAdmonition.vue'
|
|||||||
export { default as CollapsibleRegion } from './CollapsibleRegion.vue'
|
export { default as CollapsibleRegion } from './CollapsibleRegion.vue'
|
||||||
export type { ComboboxOption } from './Combobox.vue'
|
export type { ComboboxOption } from './Combobox.vue'
|
||||||
export { default as Combobox } from './Combobox.vue'
|
export { default as Combobox } from './Combobox.vue'
|
||||||
export { default as ContentPageHeader } from './ContentPageHeader.vue'
|
|
||||||
export { default as CopyCode } from './CopyCode.vue'
|
export { default as CopyCode } from './CopyCode.vue'
|
||||||
export { default as DatePicker } from './DatePicker.vue'
|
export { default as DatePicker } from './DatePicker.vue'
|
||||||
export { default as DoubleIcon } from './DoubleIcon.vue'
|
export { default as DoubleIcon } from './DoubleIcon.vue'
|
||||||
@@ -65,6 +64,23 @@ export { default as OptionGroup } from './OptionGroup.vue'
|
|||||||
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
||||||
export { default as OverflowMenu } from './OverflowMenu.vue'
|
export { default as OverflowMenu } from './OverflowMenu.vue'
|
||||||
export { default as Page } from './Page.vue'
|
export { default as Page } from './Page.vue'
|
||||||
|
export { default as PageHeader } from './page-header/index.vue'
|
||||||
|
export { default as PageHeaderMetadata } from './page-header/metadata/index.vue'
|
||||||
|
export { default as PageHeaderMetadataItem } from './page-header/metadata/page-header-metadata-item.vue'
|
||||||
|
export { default as PageHeaderMetadataNumberItem } from './page-header/metadata/page-header-metadata-number-item.vue'
|
||||||
|
export { default as PageHeaderMetadataTagsItem } from './page-header/metadata/page-header-metadata-tags-item.vue'
|
||||||
|
export { default as PageHeaderMetadataTimeItem } from './page-header/metadata/page-header-metadata-time-item.vue'
|
||||||
|
export { default as PageHeaderActions } from './page-header/page-header-actions.vue'
|
||||||
|
export { default as PageHeaderBadgeItem } from './page-header/page-header-badge-item.vue'
|
||||||
|
export type {
|
||||||
|
PageHeaderClass,
|
||||||
|
PageHeaderClickHandler,
|
||||||
|
PageHeaderIconProps,
|
||||||
|
PageHeaderInteractiveProps,
|
||||||
|
PageHeaderMetadataItemProps,
|
||||||
|
PageHeaderProps,
|
||||||
|
PageHeaderTarget,
|
||||||
|
} from './page-header/types'
|
||||||
export { default as Pagination } from './Pagination.vue'
|
export { default as Pagination } from './Pagination.vue'
|
||||||
export { default as PopoutMenu } from './PopoutMenu.vue'
|
export { default as PopoutMenu } from './PopoutMenu.vue'
|
||||||
export { default as PreviewSelectButton } from './PreviewSelectButton.vue'
|
export { default as PreviewSelectButton } from './PreviewSelectButton.vue'
|
||||||
@@ -89,6 +105,11 @@ export type { TabsTab, TabsValue } from './Tabs.vue'
|
|||||||
export { default as Tabs } from './Tabs.vue'
|
export { default as Tabs } from './Tabs.vue'
|
||||||
export { default as TagItem } from './TagItem.vue'
|
export { default as TagItem } from './TagItem.vue'
|
||||||
export { default as TagTagItem } from './TagTagItem.vue'
|
export { default as TagTagItem } from './TagTagItem.vue'
|
||||||
|
export type {
|
||||||
|
Item as TeleportOverflowMenuItem,
|
||||||
|
Option as TeleportOverflowMenuOption,
|
||||||
|
} from './TeleportOverflowMenu.vue'
|
||||||
|
export { default as TeleportOverflowMenu } from './TeleportOverflowMenu.vue'
|
||||||
export type {
|
export type {
|
||||||
TimeFrameLastUnit,
|
TimeFrameLastUnit,
|
||||||
TimeFrameLastUnitOption,
|
TimeFrameLastUnitOption,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2" :class="rootClass">
|
||||||
|
<div class="flex flex-wrap items-start gap-4 max-md:flex-col" :class="rowClass">
|
||||||
|
<div class="flex min-w-0 flex-1 gap-4" :class="mainClass">
|
||||||
|
<div v-if="$slots.leading" class="flex shrink-0 items-center gap-4">
|
||||||
|
<slot name="leading" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col justify-center gap-2">
|
||||||
|
<div class="flex flex-col justify-center gap-1.5">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<h1
|
||||||
|
class="m-0 min-w-0 max-w-full text-2xl font-semibold leading-none text-contrast"
|
||||||
|
:class="titleClassValue"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<slot name="badges" />
|
||||||
|
</div>
|
||||||
|
<p v-if="hasSummary" class="m-0 max-w-[44rem]" :class="summaryClass">
|
||||||
|
<slot name="summary">{{ summary }}</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.metadata" class="max-md:hidden">
|
||||||
|
<slot name="metadata" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.metadata" class="flex justify-between md:hidden">
|
||||||
|
<slot name="metadata" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useSlots } from 'vue'
|
||||||
|
|
||||||
|
import type { PageHeaderProps } from './types'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PageHeaderProps>(), {
|
||||||
|
summary: null,
|
||||||
|
headerClass: '',
|
||||||
|
rowClass: '',
|
||||||
|
mainClass: '',
|
||||||
|
titleClass: '',
|
||||||
|
truncateTitle: false,
|
||||||
|
divider: true,
|
||||||
|
bottomPadding: true,
|
||||||
|
disableLineClamp: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const rootClass = computed(() => [
|
||||||
|
props.divider ? 'border-0 border-b border-solid border-divider' : '',
|
||||||
|
props.bottomPadding ? 'pb-4' : '',
|
||||||
|
props.headerClass,
|
||||||
|
])
|
||||||
|
const titleClassValue = computed(() => [props.truncateTitle ? 'truncate' : '', props.titleClass])
|
||||||
|
const summaryClass = computed(() => (props.disableLineClamp ? '' : 'line-clamp-2'))
|
||||||
|
const hasSummary = computed(() => !!props.summary || !!slots.summary)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="metadata"
|
||||||
|
class="page-header-metadata flex min-w-0 flex-wrap items-center gap-x-2 gap-y-2"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const metadata = ref<HTMLElement | null>(null)
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
let mutationObserver: MutationObserver | null = null
|
||||||
|
const observedItems = new Set<Element>()
|
||||||
|
|
||||||
|
function getItems() {
|
||||||
|
return Array.from(
|
||||||
|
metadata.value?.querySelectorAll<HTMLElement>('[data-page-header-metadata-item]') ?? [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeItems() {
|
||||||
|
if (!resizeObserver) return
|
||||||
|
|
||||||
|
for (const item of getItems()) {
|
||||||
|
if (observedItems.has(item)) continue
|
||||||
|
|
||||||
|
resizeObserver.observe(item)
|
||||||
|
observedItems.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowStarts() {
|
||||||
|
const root = metadata.value
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
const isRtl = getComputedStyle(root).direction === 'rtl'
|
||||||
|
let previousOffset: number | null = null
|
||||||
|
|
||||||
|
for (const item of getItems()) {
|
||||||
|
const offset = item.offsetLeft
|
||||||
|
const startsRow =
|
||||||
|
previousOffset === null ||
|
||||||
|
(isRtl ? offset >= previousOffset - 1 : offset <= previousOffset + 1)
|
||||||
|
|
||||||
|
item.toggleAttribute('data-page-header-metadata-row-start', startsRow)
|
||||||
|
previousOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleUpdate() {
|
||||||
|
void nextTick(() => {
|
||||||
|
observeItems()
|
||||||
|
updateRowStarts()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scheduleUpdate()
|
||||||
|
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
resizeObserver = new ResizeObserver(scheduleUpdate)
|
||||||
|
if (metadata.value) {
|
||||||
|
resizeObserver.observe(metadata.value)
|
||||||
|
}
|
||||||
|
observeItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof MutationObserver !== 'undefined' && metadata.value) {
|
||||||
|
mutationObserver = new MutationObserver(scheduleUpdate)
|
||||||
|
mutationObserver.observe(metadata.value, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
mutationObserver?.disconnect()
|
||||||
|
observedItems.clear()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-header-metadata
|
||||||
|
:deep([data-page-header-metadata-item]:first-child .page-header-metadata-item-divider),
|
||||||
|
.page-header-metadata
|
||||||
|
:deep([data-page-header-metadata-row-start] .page-header-metadata-item-divider) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="rootClass" data-page-header-metadata-item v-bind="$attrs">
|
||||||
|
<BulletDivider class="page-header-metadata-item-divider shrink-0" />
|
||||||
|
<AutoLink
|
||||||
|
v-if="to && !disabled"
|
||||||
|
v-tooltip="tooltip"
|
||||||
|
:to="to"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:link-class="contentClassString"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</AutoLink>
|
||||||
|
<button
|
||||||
|
v-else-if="action || disabled"
|
||||||
|
v-tooltip="tooltip"
|
||||||
|
type="button"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:class="contentClass"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
<div v-else v-tooltip="tooltip" :aria-label="ariaLabel" :class="contentClass">
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import AutoLink from '../../AutoLink.vue'
|
||||||
|
import BulletDivider from '../../BulletDivider.vue'
|
||||||
|
import type { PageHeaderMetadataItemProps } from '../types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PageHeaderMetadataItemProps>(), {
|
||||||
|
icon: undefined,
|
||||||
|
iconProps: undefined,
|
||||||
|
iconClass: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
to: undefined,
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultIconClass = 'block size-5 shrink-0 text-current'
|
||||||
|
const baseClass =
|
||||||
|
'flex min-w-0 items-center gap-2 font-medium leading-none text-secondary text-nowrap'
|
||||||
|
const contentBaseClass = 'inline-flex min-w-0 items-center gap-2 text-inherit'
|
||||||
|
const interactiveClass = 'm-0 cursor-pointer border-0 bg-transparent p-0 hover:underline'
|
||||||
|
|
||||||
|
const rootClass = computed(() => [baseClass, props.disabled ? 'cursor-not-allowed opacity-60' : ''])
|
||||||
|
const contentClass = computed(() => [
|
||||||
|
contentBaseClass,
|
||||||
|
props.to || props.action ? interactiveClass : '',
|
||||||
|
])
|
||||||
|
const contentClassString = computed(() => contentClass.value.filter(Boolean).join(' '))
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
void props.action?.(event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<PageHeaderMetadataItem
|
||||||
|
:icon="icon"
|
||||||
|
:icon-props="iconProps"
|
||||||
|
:icon-class="iconClass"
|
||||||
|
:tooltip="resolvedTooltip"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:to="to"
|
||||||
|
:action="action"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-baseline gap-1">
|
||||||
|
<span>{{ formattedValue }}</span>
|
||||||
|
<span v-if="label">{{ label }}</span>
|
||||||
|
</span>
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { PageHeaderMetadataItemProps } from '../types'
|
||||||
|
import PageHeaderMetadataItem from './page-header-metadata-item.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<
|
||||||
|
PageHeaderMetadataItemProps & {
|
||||||
|
value: number
|
||||||
|
label?: string
|
||||||
|
locale?: string
|
||||||
|
compact?: boolean
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
locale: undefined,
|
||||||
|
compact: true,
|
||||||
|
icon: undefined,
|
||||||
|
iconProps: undefined,
|
||||||
|
iconClass: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
to: undefined,
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const formattedValue = computed(() =>
|
||||||
|
new Intl.NumberFormat(props.locale, {
|
||||||
|
notation: props.compact ? 'compact' : 'standard',
|
||||||
|
maximumFractionDigits: props.compact ? 1 : 0,
|
||||||
|
}).format(props.value),
|
||||||
|
)
|
||||||
|
const fullValue = computed(() => new Intl.NumberFormat(props.locale).format(props.value))
|
||||||
|
const resolvedTooltip = computed(() => {
|
||||||
|
if (props.tooltip) return props.tooltip
|
||||||
|
if (!props.compact || formattedValue.value === fullValue.value) return undefined
|
||||||
|
return [fullValue.value, props.label].filter(Boolean).join(' ')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<PageHeaderMetadataItem
|
||||||
|
class="!text-wrap"
|
||||||
|
:icon="icon"
|
||||||
|
:icon-props="iconProps"
|
||||||
|
:icon-class="iconClass"
|
||||||
|
:tooltip="tooltip"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:to="to"
|
||||||
|
:action="action"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageHeaderMetadataItemProps } from '../types'
|
||||||
|
import PageHeaderMetadataItem from './page-header-metadata-item.vue'
|
||||||
|
|
||||||
|
withDefaults(defineProps<PageHeaderMetadataItemProps>(), {
|
||||||
|
icon: undefined,
|
||||||
|
iconProps: undefined,
|
||||||
|
iconClass: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
to: undefined,
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<PageHeaderMetadataItem
|
||||||
|
:icon="icon"
|
||||||
|
:icon-props="iconProps"
|
||||||
|
:icon-class="iconClass"
|
||||||
|
:tooltip="resolvedTooltip"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:to="to"
|
||||||
|
:action="action"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<span>{{ displayLabel }}</span>
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { useFormatDateTime, useRelativeTime } from '../../../../composables'
|
||||||
|
import type { PageHeaderMetadataItemProps } from '../types'
|
||||||
|
import PageHeaderMetadataItem from './page-header-metadata-item.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<
|
||||||
|
PageHeaderMetadataItemProps & {
|
||||||
|
date: string | number | Date
|
||||||
|
label?: string
|
||||||
|
relative?: boolean
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
relative: true,
|
||||||
|
icon: undefined,
|
||||||
|
iconProps: undefined,
|
||||||
|
iconClass: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
to: undefined,
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatDateTime = useFormatDateTime({
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
|
const parsedDate = computed(() => {
|
||||||
|
const date = props.date instanceof Date ? props.date : new Date(props.date)
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date
|
||||||
|
})
|
||||||
|
const absoluteDate = computed(() => {
|
||||||
|
if (!parsedDate.value) return ''
|
||||||
|
return formatDateTime(parsedDate.value)
|
||||||
|
})
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
if (!parsedDate.value) return ''
|
||||||
|
if (!props.relative) return absoluteDate.value
|
||||||
|
return formatRelativeTime(parsedDate.value)
|
||||||
|
})
|
||||||
|
const resolvedTooltip = computed(() => props.tooltip ?? (absoluteDate.value || undefined))
|
||||||
|
const displayLabel = computed(() => [props.label, formattedDate.value].filter(Boolean).join(' '))
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<AutoLink
|
||||||
|
v-if="to && !disabled"
|
||||||
|
v-tooltip="tooltip"
|
||||||
|
:to="to"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:link-class="badgeClassString"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</AutoLink>
|
||||||
|
<button
|
||||||
|
v-else-if="action || disabled"
|
||||||
|
v-tooltip="tooltip"
|
||||||
|
type="button"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:class="badgeClass"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
<div v-else v-tooltip="tooltip" :aria-label="ariaLabel" :class="badgeClass" v-bind="$attrs">
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
|
:class="iconClass ?? defaultIconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="iconProps"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import AutoLink from '../AutoLink.vue'
|
||||||
|
import type { PageHeaderIconProps, PageHeaderInteractiveProps } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PageHeaderIconProps & PageHeaderInteractiveProps>(), {
|
||||||
|
icon: undefined,
|
||||||
|
iconProps: undefined,
|
||||||
|
iconClass: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
to: undefined,
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeClass = computed(() => [
|
||||||
|
'inline-flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-button-bg px-2 py-1 text-sm font-semibold leading-none text-secondary text-nowrap',
|
||||||
|
props.to || props.action ? 'm-0 cursor-pointer hover:underline' : '',
|
||||||
|
props.disabled ? 'cursor-not-allowed opacity-60' : '',
|
||||||
|
])
|
||||||
|
const badgeClassString = computed(() => badgeClass.value.filter(Boolean).join(' '))
|
||||||
|
const defaultIconClass = 'block size-4 shrink-0 text-current'
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
void props.action?.(event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Component, HTMLAttributes } from 'vue'
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
|
export type PageHeaderTarget = string | RouteLocationRaw
|
||||||
|
export type PageHeaderClass = HTMLAttributes['class']
|
||||||
|
|
||||||
|
export type PageHeaderClickHandler = (event: MouseEvent) => void | Promise<void>
|
||||||
|
|
||||||
|
export type PageHeaderIconProps = {
|
||||||
|
icon?: Component
|
||||||
|
iconProps?: Record<string, unknown>
|
||||||
|
iconClass?: PageHeaderClass
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageHeaderInteractiveProps = {
|
||||||
|
tooltip?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
to?: PageHeaderTarget
|
||||||
|
action?: PageHeaderClickHandler
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageHeaderMetadataItemProps = PageHeaderIconProps & PageHeaderInteractiveProps
|
||||||
|
|
||||||
|
export type PageHeaderProps = {
|
||||||
|
title: string
|
||||||
|
summary?: string | null
|
||||||
|
headerClass?: PageHeaderClass
|
||||||
|
rowClass?: PageHeaderClass
|
||||||
|
mainClass?: PageHeaderClass
|
||||||
|
titleClass?: PageHeaderClass
|
||||||
|
truncateTitle?: boolean
|
||||||
|
divider?: boolean
|
||||||
|
bottomPadding?: boolean
|
||||||
|
disableLineClamp?: boolean
|
||||||
|
}
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ContentPageHeader>
|
|
||||||
<template #icon>
|
|
||||||
<Avatar :src="project.icon_url" :alt="project.title" size="96px" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
{{ project.title }}
|
|
||||||
</template>
|
|
||||||
<template #title-suffix>
|
|
||||||
<ProjectStatusBadge v-if="member || project.status !== 'approved'" :status="project.status" />
|
|
||||||
</template>
|
|
||||||
<template #summary>
|
|
||||||
{{ project.description }}
|
|
||||||
</template>
|
|
||||||
<template #stats>
|
|
||||||
<div class="flex items-center gap-3 flex-wrap gap-y-0">
|
|
||||||
<template v-if="isServerProject">
|
|
||||||
<ServerDetails
|
|
||||||
v-if="projectV3?.status !== 'draft'"
|
|
||||||
:online-players="playersOnline"
|
|
||||||
:status-online="statusOnline"
|
|
||||||
:recent-plays="javaServer?.verified_plays_2w ?? 0"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div
|
|
||||||
v-tooltip="
|
|
||||||
capitalizeString(
|
|
||||||
formatMessage(commonMessages.projectDownloads, {
|
|
||||||
count: project.downloads,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="flex items-center gap-2 font-semibold cursor-help"
|
|
||||||
>
|
|
||||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
|
||||||
{{ formatCompactNumber(project.downloads) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-tooltip="
|
|
||||||
capitalizeString(
|
|
||||||
formatMessage(commonMessages.projectFollowers, {
|
|
||||||
count: project.followers,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="flex items-center gap-2 cursor-help"
|
|
||||||
:class="{ 'md:border-r': project.categories.length > 0 }"
|
|
||||||
>
|
|
||||||
<HeartIcon class="h-6 w-6 text-secondary" />
|
|
||||||
<span class="font-semibold">
|
|
||||||
{{ formatCompactNumber(project.followers) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="project.categories.length > 0" class="hidden items-center gap-2 md:flex">
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<TagItem
|
|
||||||
v-for="(category, index) in project.categories"
|
|
||||||
:key="index"
|
|
||||||
:action="() => router.push(`${searchUrl}?f=categories:${category}`)"
|
|
||||||
>
|
|
||||||
<FormattedTag :tag="category" />
|
|
||||||
</TagItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<slot name="actions" />
|
|
||||||
</template>
|
|
||||||
</ContentPageHeader>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
|
||||||
import { DownloadIcon, HeartIcon } from '@modrinth/assets'
|
|
||||||
import { capitalizeString, type Project } from '@modrinth/utils'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { useCompactNumber, useVIntl } from '../../composables'
|
|
||||||
import { commonMessages } from '../../utils'
|
|
||||||
import Avatar from '../base/Avatar.vue'
|
|
||||||
import ContentPageHeader from '../base/ContentPageHeader.vue'
|
|
||||||
import FormattedTag from '../base/FormattedTag.vue'
|
|
||||||
import TagItem from '../base/TagItem.vue'
|
|
||||||
import ProjectStatusBadge from './ProjectStatusBadge.vue'
|
|
||||||
import ServerDetails from './server/ServerDetails.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const { formatCompactNumber } = useCompactNumber()
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
project: Project
|
|
||||||
member?: boolean
|
|
||||||
projectV3?: Labrinth.Projects.v3.Project | null
|
|
||||||
ping?: number
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
member: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const searchUrl = computed(
|
|
||||||
() => `/discover/${isServerProject.value ? 'servers' : `${props.project.project_type}s`}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const isServerProject = computed(() => !!props.projectV3?.minecraft_server)
|
|
||||||
const javaServer = computed(() => props.projectV3?.minecraft_java_server)
|
|
||||||
const javaServerPingData = computed(() => props.projectV3?.minecraft_java_server?.ping?.data)
|
|
||||||
const playersOnline = computed(() => javaServerPingData.value?.players_online ?? 0)
|
|
||||||
const statusOnline = computed(() => !!javaServerPingData.value)
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="project.title"
|
||||||
|
:summary="project.description"
|
||||||
|
@contextmenu="emit('contextmenu', $event)"
|
||||||
|
>
|
||||||
|
<template #leading>
|
||||||
|
<Avatar :src="project.icon_url" :alt="project.title" :tint-by="project.id" size="96px" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="showStatusBadge" #badges>
|
||||||
|
<ProjectStatusBadge :status="project.status" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<template v-if="projectV3?.minecraft_server != null">
|
||||||
|
<ServerDetails
|
||||||
|
v-if="projectV3?.status !== 'draft'"
|
||||||
|
:online-players="projectV3?.minecraft_java_server?.ping?.data?.players_online ?? 0"
|
||||||
|
:status-online="!!projectV3?.minecraft_java_server?.ping?.data"
|
||||||
|
:recent-plays="projectV3?.minecraft_java_server?.verified_plays_2w ?? 0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<PageHeaderMetadataNumberItem
|
||||||
|
:icon="DownloadIcon"
|
||||||
|
:value="project.downloads"
|
||||||
|
:label="formatMessage(messages.downloadsStat, { count: project.downloads })"
|
||||||
|
:tooltip="formatNumber(project.downloads)"
|
||||||
|
/>
|
||||||
|
<PageHeaderMetadataNumberItem
|
||||||
|
:icon="HeartIcon"
|
||||||
|
:value="project.followers"
|
||||||
|
:label="formatMessage(messages.followersStat, { count: project.followers })"
|
||||||
|
:tooltip="formatNumber(project.followers)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<PageHeaderMetadataTagsItem v-if="project.categories.length > 0" class="hidden md:flex">
|
||||||
|
<TagItem
|
||||||
|
v-for="category in project.categories"
|
||||||
|
:key="category"
|
||||||
|
:action="() => emit('category', category)"
|
||||||
|
>
|
||||||
|
<FormattedTag :tag="category" />
|
||||||
|
</TagItem>
|
||||||
|
</PageHeaderMetadataTagsItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<slot name="actions" />
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { DownloadIcon, HeartIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
|
import { defineMessages, useFormatNumber, useVIntl } from '../../composables'
|
||||||
|
import Avatar from '../base/Avatar.vue'
|
||||||
|
import FormattedTag from '../base/FormattedTag.vue'
|
||||||
|
import PageHeader from '../base/page-header/index.vue'
|
||||||
|
import PageHeaderMetadata from '../base/page-header/metadata/index.vue'
|
||||||
|
import PageHeaderMetadataNumberItem from '../base/page-header/metadata/page-header-metadata-number-item.vue'
|
||||||
|
import PageHeaderMetadataTagsItem from '../base/page-header/metadata/page-header-metadata-tags-item.vue'
|
||||||
|
import PageHeaderActions from '../base/page-header/page-header-actions.vue'
|
||||||
|
import TagItem from '../base/TagItem.vue'
|
||||||
|
import ProjectStatusBadge from './ProjectStatusBadge.vue'
|
||||||
|
import ServerDetails from './server/ServerDetails.vue'
|
||||||
|
|
||||||
|
type HeaderProject = Pick<
|
||||||
|
Labrinth.Projects.v2.Project,
|
||||||
|
'id' | 'title' | 'description' | 'status' | 'downloads' | 'followers' | 'categories'
|
||||||
|
> & {
|
||||||
|
icon_url?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderProjectV3 = Pick<
|
||||||
|
Labrinth.Projects.v3.Project,
|
||||||
|
'status' | 'minecraft_server' | 'minecraft_java_server'
|
||||||
|
>
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
project: HeaderProject
|
||||||
|
projectV3?: HeaderProjectV3 | null
|
||||||
|
showStatusBadge?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
projectV3: null,
|
||||||
|
showStatusBadge: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
category: [category: string]
|
||||||
|
contextmenu: [event: MouseEvent]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
downloadsStat: {
|
||||||
|
id: 'project.stats.downloads-label',
|
||||||
|
defaultMessage: '{count, plural, one {download} other {downloads}}',
|
||||||
|
},
|
||||||
|
followersStat: {
|
||||||
|
id: 'project.stats.followers-label',
|
||||||
|
defaultMessage: '{count, plural, one {follower} other {followers}}',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const formatNumber = useFormatNumber()
|
||||||
|
</script>
|
||||||
@@ -6,8 +6,8 @@ export { default as ProjectCard } from './card/ProjectCard.vue'
|
|||||||
export { default as ProjectBackgroundGradient } from './ProjectBackgroundGradient.vue'
|
export { default as ProjectBackgroundGradient } from './ProjectBackgroundGradient.vue'
|
||||||
export { default as ProjectCardList } from './ProjectCardList.vue'
|
export { default as ProjectCardList } from './ProjectCardList.vue'
|
||||||
export { default as ProjectCombobox } from './ProjectCombobox.vue'
|
export { default as ProjectCombobox } from './ProjectCombobox.vue'
|
||||||
export { default as ProjectHeader } from './ProjectHeader.vue'
|
|
||||||
export { default as ProjectPageDescription } from './ProjectPageDescription.vue'
|
export { default as ProjectPageDescription } from './ProjectPageDescription.vue'
|
||||||
|
export { default as ProjectPageHeader } from './ProjectPageHeader.vue'
|
||||||
export { default as ProjectPageVersions } from './ProjectPageVersions.vue'
|
export { default as ProjectPageVersions } from './ProjectPageVersions.vue'
|
||||||
export { default as ProjectSidebarCompatibility } from './ProjectSidebarCompatibility.vue'
|
export { default as ProjectSidebarCompatibility } from './ProjectSidebarCompatibility.vue'
|
||||||
export { default as ProjectSidebarCreators } from './ProjectSidebarCreators.vue'
|
export { default as ProjectSidebarCreators } from './ProjectSidebarCreators.vue'
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="contents">
|
|
||||||
<ButtonStyled circular type="transparent" size="large">
|
|
||||||
<TeleportOverflowMenu :options="menuOptions">
|
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
|
||||||
<template #allServers>
|
|
||||||
<ServerIcon class="h-5 w-5" />
|
|
||||||
<span>All servers</span>
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
|
||||||
<span>Copy ID</span>
|
|
||||||
</template>
|
|
||||||
</TeleportOverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ClipboardCopyIcon, MoreVerticalIcon, ServerIcon } from '@modrinth/assets'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { ButtonStyled } from '#ui/components'
|
|
||||||
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
|
|
||||||
import { injectModrinthServerContext } from '#ui/providers'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
disabled?: boolean
|
|
||||||
showCopyIdAction?: boolean
|
|
||||||
showDebugInfo?: boolean
|
|
||||||
uptimeSeconds?: number
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
showCopyIdAction: false,
|
|
||||||
showDebugInfo: false,
|
|
||||||
uptimeSeconds: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { serverId } = injectModrinthServerContext()
|
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
|
||||||
{
|
|
||||||
id: 'allServers',
|
|
||||||
label: 'All servers',
|
|
||||||
icon: ServerIcon,
|
|
||||||
action: () => router.push('/hosting/manage'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'copy-id',
|
|
||||||
label: 'Copy ID',
|
|
||||||
icon: ClipboardCopyIcon,
|
|
||||||
action: () => copyId(),
|
|
||||||
shown: props.showCopyIdAction,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
async function copyId() {
|
|
||||||
await navigator.clipboard.writeText(serverId)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
|
|
||||||
<ContentPageHeader :class="props.headerClass">
|
|
||||||
<template #icon>
|
|
||||||
<ServerIcon
|
|
||||||
:image="headerImage"
|
|
||||||
:class="isNuxt ? 'size-20 !rounded-2xl' : 'size-16 !rounded-xl'"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
{{ props.server?.name || 'Server' }}
|
|
||||||
</template>
|
|
||||||
<template #stats>
|
|
||||||
<div
|
|
||||||
v-if="props.server?.flows?.intro"
|
|
||||||
class="flex items-center gap-2 font-semibold text-secondary"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
Configuring server...
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-wrap items-center gap-2">
|
|
||||||
<div v-if="props.server?.loader" class="flex items-center gap-2 font-medium">
|
|
||||||
<LoaderIcon :loader="props.server.loader" class="flex shrink-0 [&&]:size-5" />
|
|
||||||
{{ formatLoaderLabel(props.server.loader) }} {{ props.server.mc_version }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
props.server?.loader &&
|
|
||||||
props.server?.net?.domain &&
|
|
||||||
!userPreferences.hideSubdomainLabel
|
|
||||||
"
|
|
||||||
class="h-1.5 w-1.5 rounded-full bg-surface-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.server?.net?.domain && !userPreferences.hideSubdomainLabel"
|
|
||||||
v-tooltip="'Copy server address'"
|
|
||||||
class="flex cursor-pointer items-center gap-2 font-medium hover:underline text-nowrap"
|
|
||||||
@click="copyServerAddress"
|
|
||||||
>
|
|
||||||
<LinkIcon class="flex size-5 shrink-0" />
|
|
||||||
{{ props.server.net.domain }}.modrinth.gg
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showUptime" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
|
||||||
|
|
||||||
<div v-if="showUptime" class="flex items-center gap-2 font-medium">
|
|
||||||
<TimerIcon class="flex size-5 shrink-0" />
|
|
||||||
{{ formattedUptime }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="showProject && (props.server?.loader || props.server?.net?.domain || showUptime)"
|
|
||||||
class="h-1.5 w-1.5 rounded-full bg-surface-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="showProject"
|
|
||||||
class="flex items-center gap-1.5 font-medium text-primary text-nowrap"
|
|
||||||
>
|
|
||||||
Linked to
|
|
||||||
<Avatar
|
|
||||||
:src="props.serverProject?.icon_url ?? undefined"
|
|
||||||
:alt="props.serverProject?.title ?? ''"
|
|
||||||
size="24px"
|
|
||||||
/>
|
|
||||||
<AutoLink :to="serverProjectLink" class="truncate text-primary hover:underline">
|
|
||||||
{{ props.serverProject?.title }}
|
|
||||||
</AutoLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<slot name="actions" />
|
|
||||||
</template>
|
|
||||||
</ContentPageHeader>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Archon } from '@modrinth/api-client'
|
|
||||||
import { NuxtModrinthClient } from '@modrinth/api-client'
|
|
||||||
import { LinkIcon, SettingsIcon, TimerIcon } from '@modrinth/assets'
|
|
||||||
import { useStorage } from '@vueuse/core'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
import { AutoLink, Avatar, ContentPageHeader, ServerIcon } from '#ui/components'
|
|
||||||
import {
|
|
||||||
injectModrinthClient,
|
|
||||||
injectModrinthServerContext,
|
|
||||||
injectNotificationManager,
|
|
||||||
} from '#ui/providers'
|
|
||||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
|
||||||
|
|
||||||
import LoaderIcon from '../icons/LoaderIcon.vue'
|
|
||||||
|
|
||||||
type ServerProjectSummary = {
|
|
||||||
id: string
|
|
||||||
slug?: string | null
|
|
||||||
title: string
|
|
||||||
icon_url?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
server: Archon.Servers.v0.Server | null | undefined
|
|
||||||
serverImage?: string | null
|
|
||||||
serverProject?: ServerProjectSummary | null
|
|
||||||
serverProjectLink?: string
|
|
||||||
uptimeSeconds?: number
|
|
||||||
showUptime?: boolean
|
|
||||||
backHref?: string
|
|
||||||
breadcrumbClass?: string
|
|
||||||
headerClass?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
serverImage: null,
|
|
||||||
serverProject: null,
|
|
||||||
serverProjectLink: '',
|
|
||||||
uptimeSeconds: 0,
|
|
||||||
showUptime: true,
|
|
||||||
backHref: '/hosting/manage',
|
|
||||||
breadcrumbClass: 'breadcrumb goto-link flex w-fit items-center',
|
|
||||||
headerClass: '',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
|
||||||
const { addNotification } = injectNotificationManager()
|
|
||||||
const { serverId } = injectModrinthServerContext()
|
|
||||||
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
|
||||||
hideSubdomainLabel: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const headerImage = computed(() => {
|
|
||||||
if (props.server?.is_medal) {
|
|
||||||
return 'https://cdn-raw.modrinth.com/medal_icon.webp'
|
|
||||||
}
|
|
||||||
return props.serverImage ?? undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const showUptime = computed(() => props.showUptime && (props.uptimeSeconds ?? 0) > 0)
|
|
||||||
|
|
||||||
const formattedUptime = computed(() => {
|
|
||||||
const uptime = props.uptimeSeconds ?? 0
|
|
||||||
const days = Math.floor(uptime / (24 * 3600))
|
|
||||||
const hours = Math.floor((uptime % (24 * 3600)) / 3600)
|
|
||||||
const minutes = Math.floor((uptime % 3600) / 60)
|
|
||||||
const seconds = uptime % 60
|
|
||||||
|
|
||||||
let formatted = ''
|
|
||||||
if (days > 0) formatted += `${days}d `
|
|
||||||
if (hours > 0 || days > 0) formatted += `${hours}h `
|
|
||||||
formatted += `${minutes}m ${seconds}s`
|
|
||||||
return formatted.trim()
|
|
||||||
})
|
|
||||||
|
|
||||||
const showProject = computed(() => !!props.serverProject)
|
|
||||||
|
|
||||||
const serverProjectLink = computed(() => {
|
|
||||||
if (props.serverProjectLink) {
|
|
||||||
return props.serverProjectLink
|
|
||||||
}
|
|
||||||
if (!props.serverProject) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return `/project/${props.serverProject.slug ?? props.serverProject.id}`
|
|
||||||
})
|
|
||||||
|
|
||||||
function copyServerAddress() {
|
|
||||||
if (!props.server?.net?.domain) return
|
|
||||||
navigator.clipboard.writeText(`${props.server.net.domain}.modrinth.gg`)
|
|
||||||
addNotification({
|
|
||||||
title: 'Server address copied',
|
|
||||||
text: "Your server's address has been copied to your clipboard.",
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
|
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
|
||||||
export { default as PanelServerOverflowMenu } from './PanelServerOverflowMenu.vue'
|
|
||||||
export { default as ServerManageHeader } from './ServerManageHeader.vue'
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LeftArrowIcon } from '@modrinth/assets'
|
import { LeftArrowIcon, TagCategoryGamepad2Icon as Gamepad2Icon } from '@modrinth/assets'
|
||||||
|
import type { Component } from 'vue'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import Admonition from '#ui/components/base/Admonition.vue'
|
import PageHeader from '#ui/components/base/page-header/index.vue'
|
||||||
import Avatar from '#ui/components/base/Avatar.vue'
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
|
||||||
import { useServerImage } from '#ui/composables/use-server-image'
|
import { useServerImage } from '#ui/composables/use-server-image'
|
||||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
@@ -18,8 +18,17 @@ const MEDAL_ICON_URL = 'https://cdn-raw.modrinth.com/medal_icon.webp'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
installContext?: BrowseInstallContext | null
|
installContext?: BrowseInstallContext | null
|
||||||
|
divider?: boolean
|
||||||
|
bottomPadding?: boolean
|
||||||
}>()
|
}>()
|
||||||
type SelectedProjectsLeaveResult = 'cancel' | 'discard' | 'install'
|
type SelectedProjectsLeaveResult = 'cancel' | 'discard' | 'install'
|
||||||
|
type BrowseHeaderMetadataItem = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon?: Component
|
||||||
|
iconProps?: Record<string, unknown>
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = injectBrowseManager(null)
|
const ctx = injectBrowseManager(null)
|
||||||
const installContext = computed(() => props.installContext ?? ctx?.installContext?.value ?? null)
|
const installContext = computed(() => props.installContext ?? ctx?.installContext?.value ?? null)
|
||||||
@@ -37,14 +46,66 @@ const iconSrc = computed(() => {
|
|||||||
return fetchedIcon.value ?? installContext.value?.iconSrc ?? null
|
return fetchedIcon.value ?? installContext.value?.iconSrc ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const leadingItems = computed(() => {
|
||||||
|
const context = installContext.value
|
||||||
|
if (!context) return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'back',
|
||||||
|
type: 'button' as const,
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
ariaLabel: context.backLabel,
|
||||||
|
tooltip: context.backLabel,
|
||||||
|
onClick: handleBack,
|
||||||
|
},
|
||||||
|
...(iconSrc.value
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'icon',
|
||||||
|
type: 'avatar' as const,
|
||||||
|
src: iconSrc.value,
|
||||||
|
alt: context.name,
|
||||||
|
avatarSize: '48px',
|
||||||
|
class: 'shrink-0',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const metadataItems = computed(() => {
|
const metadataItems = computed(() => {
|
||||||
const context = installContext.value
|
const context = installContext.value
|
||||||
if (!context) return []
|
if (!context) return []
|
||||||
return [
|
|
||||||
context.heading,
|
const items: BrowseHeaderMetadataItem[] = []
|
||||||
context.gameVersion ? `MC ${context.gameVersion}` : '',
|
if (context.heading) {
|
||||||
context.loader ? formatLoaderLabel(context.loader) : '',
|
items.push({
|
||||||
].filter(Boolean)
|
id: 'heading',
|
||||||
|
label: context.heading,
|
||||||
|
class: '!text-primary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (context.gameVersion) {
|
||||||
|
items.push({
|
||||||
|
id: 'game-version',
|
||||||
|
label: `Minecraft ${context.gameVersion}`,
|
||||||
|
icon: Gamepad2Icon,
|
||||||
|
class: '!text-primary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (context.loader) {
|
||||||
|
const loaderName = formatLoaderLabel(context.loader)
|
||||||
|
const loaderLabel = [loaderName, context.loaderVersion].filter(Boolean).join(' ')
|
||||||
|
items.push({
|
||||||
|
id: 'loader',
|
||||||
|
label: loaderLabel,
|
||||||
|
icon: LoaderIcon,
|
||||||
|
iconProps: { loader: loaderName },
|
||||||
|
class: '!text-primary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedCount = computed(() => installContext.value?.selectedProjects?.length ?? 0)
|
const selectedCount = computed(() => installContext.value?.selectedProjects?.length ?? 0)
|
||||||
@@ -99,39 +160,15 @@ async function handleSelectedProjectsLeaveResult(
|
|||||||
:count="selectedCount"
|
:count="selectedCount"
|
||||||
:installing="isInstallingSelected"
|
:installing="isInstallingSelected"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-2">
|
<PageHeader
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
:title="installContext.name"
|
||||||
<div class="flex min-w-0 items-center gap-4">
|
:leading="leadingItems"
|
||||||
<ButtonStyled circular size="large">
|
:metadata="metadataItems"
|
||||||
<button :aria-label="installContext.backLabel" @click="handleBack">
|
:divider="props.divider ?? false"
|
||||||
<LeftArrowIcon />
|
:bottom-padding="props.bottomPadding ?? false"
|
||||||
</button>
|
main-class="items-center"
|
||||||
</ButtonStyled>
|
title-class="leading-8"
|
||||||
|
truncate-title
|
||||||
<Avatar v-if="iconSrc" :src="iconSrc" size="48px" class="shrink-0" />
|
/>
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-col justify-center gap-1">
|
|
||||||
<h1 class="m-0 truncate text-2xl font-semibold leading-8 text-contrast">
|
|
||||||
{{ installContext.name }}
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
v-if="metadataItems.length"
|
|
||||||
class="flex flex-wrap items-center gap-2 text-base font-medium leading-6 text-primary"
|
|
||||||
>
|
|
||||||
<template v-for="(item, index) in metadataItems" :key="item">
|
|
||||||
<span
|
|
||||||
v-if="index > 0"
|
|
||||||
class="h-1.5 w-1.5 shrink-0 rounded-full bg-current opacity-60"
|
|
||||||
/>
|
|
||||||
<span>{{ item }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Admonition v-if="installContext.warning" type="warning" class="mb-1">
|
|
||||||
{{ installContext.warning }}
|
|
||||||
</Admonition>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -115,68 +115,19 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template v-if="revealState !== 'pending' || isOnboarding">
|
<template v-if="revealState !== 'pending' || isOnboarding">
|
||||||
<ServerManageHeader
|
<div
|
||||||
v-if="!isOnboarding"
|
v-if="!isOnboarding"
|
||||||
:class="['server-stagger-item', containedLayout ? 'shrink-0' : '']"
|
class="w-full flex flex-col gap-4"
|
||||||
|
:class="['server-stagger-item', containedLayout ? 'shrink-0' : '', { 'mt-4': isNuxt }]"
|
||||||
:style="{ '--si': 0 }"
|
:style="{ '--si': 0 }"
|
||||||
:server="serverData"
|
|
||||||
:server-image="serverImage"
|
|
||||||
:server-project="serverProject"
|
|
||||||
:uptime-seconds="showUptime ? uptimeSeconds : undefined"
|
|
||||||
>
|
>
|
||||||
<template #actions>
|
<PageHeader
|
||||||
<div class="flex gap-2">
|
:title="serverData?.name || 'Server'"
|
||||||
<PanelServerActionButton :disabled="!!installError" />
|
:leading="serverHeaderLeading"
|
||||||
<Tooltip
|
:metadata="serverHeaderMetadata"
|
||||||
theme="dismissable-prompt"
|
:actions="serverHeaderActions"
|
||||||
:triggers="[]"
|
/>
|
||||||
:shown="showSettingsHint"
|
</div>
|
||||||
:auto-hide="false"
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<ButtonStyled circular size="large">
|
|
||||||
<button
|
|
||||||
v-tooltip="showSettingsHint ? undefined : 'Server settings'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
openServerSettingsModal()
|
|
||||||
dismissSettingsHint()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<template #popper>
|
|
||||||
<div class="grid grid-cols-[min-content] gap-1">
|
|
||||||
<div class="flex min-w-48 items-center justify-between gap-8">
|
|
||||||
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
|
|
||||||
{{ formatMessage(settingsHintMessages.title) }}
|
|
||||||
</h3>
|
|
||||||
<ButtonStyled size="small" circular>
|
|
||||||
<button
|
|
||||||
v-tooltip="formatMessage(settingsHintMessages.dismiss)"
|
|
||||||
@click="dismissSettingsHint"
|
|
||||||
>
|
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
|
||||||
{{ formatMessage(settingsHintMessages.description) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tooltip>
|
|
||||||
<PanelServerOverflowMenu
|
|
||||||
:disabled="!!installError"
|
|
||||||
:uptime-seconds="uptimeSeconds"
|
|
||||||
:show-copy-id-action="showCopyIdAction"
|
|
||||||
:show-debug-info="showAdvancedDebugInfo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ServerManageHeader>
|
|
||||||
|
|
||||||
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
|
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
|
||||||
|
|
||||||
@@ -350,7 +301,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||||
import { getNodeWebSocketUrl, ModrinthApiError } from '@modrinth/api-client'
|
import { getNodeWebSocketUrl, ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
|
||||||
import {
|
import {
|
||||||
BoxesIcon,
|
BoxesIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
@@ -360,34 +311,35 @@ import {
|
|||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
LayoutTemplateIcon,
|
LayoutTemplateIcon,
|
||||||
|
LinkIcon,
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
|
ServerIcon as ServerAssetIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
|
TimerIcon,
|
||||||
TransferIcon,
|
TransferIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { Tooltip } from 'floating-vue'
|
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
|
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
|
||||||
import NavTabs from '#ui/components/base/NavTabs.vue'
|
import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||||
|
import PageHeader from '#ui/components/base/page-header/index.vue'
|
||||||
import ServerNotice from '#ui/components/base/ServerNotice.vue'
|
import ServerNotice from '#ui/components/base/ServerNotice.vue'
|
||||||
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||||
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
|
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
|
||||||
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||||
|
import ServerIcon from '#ui/components/servers/icons/ServerIcon.vue'
|
||||||
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
|
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
|
||||||
import {
|
import { PanelServerActionButton } from '#ui/components/servers/server-header'
|
||||||
PanelServerActionButton,
|
|
||||||
PanelServerOverflowMenu,
|
|
||||||
ServerManageHeader,
|
|
||||||
} from '#ui/components/servers/server-header'
|
|
||||||
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
|
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
|
||||||
import {
|
import {
|
||||||
hasServerPermission,
|
hasServerPermission,
|
||||||
@@ -508,6 +460,7 @@ const { addNotification } = injectNotificationManager()
|
|||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const constrainWidth = computed(() => props.constrainWidth)
|
const constrainWidth = computed(() => props.constrainWidth)
|
||||||
const containedLayout = computed(() => props.layoutMode === 'contained')
|
const containedLayout = computed(() => props.layoutMode === 'contained')
|
||||||
|
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -527,6 +480,10 @@ const isOnboarding = computed(() => serverData.value?.flows?.intro)
|
|||||||
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
|
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
|
||||||
const settingsHintDismissed = useStorage(SETTINGS_HINT_KEY, false)
|
const settingsHintDismissed = useStorage(SETTINGS_HINT_KEY, false)
|
||||||
const showSettingsHint = ref(!settingsHintDismissed.value)
|
const showSettingsHint = ref(!settingsHintDismissed.value)
|
||||||
|
const serverPreferences = useStorage(`pyro-server-${props.serverId}-preferences`, {
|
||||||
|
hideSubdomainLabel: false,
|
||||||
|
})
|
||||||
|
|
||||||
function dismissSettingsHint() {
|
function dismissSettingsHint() {
|
||||||
showSettingsHint.value = false
|
showSettingsHint.value = false
|
||||||
settingsHintDismissed.value = true
|
settingsHintDismissed.value = true
|
||||||
@@ -702,6 +659,166 @@ const {
|
|||||||
onStateEvent,
|
onStateEvent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const serverHeaderLeading = computed(() => [
|
||||||
|
{
|
||||||
|
id: 'server-icon',
|
||||||
|
type: 'component' as const,
|
||||||
|
component: ServerIcon,
|
||||||
|
componentProps: {
|
||||||
|
image: serverImage.value,
|
||||||
|
},
|
||||||
|
class: isNuxt.value ? 'size-20 !rounded-2xl' : 'size-16 !rounded-xl',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const showServerUptime = computed(() => props.showUptime && uptimeSeconds.value > 0)
|
||||||
|
|
||||||
|
const formattedUptime = computed(() => formatUptime(uptimeSeconds.value))
|
||||||
|
|
||||||
|
const serverProjectLink = computed(() => {
|
||||||
|
if (!serverProject.value) return ''
|
||||||
|
return `/project/${serverProject.value.slug ?? serverProject.value.id}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverHeaderMetadata = computed(() => {
|
||||||
|
const server = serverData.value
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
if (server?.flows?.intro) {
|
||||||
|
items.push({
|
||||||
|
id: 'intro',
|
||||||
|
icon: SettingsIcon,
|
||||||
|
label: 'Configuring server...',
|
||||||
|
class: 'font-semibold',
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server?.loader) {
|
||||||
|
items.push({
|
||||||
|
id: 'loader',
|
||||||
|
icon: LoaderIcon,
|
||||||
|
iconProps: {
|
||||||
|
loader: server.loader,
|
||||||
|
},
|
||||||
|
label: `${formatLoaderLabel(server.loader)} ${server.mc_version}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server?.net?.domain && !serverPreferences.value.hideSubdomainLabel) {
|
||||||
|
items.push({
|
||||||
|
id: 'server-address',
|
||||||
|
icon: LinkIcon,
|
||||||
|
label: `${server.net.domain}.modrinth.gg`,
|
||||||
|
tooltip: 'Copy server address',
|
||||||
|
onClick: copyServerAddress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showServerUptime.value) {
|
||||||
|
items.push({
|
||||||
|
id: 'uptime',
|
||||||
|
icon: TimerIcon,
|
||||||
|
label: formattedUptime.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverProject.value) {
|
||||||
|
items.push({
|
||||||
|
id: 'linked-project',
|
||||||
|
label: 'Linked to',
|
||||||
|
value: serverProject.value.title,
|
||||||
|
valueClass: 'text-primary',
|
||||||
|
avatarSrc: serverProject.value.icon_url,
|
||||||
|
avatarAlt: serverProject.value.title,
|
||||||
|
to: serverProjectLink.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverHeaderActions = computed(() => [
|
||||||
|
{
|
||||||
|
id: 'server-power',
|
||||||
|
label: 'Server power',
|
||||||
|
kind: 'component' as const,
|
||||||
|
component: PanelServerActionButton,
|
||||||
|
componentProps: {
|
||||||
|
disabled: !!installError.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: 'Server settings',
|
||||||
|
icon: SettingsIcon,
|
||||||
|
labelHidden: true,
|
||||||
|
circular: true,
|
||||||
|
tooltip: showSettingsHint.value ? undefined : 'Server settings',
|
||||||
|
onClick: () => openServerSettingsModal(),
|
||||||
|
prompt: {
|
||||||
|
title: formatMessage(settingsHintMessages.title),
|
||||||
|
description: formatMessage(settingsHintMessages.description),
|
||||||
|
dismissLabel: formatMessage(settingsHintMessages.dismiss),
|
||||||
|
shown: showSettingsHint.value,
|
||||||
|
placement: 'bottom-end',
|
||||||
|
onDismiss: dismissSettingsHint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'more',
|
||||||
|
label: 'More server options',
|
||||||
|
icon: MoreVerticalIcon,
|
||||||
|
labelHidden: true,
|
||||||
|
circular: true,
|
||||||
|
type: 'transparent' as const,
|
||||||
|
menuActions: [
|
||||||
|
{
|
||||||
|
id: 'all-servers',
|
||||||
|
label: 'All servers',
|
||||||
|
icon: ServerAssetIcon,
|
||||||
|
action: () => router.push('/hosting/manage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-id',
|
||||||
|
label: 'Copy ID',
|
||||||
|
icon: CopyIcon,
|
||||||
|
action: copyServerId,
|
||||||
|
shown: props.showCopyIdAction,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
function formatUptime(uptime: number) {
|
||||||
|
const days = Math.floor(uptime / (24 * 3600))
|
||||||
|
const hours = Math.floor((uptime % (24 * 3600)) / 3600)
|
||||||
|
const minutes = Math.floor((uptime % 3600) / 60)
|
||||||
|
const seconds = uptime % 60
|
||||||
|
|
||||||
|
let formatted = ''
|
||||||
|
if (days > 0) formatted += `${days}d `
|
||||||
|
if (hours > 0 || days > 0) formatted += `${hours}h `
|
||||||
|
formatted += `${minutes}m ${seconds}s`
|
||||||
|
return formatted.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyServerAddress() {
|
||||||
|
const domain = serverData.value?.net?.domain
|
||||||
|
if (!domain) return
|
||||||
|
|
||||||
|
void navigator.clipboard.writeText(`${domain}.modrinth.gg`)
|
||||||
|
addNotification({
|
||||||
|
title: 'Server address copied',
|
||||||
|
text: "Your server's address has been copied to your clipboard.",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyServerId() {
|
||||||
|
void navigator.clipboard.writeText(props.serverId)
|
||||||
|
}
|
||||||
|
|
||||||
const isUploading = computed(() => uploadState.value.isUploading)
|
const isUploading = computed(() => uploadState.value.isUploading)
|
||||||
const canSetup = computed(() =>
|
const canSetup = computed(() =>
|
||||||
hasServerPermission(serverData.value?.current_user_permissions ?? 0, 'SETUP'),
|
hasServerPermission(serverData.value?.current_user_permissions ?? 0, 'SETUP'),
|
||||||
|
|||||||
@@ -3506,6 +3506,12 @@
|
|||||||
"project.settings.view.title": {
|
"project.settings.view.title": {
|
||||||
"defaultMessage": "View"
|
"defaultMessage": "View"
|
||||||
},
|
},
|
||||||
|
"project.stats.downloads-label": {
|
||||||
|
"defaultMessage": "{count, plural, one {download} other {downloads}}"
|
||||||
|
},
|
||||||
|
"project.stats.followers-label": {
|
||||||
|
"defaultMessage": "{count, plural, one {follower} other {followers}}"
|
||||||
|
},
|
||||||
"project.versions.channel.alpha.symbol": {
|
"project.versions.channel.alpha.symbol": {
|
||||||
"defaultMessage": "A"
|
"defaultMessage": "A"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
||||||
|
|
||||||
import Avatar from '../../components/base/Avatar.vue'
|
|
||||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
|
||||||
import ContentPageHeader from '../../components/base/ContentPageHeader.vue'
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Base/ContentPageHeader',
|
|
||||||
component: ContentPageHeader,
|
|
||||||
} satisfies Meta<typeof ContentPageHeader>
|
|
||||||
|
|
||||||
export default meta
|
|
||||||
type Story = StoryObj<typeof meta>
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
render: () => ({
|
|
||||||
components: { ContentPageHeader, Avatar, ButtonStyled },
|
|
||||||
template: `
|
|
||||||
<ContentPageHeader>
|
|
||||||
<template #icon>
|
|
||||||
<Avatar size="64px" />
|
|
||||||
</template>
|
|
||||||
<template #title>Project Name</template>
|
|
||||||
<template #summary>A brief description of the project goes here.</template>
|
|
||||||
<template #stats>
|
|
||||||
<span>1.2M downloads</span>
|
|
||||||
<span>50K followers</span>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<ButtonStyled color="brand">
|
|
||||||
<button>Follow</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</template>
|
|
||||||
</ContentPageHeader>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WithTitleSuffix: Story = {
|
|
||||||
render: () => ({
|
|
||||||
components: { ContentPageHeader, Avatar, ButtonStyled },
|
|
||||||
template: `
|
|
||||||
<ContentPageHeader>
|
|
||||||
<template #icon>
|
|
||||||
<Avatar size="64px" />
|
|
||||||
</template>
|
|
||||||
<template #title>Featured Project</template>
|
|
||||||
<template #title-suffix>
|
|
||||||
<span class="px-2 py-1 bg-brand-highlight text-brand rounded-full text-sm">Featured</span>
|
|
||||||
</template>
|
|
||||||
<template #summary>This project has been featured by the Modrinth team.</template>
|
|
||||||
<template #actions>
|
|
||||||
<ButtonStyled color="brand">
|
|
||||||
<button>Download</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled type="transparent">
|
|
||||||
<button>Share</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</template>
|
|
||||||
</ContentPageHeader>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
import {
|
||||||
|
AffiliateIcon,
|
||||||
|
BoxIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
HeartIcon,
|
||||||
|
LeftArrowIcon,
|
||||||
|
LinkIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
SlashIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||||
|
TimerIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import AutoLink from '../../components/base/AutoLink.vue'
|
||||||
|
import Avatar from '../../components/base/Avatar.vue'
|
||||||
|
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||||
|
import FormattedTag from '../../components/base/FormattedTag.vue'
|
||||||
|
import JoinedButtons from '../../components/base/JoinedButtons.vue'
|
||||||
|
import PageHeader from '../../components/base/page-header/index.vue'
|
||||||
|
import PageHeaderMetadata from '../../components/base/page-header/metadata/index.vue'
|
||||||
|
import PageHeaderMetadataItem from '../../components/base/page-header/metadata/page-header-metadata-item.vue'
|
||||||
|
import PageHeaderMetadataNumberItem from '../../components/base/page-header/metadata/page-header-metadata-number-item.vue'
|
||||||
|
import PageHeaderMetadataTagsItem from '../../components/base/page-header/metadata/page-header-metadata-tags-item.vue'
|
||||||
|
import PageHeaderMetadataTimeItem from '../../components/base/page-header/metadata/page-header-metadata-time-item.vue'
|
||||||
|
import PageHeaderActions from '../../components/base/page-header/page-header-actions.vue'
|
||||||
|
import PageHeaderBadgeItem from '../../components/base/page-header/page-header-badge-item.vue'
|
||||||
|
import TagItem from '../../components/base/TagItem.vue'
|
||||||
|
import TeleportOverflowMenu from '../../components/base/TeleportOverflowMenu.vue'
|
||||||
|
import LoaderIcon from '../../components/servers/icons/LoaderIcon.vue'
|
||||||
|
import ServerIcon from '../../components/servers/icons/ServerIcon.vue'
|
||||||
|
|
||||||
|
const noop = () => undefined
|
||||||
|
const categories = ['adventure', 'magic', 'technology']
|
||||||
|
const joinedDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 6)
|
||||||
|
const menuActions = [
|
||||||
|
{
|
||||||
|
id: 'open-folder',
|
||||||
|
label: 'Open folder',
|
||||||
|
icon: GlobeIcon,
|
||||||
|
action: noop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-id',
|
||||||
|
label: 'Copy ID',
|
||||||
|
icon: ClipboardCopyIcon,
|
||||||
|
action: noop,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const joinedActions = [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stop',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: noop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: noop,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const pageHeaderIcons = {
|
||||||
|
AffiliateIcon,
|
||||||
|
BoxIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
Gamepad2Icon,
|
||||||
|
GlobeIcon,
|
||||||
|
HeartIcon,
|
||||||
|
LeftArrowIcon,
|
||||||
|
LinkIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
TimerIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageHeaderComponents = {
|
||||||
|
AutoLink,
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
FormattedTag,
|
||||||
|
JoinedButtons,
|
||||||
|
PageHeader,
|
||||||
|
PageHeaderActions,
|
||||||
|
PageHeaderBadgeItem,
|
||||||
|
PageHeaderMetadata,
|
||||||
|
PageHeaderMetadataItem,
|
||||||
|
PageHeaderMetadataNumberItem,
|
||||||
|
PageHeaderMetadataTagsItem,
|
||||||
|
PageHeaderMetadataTimeItem,
|
||||||
|
TagItem,
|
||||||
|
TeleportOverflowMenu,
|
||||||
|
...pageHeaderIcons,
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Base/PageHeader',
|
||||||
|
component: PageHeader,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(story) => ({
|
||||||
|
components: { story },
|
||||||
|
template: '<div class="w-full"><story /></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof PageHeader>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const ProjectHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: pageHeaderComponents,
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
categories,
|
||||||
|
menuActions,
|
||||||
|
noop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="Fabric API" summary="Lightweight and modular API providing common hooks and intercompatibility measures for Fabric mods.">
|
||||||
|
<template #leading>
|
||||||
|
<Avatar src="" alt="Fabric API" size="96px" tint-by="fabric-api" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #badges>
|
||||||
|
<PageHeaderBadgeItem :icon="AffiliateIcon">
|
||||||
|
Featured
|
||||||
|
</PageHeaderBadgeItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataNumberItem :icon="DownloadIcon" :value="128452395" label="downloads" />
|
||||||
|
<PageHeaderMetadataNumberItem :icon="HeartIcon" :value="412300" label="followers" />
|
||||||
|
<PageHeaderMetadataTagsItem>
|
||||||
|
<TagItem v-for="category in categories" :key="category" :action="noop">
|
||||||
|
<FormattedTag :tag="category" />
|
||||||
|
</TagItem>
|
||||||
|
</PageHeaderMetadataTagsItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<ButtonStyled color="brand" size="large">
|
||||||
|
<button type="button" @click="noop">
|
||||||
|
<DownloadIcon />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular size="large" type="transparent">
|
||||||
|
<TeleportOverflowMenu :options="menuActions" aria-label="More actions">
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
</TeleportOverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreatorHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: pageHeaderComponents,
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
joinedDate,
|
||||||
|
noop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="Prospector" summary="A Modrinth creator with a handful of popular projects.">
|
||||||
|
<template #leading>
|
||||||
|
<Avatar src="" alt="Prospector" size="96px" tint-by="Prospector" circle />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #badges>
|
||||||
|
<PageHeaderBadgeItem :icon="AffiliateIcon" class="!border-brand-highlight !bg-brand-highlight !text-brand">
|
||||||
|
Affiliate
|
||||||
|
</PageHeaderBadgeItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataNumberItem :icon="BoxIcon" :value="12" label="projects" />
|
||||||
|
<PageHeaderMetadataNumberItem :icon="DownloadIcon" :value="4200000" label="downloads" />
|
||||||
|
<PageHeaderMetadataTimeItem :icon="CalendarIcon" :date="joinedDate" label="Joined" />
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<ButtonStyled color="brand" size="large">
|
||||||
|
<button type="button" @click="noop">
|
||||||
|
<HeartIcon />
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppInstanceHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: {
|
||||||
|
...pageHeaderComponents,
|
||||||
|
LoaderIcon,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
LoaderIcon,
|
||||||
|
menuActions,
|
||||||
|
noop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="Create: Astral">
|
||||||
|
<template #leading>
|
||||||
|
<Avatar src="" alt="Create: Astral" size="64px" tint-by="create-astral" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">Minecraft 1.20.1</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem :icon="LoaderIcon" :icon-props="{ loader: 'Fabric' }" tooltip="Mod loader">
|
||||||
|
Fabric 0.16.14
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem :icon="TimerIcon" tooltip="Total playtime">12 hours</PageHeaderMetadataItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<ButtonStyled color="brand" size="large">
|
||||||
|
<button type="button" @click="noop">
|
||||||
|
<PlayIcon />
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button type="button" aria-label="Instance settings" @click="noop">
|
||||||
|
<SettingsIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular size="large" type="transparent">
|
||||||
|
<TeleportOverflowMenu :options="menuActions" aria-label="More actions">
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
</TeleportOverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrowseHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: {
|
||||||
|
...pageHeaderComponents,
|
||||||
|
LoaderIcon,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
LoaderIcon,
|
||||||
|
noop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="Survival SMP" :divider="false" :bottom-padding="false" main-class="items-center" title-class="leading-8" truncate-title>
|
||||||
|
<template #leading>
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button type="button" aria-label="Back to instance" @click="noop">
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<Avatar src="" alt="Survival SMP" size="48px" tint-by="survival-smp" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataItem class="!text-primary">Installing content</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem class="!text-primary" :icon="Gamepad2Icon" tooltip="Minecraft version">
|
||||||
|
Minecraft 1.20.1
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem class="!text-primary" :icon="LoaderIcon" :icon-props="{ loader: 'Fabric' }" tooltip="Mod loader">
|
||||||
|
Fabric
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerPanelRootHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: {
|
||||||
|
...pageHeaderComponents,
|
||||||
|
ServerIcon,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
menuActions,
|
||||||
|
noop,
|
||||||
|
serverImage: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="Survival SMP">
|
||||||
|
<template #leading>
|
||||||
|
<ServerIcon class="size-16 !rounded-xl" :image="serverImage" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataItem :icon="GlobeIcon" tooltip="Active instance">My World</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem :icon="LinkIcon" tooltip="Copy server address" :action="noop">
|
||||||
|
play.modrinth.gg
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<ButtonStyled color="brand" size="large">
|
||||||
|
<button type="button" @click="noop">
|
||||||
|
<PlayIcon />
|
||||||
|
Start server
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button type="button" aria-label="Server settings" @click="noop">
|
||||||
|
<SettingsIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerPanelInstanceHeader: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: {
|
||||||
|
...pageHeaderComponents,
|
||||||
|
LoaderIcon,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
joinedActions,
|
||||||
|
LoaderIcon,
|
||||||
|
noop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader title="My World">
|
||||||
|
<template #leading>
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button type="button" aria-label="All instances" @click="noop">
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">Minecraft 1.20.1</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem :icon="LoaderIcon" :icon-props="{ loader: 'Fabric' }" tooltip="Mod loader">
|
||||||
|
Fabric 0.19.2
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
<PageHeaderMetadataItem :icon="TimerIcon" tooltip="Last activity">Last active 2 weeks ago</PageHeaderMetadataItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<PageHeaderActions>
|
||||||
|
<ButtonStyled color="brand" size="large">
|
||||||
|
<button type="button" @click="noop">
|
||||||
|
<PlayIcon />
|
||||||
|
Start instance
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<JoinedButtons :actions="joinedActions" color="red" size="large" />
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button type="button" aria-label="Instance settings" @click="noop">
|
||||||
|
<SettingsIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</PageHeaderActions>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomMetadata: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: pageHeaderComponents,
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
...pageHeaderIcons,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<PageHeader
|
||||||
|
title="Custom Metadata Project"
|
||||||
|
summary="Custom metadata stays in markup so page-specific components can be composed without expanding PageHeader itself."
|
||||||
|
>
|
||||||
|
<template #leading>
|
||||||
|
<Avatar src="" alt="Custom Metadata Project" size="96px" tint-by="custom-metadata-project" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #metadata>
|
||||||
|
<PageHeaderMetadata>
|
||||||
|
<PageHeaderMetadataItem>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2 font-semibold">
|
||||||
|
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||||
|
1.2M
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 font-semibold">
|
||||||
|
<HeartIcon class="h-6 w-6 text-secondary" />
|
||||||
|
50K
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageHeaderMetadataItem>
|
||||||
|
</PageHeaderMetadata>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
Generated
+38
-217
@@ -176,7 +176,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/compat':
|
'@eslint/compat':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.4.1(eslint@9.39.2(jiti@1.21.7))
|
version: 1.4.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
'@formatjs/cli':
|
'@formatjs/cli':
|
||||||
specifier: ^6.2.12
|
specifier: ^6.2.12
|
||||||
version: 6.12.2(@vue/compiler-core@3.5.27)(vue@3.5.27(typescript@5.9.3))
|
version: 6.12.2(@vue/compiler-core@3.5.27)(vue@3.5.27(typescript@5.9.3))
|
||||||
@@ -185,22 +185,22 @@ importers:
|
|||||||
version: link:../../packages/tooling-config
|
version: link:../../packages/tooling-config
|
||||||
'@nuxt/eslint-config':
|
'@nuxt/eslint-config':
|
||||||
specifier: ^0.5.6
|
specifier: ^0.5.6
|
||||||
version: 0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
version: 0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@taijased/vue-render-tracker':
|
'@taijased/vue-render-tracker':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(vue@3.5.27(typescript@5.9.3))
|
version: 1.0.7(vue@3.5.27(typescript@5.9.3))
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.4(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
|
version: 6.0.4(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.19
|
specifier: ^10.4.19
|
||||||
version: 10.4.24(postcss@8.5.6)
|
version: 10.4.24(postcss@8.5.6)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.9.1
|
specifier: ^9.9.1
|
||||||
version: 9.39.2(jiti@1.21.7)
|
version: 9.39.2(jiti@2.6.1)
|
||||||
eslint-plugin-turbo:
|
eslint-plugin-turbo:
|
||||||
specifier: ^2.5.4
|
specifier: ^2.5.4
|
||||||
version: 2.8.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.8.2)
|
version: 2.8.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.2)
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.39
|
specifier: ^8.4.39
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
@@ -218,7 +218,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
|
version: 8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
|
||||||
vue-component-type-helpers:
|
vue-component-type-helpers:
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.2.4
|
version: 3.2.4
|
||||||
@@ -790,7 +790,7 @@ importers:
|
|||||||
version: 5.2.4(vite@5.4.21(@types/node@24.12.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0))(vue@3.5.27(typescript@5.9.3))
|
version: 5.2.4(vite@5.4.21(@types/node@24.12.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0))(vue@3.5.27(typescript@5.9.3))
|
||||||
eslint-plugin-storybook:
|
eslint-plugin-storybook:
|
||||||
specifier: ^10.1.10
|
specifier: ^10.1.10
|
||||||
version: 10.2.4(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
version: 10.2.4(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||||
storybook:
|
storybook:
|
||||||
specifier: ^10.1.10
|
specifier: ^10.1.10
|
||||||
version: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -951,6 +951,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/code-frame@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/compat-data@7.29.0':
|
'@babel/compat-data@7.29.0':
|
||||||
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
|
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1021,6 +1025,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1':
|
'@babel/helper-validator-option@7.27.1':
|
||||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1056,6 +1064,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.28.6':
|
||||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -9941,8 +9953,8 @@ packages:
|
|||||||
vue-component-type-helpers@3.2.4:
|
vue-component-type-helpers@3.2.4:
|
||||||
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
|
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
|
||||||
|
|
||||||
vue-component-type-helpers@3.3.3:
|
vue-component-type-helpers@3.3.5:
|
||||||
resolution: {integrity: sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==}
|
resolution: {integrity: sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==}
|
||||||
|
|
||||||
vue-confetti-explosion@1.0.2:
|
vue-confetti-explosion@1.0.2:
|
||||||
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
|
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
|
||||||
@@ -10456,6 +10468,12 @@ snapshots:
|
|||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
'@babel/code-frame@7.29.7':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
|
||||||
'@babel/compat-data@7.29.0': {}
|
'@babel/compat-data@7.29.0': {}
|
||||||
|
|
||||||
'@babel/core@7.29.0':
|
'@babel/core@7.29.0':
|
||||||
@@ -10562,6 +10580,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1': {}
|
'@babel/helper-validator-option@7.27.1': {}
|
||||||
|
|
||||||
'@babel/helpers@7.28.6':
|
'@babel/helpers@7.28.6':
|
||||||
@@ -10596,6 +10616,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/runtime@7.28.6': {}
|
'@babel/runtime@7.28.6': {}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7': {}
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
@@ -11237,12 +11259,6 @@ snapshots:
|
|||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
'@eslint/compat@1.4.1(eslint@9.39.2(jiti@1.21.7))':
|
|
||||||
dependencies:
|
|
||||||
'@eslint/core': 0.17.0
|
|
||||||
optionalDependencies:
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
|
|
||||||
'@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
|
'@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
@@ -11905,31 +11921,6 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@eslint/js': 9.39.2
|
|
||||||
'@nuxt/eslint-plugin': 0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
eslint-config-flat-gitignore: 0.3.0(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint-flat-config-utils: 0.4.0
|
|
||||||
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint-plugin-jsdoc: 50.8.0(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint-plugin-unicorn: 55.0.0(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint-plugin-vue: 9.33.0(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
globals: 15.15.0
|
|
||||||
local-pkg: 0.5.1
|
|
||||||
pathe: 1.1.2
|
|
||||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@typescript-eslint/utils'
|
|
||||||
- eslint-import-resolver-node
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/js': 9.39.2
|
'@eslint/js': 9.39.2
|
||||||
@@ -11955,15 +11946,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 8.54.0
|
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.54.0
|
'@typescript-eslint/types': 8.54.0
|
||||||
@@ -13454,22 +13436,10 @@ snapshots:
|
|||||||
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.5.27(typescript@5.9.3)
|
vue: 3.5.27(typescript@5.9.3)
|
||||||
vue-component-type-helpers: 3.3.3
|
vue-component-type-helpers: 3.3.5
|
||||||
|
|
||||||
'@stripe/stripe-js@7.9.0': {}
|
'@stripe/stripe-js@7.9.0': {}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
eslint-visitor-keys: 4.2.1
|
|
||||||
espree: 10.4.0
|
|
||||||
estraverse: 5.3.0
|
|
||||||
picomatch: 4.0.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
@@ -13676,8 +13646,8 @@ snapshots:
|
|||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.29.7
|
||||||
'@types/aria-query': 5.0.4
|
'@types/aria-query': 5.0.4
|
||||||
aria-query: 5.3.0
|
aria-query: 5.3.0
|
||||||
dom-accessibility-api: 0.5.16
|
dom-accessibility-api: 0.5.16
|
||||||
@@ -13884,22 +13854,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.12.2
|
'@types/node': 24.12.2
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@eslint-community/regexpp': 4.12.2
|
|
||||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/scope-manager': 8.54.0
|
|
||||||
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/visitor-keys': 8.54.0
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
ignore: 7.0.5
|
|
||||||
natural-compare: 1.4.0
|
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -13916,18 +13870,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/scope-manager': 8.54.0
|
|
||||||
'@typescript-eslint/types': 8.54.0
|
|
||||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/visitor-keys': 8.54.0
|
|
||||||
debug: 4.4.3
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.54.0
|
'@typescript-eslint/scope-manager': 8.54.0
|
||||||
@@ -13958,18 +13900,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 8.54.0
|
|
||||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
debug: 4.4.3
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.54.0
|
'@typescript-eslint/types': 8.54.0
|
||||||
@@ -14140,12 +14070,6 @@ snapshots:
|
|||||||
vite: 7.3.1(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
|
||||||
vue: 3.5.27(typescript@5.9.3)
|
vue: 3.5.27(typescript@5.9.3)
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.4(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
|
|
||||||
dependencies:
|
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
|
||||||
vite: 8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
|
|
||||||
vue: 3.5.27(typescript@5.9.3)
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.4(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
|
'@vitejs/plugin-vue@6.0.4(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||||
@@ -15736,12 +15660,6 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
|
|
||||||
eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@eslint/compat': 1.4.1(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
find-up-simple: 1.0.1
|
|
||||||
|
|
||||||
eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -15763,23 +15681,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
|
|
||||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 8.54.0
|
|
||||||
comment-parser: 1.4.5
|
|
||||||
debug: 4.4.3
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
|
||||||
is-glob: 4.0.3
|
|
||||||
minimatch: 10.1.2
|
|
||||||
semver: 7.7.4
|
|
||||||
stable-hash-x: 0.2.0
|
|
||||||
unrs-resolver: 1.11.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.54.0
|
'@typescript-eslint/types': 8.54.0
|
||||||
@@ -15797,22 +15698,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@es-joy/jsdoccomment': 0.50.2
|
|
||||||
are-docs-informative: 0.0.2
|
|
||||||
comment-parser: 1.4.1
|
|
||||||
debug: 4.4.3
|
|
||||||
escape-string-regexp: 4.0.0
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
espree: 10.4.0
|
|
||||||
esquery: 1.7.0
|
|
||||||
parse-imports-exports: 0.2.4
|
|
||||||
semver: 7.7.4
|
|
||||||
spdx-expression-parse: 4.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@es-joy/jsdoccomment': 0.50.2
|
'@es-joy/jsdoccomment': 0.50.2
|
||||||
@@ -15838,17 +15723,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
||||||
|
|
||||||
eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
'@eslint-community/regexpp': 4.12.2
|
|
||||||
comment-parser: 1.4.5
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
jsdoc-type-pratt-parser: 4.8.0
|
|
||||||
refa: 0.12.1
|
|
||||||
regexp-ast-analysis: 0.7.1
|
|
||||||
scslre: 0.3.0
|
|
||||||
|
|
||||||
eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -15864,47 +15738,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
|
|
||||||
eslint-plugin-storybook@10.2.4(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
|
eslint-plugin-storybook@10.2.4(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
eslint-plugin-turbo@2.8.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.8.2):
|
|
||||||
dependencies:
|
|
||||||
dotenv: 16.0.3
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
turbo: 2.8.2
|
|
||||||
|
|
||||||
eslint-plugin-turbo@2.8.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.2):
|
eslint-plugin-turbo@2.8.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
dotenv: 16.0.3
|
dotenv: 16.0.3
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
turbo: 2.8.2
|
turbo: 2.8.2
|
||||||
|
|
||||||
eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
ci-info: 4.4.0
|
|
||||||
clean-regexp: 1.0.0
|
|
||||||
core-js-compat: 3.48.0
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
esquery: 1.7.0
|
|
||||||
globals: 15.15.0
|
|
||||||
indent-string: 4.0.0
|
|
||||||
is-builtin-module: 3.2.1
|
|
||||||
jsesc: 3.1.0
|
|
||||||
pluralize: 8.0.0
|
|
||||||
read-pkg-up: 7.0.1
|
|
||||||
regexp-tree: 0.1.27
|
|
||||||
regjsparser: 0.10.0
|
|
||||||
semver: 7.7.4
|
|
||||||
strip-indent: 3.0.0
|
|
||||||
|
|
||||||
eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
@@ -15939,20 +15787,6 @@ snapshots:
|
|||||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
|
||||||
eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
globals: 13.24.0
|
|
||||||
natural-compare: 1.4.0
|
|
||||||
nth-check: 2.1.1
|
|
||||||
postcss-selector-parser: 6.1.2
|
|
||||||
semver: 7.7.4
|
|
||||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
|
|
||||||
xml-name-validator: 4.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -20595,7 +20429,7 @@ snapshots:
|
|||||||
|
|
||||||
vue-component-type-helpers@3.2.4: {}
|
vue-component-type-helpers@3.2.4: {}
|
||||||
|
|
||||||
vue-component-type-helpers@3.3.3: {}
|
vue-component-type-helpers@3.3.5: {}
|
||||||
|
|
||||||
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
|
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -20635,19 +20469,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@1.21.7)):
|
|
||||||
dependencies:
|
|
||||||
debug: 4.4.3
|
|
||||||
eslint: 9.39.2(jiti@1.21.7)
|
|
||||||
eslint-scope: 7.2.2
|
|
||||||
eslint-visitor-keys: 3.4.3
|
|
||||||
espree: 9.6.1
|
|
||||||
esquery: 1.7.0
|
|
||||||
lodash: 4.17.23
|
|
||||||
semver: 7.7.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@2.6.1)):
|
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Component Structure
|
||||||
|
|
||||||
|
## Component folders
|
||||||
|
|
||||||
|
Prefer giving non-trivial components their own folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
└── analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── analytics-chart-header.vue
|
||||||
|
├── analytics-chart-plot.vue
|
||||||
|
├── analytics-chart-data.ts
|
||||||
|
└── use-analytics-chart.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
The folder name should match the public component name in kebab case. The main component in that folder should be `index.vue`.
|
||||||
|
|
||||||
|
This keeps imports short:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import AnalyticsChart from '@/components/analytics-chart/index.vue'
|
||||||
|
```
|
||||||
|
|
||||||
|
If the local resolver supports directory indexes, importing the folder is also fine:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import AnalyticsChart from '@/components/analytics-chart/'
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the explicit `index.vue` import when the TypeScript setup cannot resolve the directory import reliably.
|
||||||
|
|
||||||
|
## Local implementation files
|
||||||
|
|
||||||
|
Keep files that only exist to support one component inside that component's folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── analytics-chart-header.vue
|
||||||
|
├── analytics-chart-plot.vue
|
||||||
|
├── analytics-chart-tooltip.vue
|
||||||
|
├── chart-ranges.ts
|
||||||
|
└── use-chart-hover-state.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Good candidates for local files:
|
||||||
|
|
||||||
|
- Small subcomponents used only by the main component
|
||||||
|
- Local composables used only by the main component or its local subcomponents
|
||||||
|
- Helpers that split up a large `<script setup>` block
|
||||||
|
- Types that describe local component state or props
|
||||||
|
|
||||||
|
This is preferred over allowing a single component file to grow into a large, hard-to-review script block.
|
||||||
|
|
||||||
|
## Naming local subcomponents
|
||||||
|
|
||||||
|
Local subcomponents should still have clear names that explain their relationship to the main component:
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── analytics-chart-header.vue
|
||||||
|
└── analytics-chart-plot.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid vague names that make a local component look like a standalone public component:
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── events.vue
|
||||||
|
└── header.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
If a file is local to `analytics-chart`, prefixing it with `analytics-chart-` makes that relationship clear when it appears in search results, editor tabs, and imports.
|
||||||
|
|
||||||
|
## Nesting
|
||||||
|
|
||||||
|
One level of nesting is usually enough.
|
||||||
|
|
||||||
|
Prefer this:
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── analytics-chart-header.vue
|
||||||
|
├── analytics-chart-plot.vue
|
||||||
|
├── use-chart-hover-state.ts
|
||||||
|
└── use-chart-selection.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid this unless a local area has become large enough to justify its own module boundary:
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-chart/
|
||||||
|
├── index.vue
|
||||||
|
├── header/
|
||||||
|
│ └── index.vue
|
||||||
|
└── plot/
|
||||||
|
├── index.vue
|
||||||
|
└── use-plot-state.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Subfolders are fine when they reduce real complexity, but do not create a folder for every small subcomponent by default. Deep nesting makes the file tree harder to scan and often adds duplicated names without improving ownership.
|
||||||
|
|
||||||
|
## When not to use a folder
|
||||||
|
|
||||||
|
Small, leaf components can stay as a single `.vue` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── avatar-stack.vue
|
||||||
|
├── empty-state.vue
|
||||||
|
└── project-status-pill.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Move a component into a folder once it grows local helpers, local composables, or local subcomponents.
|
||||||
|
|
||||||
|
## Public versus local components
|
||||||
|
|
||||||
|
Only the main `index.vue` should be treated as the public entry point for the folder. Other files in the folder are implementation details unless there is a clear reason to import them from outside.
|
||||||
|
|
||||||
|
If a local subcomponent starts being imported elsewhere, either:
|
||||||
|
|
||||||
|
- Promote it into its own component folder
|
||||||
|
- Move it to the nearest shared component area if it is genuinely reusable
|
||||||
|
- Keep it local and pass behavior through the main component if external imports would leak implementation details
|
||||||
Reference in New Issue
Block a user