mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
317 lines
9.8 KiB
Vue
317 lines
9.8 KiB
Vue
<template>
|
|
<article
|
|
class="flex min-h-[19.75rem] w-full flex-col overflow-hidden rounded-2xl border border-solid border-surface-5 bg-bg-raised shadow-xl"
|
|
:class="(world as any)?.active ? '!border-brand' : ''"
|
|
>
|
|
<template v-if="world.type === 'empty'">
|
|
<div class="flex flex-1 flex-col items-center pt-[3.125rem] text-center">
|
|
<svg
|
|
class="size-[5.6645rem]"
|
|
viewBox="0 0 91 91"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden="true"
|
|
>
|
|
<rect
|
|
x="22.4356"
|
|
y="0.629395"
|
|
width="71"
|
|
height="71"
|
|
rx="19.5"
|
|
transform="rotate(17.8856 22.4356 0.629395)"
|
|
stroke="#42444A"
|
|
/>
|
|
<rect
|
|
x="4.36354"
|
|
y="16.5661"
|
|
width="71"
|
|
height="71"
|
|
rx="19.5"
|
|
transform="rotate(-8.79487 4.36354 16.5661)"
|
|
fill="#34363C"
|
|
/>
|
|
<rect
|
|
x="4.36354"
|
|
y="16.5661"
|
|
width="71"
|
|
height="71"
|
|
rx="19.5"
|
|
transform="rotate(-8.79487 4.36354 16.5661)"
|
|
stroke="#42444A"
|
|
/>
|
|
<g :clip-path="`url(#${emptyCardClipId})`">
|
|
<path
|
|
d="M47.4227 62.6919C56.5193 61.2846 62.7525 52.7695 61.3452 43.673C59.9378 34.5764 51.4227 28.3432 42.3262 29.7505C33.2297 31.1579 26.9964 39.673 28.4038 48.7695C29.8111 57.866 38.3262 64.0993 47.4227 62.6919Z"
|
|
stroke="#B0BAC5"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<path
|
|
d="M42.3246 29.7502C38.7824 34.8453 37.3358 41.1078 38.2846 47.2402C39.2334 53.3727 42.5048 58.9052 47.4212 62.6916C50.9634 57.5965 52.41 51.3341 51.4612 45.2016C50.5124 39.0691 47.241 33.5366 42.3246 29.7502Z"
|
|
stroke="#B0BAC5"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<path
|
|
d="M28.4023 48.7695L61.3437 43.673"
|
|
stroke="#B0BAC5"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</g>
|
|
<defs>
|
|
<clipPath :id="emptyCardClipId">
|
|
<rect
|
|
width="40"
|
|
height="40"
|
|
fill="white"
|
|
transform="translate(22.0508 29.5137) rotate(-8.79487)"
|
|
/>
|
|
</clipPath>
|
|
</defs>
|
|
</svg>
|
|
<div class="mt-6 flex flex-col gap-1">
|
|
<h2 class="m-0 text-2xl font-semibold leading-8 text-contrast">{{ world.name }}</h2>
|
|
<p class="m-0 text-base leading-6 text-secondary">
|
|
{{ formatMessage(messages.newInstance) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-5 pb-4">
|
|
<ButtonStyled color="brand">
|
|
<button class="w-full !h-10" :disabled="creating" @click="emit('create', world.id)">
|
|
<LoaderCircleIcon v-if="creating" class="animate-spin" aria-hidden="true" />
|
|
<PlusIcon v-else aria-hidden="true" />
|
|
{{ formatMessage(messages.createInstance) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<header class="flex min-h-[5.75rem] flex-col justify-center gap-1 px-5 py-4">
|
|
<div class="flex min-w-0 items-center justify-between gap-3">
|
|
<span
|
|
ref="worldNameRef"
|
|
v-tooltip="truncatedTooltip(worldNameRef, world.name)"
|
|
class="m-0 truncate text-xl font-semibold text-contrast"
|
|
>{{ world.name }}</span
|
|
>
|
|
<span
|
|
v-if="world.active"
|
|
class="shrink-0 rounded-full bg-brand-highlight border border-brand border-solid px-2.5 py-1 text-green"
|
|
>
|
|
{{ formatMessage(messages.active) }}
|
|
</span>
|
|
</div>
|
|
<div class="flex min-w-0 text-md items-center gap-2 text-secondary">
|
|
{{ world.gameVersion }} <BulletDivider /> {{ world.loaderLabel }}
|
|
</div>
|
|
</header>
|
|
|
|
<div
|
|
class="flex flex-1 flex-col gap-3 border-0 border-y bg-surface-2 border-solid border-surface-5 my-auto px-5 py-4"
|
|
>
|
|
<div
|
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,70%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
|
>
|
|
<span>{{ formatMessage(commonMessages.modpackLabel) }}</span>
|
|
<div
|
|
v-if="world.linkedModpack"
|
|
class="flex min-w-0 items-center gap-2 text-right font-semibold leading-5 text-contrast"
|
|
>
|
|
<AutoLink :to="world.linkedModpack.link" class="flex shrink-0 items-center">
|
|
<Avatar
|
|
:src="world.linkedModpack.iconUrl"
|
|
:alt="world.linkedModpack.name"
|
|
:tint-by="world.linkedModpack.name"
|
|
size="1.25rem"
|
|
no-shadow
|
|
/>
|
|
</AutoLink>
|
|
<AutoLink
|
|
:to="world.linkedModpack.link"
|
|
class="flex min-w-0 items-center font-semibold leading-5 text-contrast"
|
|
:class="world.linkedModpack.link ? 'hover:underline' : ''"
|
|
>
|
|
<span
|
|
ref="modpackNameRef"
|
|
v-tooltip="truncatedTooltip(modpackNameRef, world.linkedModpack.name)"
|
|
class="block truncate leading-5"
|
|
>
|
|
{{ world.linkedModpack.name }}
|
|
</span>
|
|
</AutoLink>
|
|
</div>
|
|
<span v-else class="font-semibold text-contrast">{{
|
|
formatMessage(messages.noModpack)
|
|
}}</span>
|
|
</div>
|
|
<div
|
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,25%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
|
>
|
|
<span>{{ formatMessage(messages.installedContent) }}</span>
|
|
<span class="font-semibold text-contrast">{{ installedContentLabel }}</span>
|
|
</div>
|
|
<div
|
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
|
>
|
|
<span>{{ formatMessage(messages.lastActive) }}</span>
|
|
<span class="font-semibold text-contrast">{{ lastActiveLabel }}</span>
|
|
</div>
|
|
<div
|
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
|
>
|
|
<span>{{ formatMessage(messages.created) }}</span>
|
|
<span class="font-semibold text-contrast">{{ createdLabel }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="flex items-center justify-between gap-3 px-5 py-4">
|
|
<ButtonStyled>
|
|
<button class="!shadow-none" @click="emit('edit', world.id)">
|
|
<PencilIcon aria-hidden="true" />
|
|
{{ formatMessage(messages.editInstance) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled circular>
|
|
<button
|
|
v-tooltip="formatMessage(messages.instanceSettings)"
|
|
class="!shadow-none"
|
|
@click="emit('settings', world.id)"
|
|
>
|
|
<Settings2Icon aria-hidden="true" />
|
|
</button>
|
|
</ButtonStyled>
|
|
</footer>
|
|
</template>
|
|
</article>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { LoaderCircleIcon, PencilIcon, PlusIcon, Settings2Icon } from '@modrinth/assets'
|
|
import { capitalizeString } from '@modrinth/utils'
|
|
import { computed, useId, useTemplateRef } from 'vue'
|
|
|
|
import AutoLink from '#ui/components/base/AutoLink.vue'
|
|
import Avatar from '#ui/components/base/Avatar.vue'
|
|
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
|
import { useFormatDateTime, useRelativeTime } from '#ui/composables'
|
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
|
import { truncatedTooltip } from '#ui/utils'
|
|
import { commonMessages } from '#ui/utils/common-messages'
|
|
|
|
const messages = defineMessages({
|
|
newInstance: {
|
|
id: 'servers.manage.instances.card.empty-description',
|
|
defaultMessage: 'New instance',
|
|
},
|
|
createInstance: {
|
|
id: 'servers.manage.instances.card.create',
|
|
defaultMessage: 'Create instance',
|
|
},
|
|
active: {
|
|
id: 'servers.manage.instances.card.active',
|
|
defaultMessage: 'Active',
|
|
},
|
|
noModpack: {
|
|
id: 'servers.manage.instances.card.no-modpack',
|
|
defaultMessage: '—',
|
|
},
|
|
installedContent: {
|
|
id: 'servers.manage.instances.card.installed-content',
|
|
defaultMessage: 'Installed content',
|
|
},
|
|
lastActive: {
|
|
id: 'servers.manage.instances.card.last-active',
|
|
defaultMessage: 'Last active',
|
|
},
|
|
created: {
|
|
id: 'servers.manage.instances.card.created',
|
|
defaultMessage: 'Created',
|
|
},
|
|
editInstance: {
|
|
id: 'servers.manage.instances.card.edit',
|
|
defaultMessage: 'Edit instance',
|
|
},
|
|
instanceSettings: {
|
|
id: 'servers.manage.instances.card.settings',
|
|
defaultMessage: 'Instance settings',
|
|
},
|
|
notTrackedYet: {
|
|
id: 'servers.manage.instances.card.not-tracked-yet',
|
|
defaultMessage: 'Not tracked yet',
|
|
},
|
|
})
|
|
|
|
type LinkedModpack = {
|
|
name: string
|
|
iconUrl: string | null
|
|
link: string | null
|
|
}
|
|
|
|
type UsedWorld = {
|
|
type: 'world'
|
|
id: string
|
|
name: string
|
|
active: boolean
|
|
gameVersion: string | null
|
|
loaderLabel: string | null
|
|
linkedModpack: LinkedModpack | null
|
|
installedContentCount: number | null
|
|
lastActiveAt: string | null
|
|
createdAt: string | null
|
|
}
|
|
|
|
type EmptyWorld = {
|
|
type: 'empty'
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
world: UsedWorld | EmptyWorld
|
|
creating?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
create: [slotId: string]
|
|
edit: [worldId: string]
|
|
settings: [worldId: string]
|
|
}>()
|
|
|
|
const formatRelativeTime = useRelativeTime()
|
|
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
|
|
const { formatMessage } = useVIntl()
|
|
const emptyCardClipId = useId()
|
|
|
|
const modpackNameRef = useTemplateRef<HTMLElement>('modpackNameRef')
|
|
const worldNameRef = useTemplateRef<HTMLElement>('worldNameRef')
|
|
|
|
const installedContentLabel = computed(() => {
|
|
if (props.world.type === 'empty') return ''
|
|
return props.world.installedContentCount === null
|
|
? formatMessage(commonMessages.unknownLabel)
|
|
: String(props.world.installedContentCount)
|
|
})
|
|
|
|
const lastActiveLabel = computed(() => {
|
|
if (props.world.type === 'empty') return ''
|
|
return props.world.lastActiveAt
|
|
? capitalizeString(formatRelativeTime(props.world.lastActiveAt))
|
|
: formatMessage(messages.notTrackedYet)
|
|
})
|
|
|
|
const createdLabel = computed(() => {
|
|
if (props.world.type === 'empty') return ''
|
|
return props.world.createdAt
|
|
? formatDate(props.world.createdAt)
|
|
: formatMessage(commonMessages.unknownLabel)
|
|
})
|
|
</script>
|