feat: server management in app (#5628)

* start new server settings tabs

* update properties tab to match design

* better stying in general tab

* feat: add suffix input for hostname field

* implement tables for allocations and DNS records

* add tags for dns record type

* small gap adjustment

* polish advanced page

* adjust properties page hierarchy

* fix searching properties, empty state and projection radius appearing

* pnpm prepr

* update copy to match designs

* fix suffix input component

* style fixes and match heading size

* small fix

* fix search allocations placeholder

* adjust table styles

* move all installation settings helper text to below input

* update icon to use overflow menu buttons

* fix modal to be consistent

* open advanced properties when search

* remove other and custom properties, and update styles

* remove hide/show all java versions

* handle mc 26

* refactor: move server settings pages into /ui and add app ServerSettingsModal

* hook up server pages for app

* add server page header to app

* hook up server settings modal

* use large size

* fix card box shadow style

* fix hostname input for app

* fix app/website card containers

* implement external tabs for billing and admin billing

* fix save banner fixed to parent instead of page body

* remove unused prop to FriendsList causing warning in app

* fix client-only not available for app

* fix bottom cut off

* wire node auth

* implement full copy buttons

* dedup copy button tailwind styles

* fix hover class not working in @apply

* fix spacing

* fix error validation styles

* apply consistent styles and spacing

* feat: update hosting server card (#5609)

* fix type errors

* fix some stylesheets not imported for storybook

* add server listing stories

* add fix for frontend stylesheet imports

* remove props.

* convert copy code to use tailwind

* update server listing component styles

* update server info label styles

* start status/player count info label, more style updates and fixes

* add new server card buttons

* hook up server cards and implement updated styles

* hook up on download button

* fix tauri throwing error when api returns 204 No Content

* hook up purchase server modal in app

* fix upgrading state loading icon

* pnpm prepr

* filter out servers past 30 days after cancellation

* do not apply opacity on lock or spiner icons

* fix disabled server icon background

* update pending change stage

* handle known suspension states

* refactor: reduce code duplication for server listing

* update disabled state text color

* fix loading icon color

* clean up copy

* fix disabled opacity for server card

* update server listing files kept to be countdown

* implement resubscribe modal

* implement proper provisioning state for resubscribe

* fix duplicate attribute and pnpm prepr

* feat: add shared UI package auth DI

* feat: update purchase server flow (#5714)

* implement server list empty state component

* fix stories and adjust spacing

* implement select plan design refresh

* implement auth for empty server list

* use refs instead of reactive

* pnpm prepr

* fix auth usage for empty servers list

* move app auth provider setup to src/providers/setup

* pnpm prepr

* fix max height

* style fix

* fix getCreds no auth is blocking api client

* implement servers guest plan modal and signin which redirects back to modal's next step

* refactor guest plan select logic into provider

* implement sign in or create account popup

* remove force empty serverList

* add download button for suspended mod and generic

* add handling for when user logs out

* QA pass style fixes

* more consistent page styles

* fix duplicate export

* refactor: remove all fallback stuff from resubscribe modal

* implement shared download latest backup util

* i18n pass

* pnpm prepr

* fix region being selected if ping failed

* pnpm prepr

* feat: servers in app finalization (#5744)

* feat: start on shared console implementation into logs and overview pages

* fix: terminal gap issues

* feat: swap word wrap for full screen

* fix: stats cards alignment

* fix: stats

* feat: fix console clear + remove copy

* fix: lint

* fix: use reset not clear

* feat: shared server header & overview page for app and website (#5736)

* feat: implement shared server header for app and website

* feat: implement wrapped overview page with shared composable and hook it up

* pnpm prepr

* fix: bugs

* qa: cleanup

* feat: root.vue shared layout

* feat: delete old options pages + fix discovery frontend

* fix: discovery

* fix: misc style/layout issues

* fix page padding

* fix: modal height jankiness

* feat: implement server install content in app and server setup modal with DI

* fix: spacing

* remove servers in app feature flag

* Revert "remove servers in app feature flag"

This reverts commit 86e284c4bd.

* fix: qa

* feat: remove legacy components from apps/frontend/src/components/ui/servers

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>

* qa pass (#5738)

* fix: qa

* feat: qa

* fix: server icon fetch fails due to global node auth race condition overriding each other

* fix: lint

* fix: server icon upload/sync and centralize logic

* fix: server settings modal not closing for server reset

* fix: better server sorting

* feat: copy address in server listing card

* fix: notification panel in modal and when overlapping with action bar

* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag

* feat: use floating action bar for save banner

* fix: saving state in save bar

* fix: edit server icon styling

* fix: confirm modal to have consistent buttons

* feat: loading animation for server panel + caching improvements for app

* pnpm prepr

* feat: search page deduplication (#5754)

* fix: action bar behind modal

* fix: remove warning modal for stopping

* fix: server cards states

* we hate webkit we hate webkit

* fix: update allocation creation to not use modal

* fix: properties tab spacing and styles

* feat: add files tab copy

* fix: advanced properties icon

* fix: remove back to all servers link

* feat: add files tab link in copy

* fix: server header styles to be consistent with instance

* fix: add header icons back

* feat: update instance settings icon to be consistent

* fix: icon container

* feat: upload state persistence across tabs

* fix: server labels text wrapping

* fix: use surface-5 border

* fix: loading spinner showing with onboarding below

* feat: new server button shows purchase modal in website

* fix: billing page not showing quarterly interval

* fix: server downgrade not showing updated subscription notification

* fix: server settings invalidate saved state and remove server context provider since its already provided in the page

* pnpm prepr

* add stripe publishable key to app build

* feat: console highlighting

* fix: rename servers title to modrinth hosting

* feat: search fix

* fix: qa/styles

* fix: ip click active and remove power dont ask again

* fix: qa

* feat: highlighting fix console

* fix: disable conflicts action

* fix: error dismiss bug

* feat: modal clarification

* fix: files perms issue

* fix: lint

* feat: modal fix

* enable show uptime

* fix: add loading state to edit server icon

* fix: notification panel take in has sidebar from settings

* fix: consistency pass on app settings

* fix: consistency pass on instance settings

* pnpm prepr

* fix: nagivate to billing button in app to go to website

* fix: stripe return url in app causing app to open modrinth.com in tauri

* refactor: better show polling UI code

* fix: new server polling comparison to use server ids instead of length

* fix: buttonstyled story

* fix: button styling

* fix: content.vue regression

* feat: project url redirects

* fix: breadcrumbs

* fix: purchase with newly added card

* fix: console ordering problems

* fix: app-frontend missing env config and staging environment

* fix: log syncing for instances and server panel accidentally

* fix: QA issues

* fix: server page loading state

* fix: stats card logic

* fix: lint

* fix: qa

* fix: console height padding

* fix: terminal padding + loading indicator

* feat: update medal server listing styling

* fix: no upgrade button for medal server listing in app

* fix: go to overview instead of content tab after onboarding

* fix: qa

* fix: teleport modals to body

* fix: logs tab + qa

* fix: local storage for user preferences

* fix: qa loading indic

* feat: considitonal debug and trace

* fix: jump to top on install bug

* feat: swap out server hard drive icon to server stack icon

* feat: servers in app feature flag default true

* fix: highlight row ufll

* fix: webkit thing onto a tag

* fix: input field

* fix: clear fix

* fix: lint

* fix: fmt

* feat: improve share modal and bring it back for sharing log

* pnpm prepr

* fix: menu overflowing

* feat: remove servers in app feature flag

* fix: server stat charts no longer showing color

* fix: library nav no primary state

* fix: better modal height and width

* fix: highlighting bugs

* fix: empty states

* fix: delay import to fix overview page slow load on MacOS

* fix: medal server listing too bright on light mode

* fix: admon analysis + fix logs

* fix: bug

* fix: clear purchase intent from sign-in after closing modal

* performance: improve server manage stats loading by splitting reactivity

* fix: deploy + admon + disable highlighting

* fix: clippy

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* feat: temp wrangler

* fix: lint

* fix: logs upload

* fix: console empty state and admon regressions

* fix: fields

* feat: log deleting + prefetch for Logs.vue

* feat: move delete before share

* feat: clear endpoint

* feat: we ball!

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-04-12 21:38:08 +00:00
committed by GitHub
co-authored by Calum H. tdgao Calum H.
parent a2a97d1313
commit 693a371d61
278 changed files with 15973 additions and 12607 deletions
@@ -0,0 +1,168 @@
<template>
<div
class="medal-promotion relative flex w-full flex-row items-center justify-between rounded-2xl p-4 shadow-xl"
>
<MedalBackgroundImage />
<div class="z-10 mr-2 flex flex-col gap-1">
<Transition
enter-from-class="opacity-0 translate-y-1"
enter-active-class="transition-all duration-300"
enter-to-class="opacity-100 translate-y-0"
leave-from-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-150"
leave-to-class="opacity-0 -translate-y-1"
>
<div
v-if="expiryDate"
class="flex items-center gap-2 whitespace-nowrap font-semibold text-contrast"
>
<ClockIcon class="clock-glow text-medal-orange size-5 shrink-0" />
<span class="w-full text-wrap text-lg">
Your <span class="text-medal-orange">Medal</span>-powered Modrinth Server will expire in
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.days }}</span> days
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.hours }}</span> hours
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.minutes }}</span> minutes
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.seconds }}</span>
seconds.
</span>
</div>
</Transition>
</div>
<ButtonStyled color="medal-promo" type="outlined" size="large">
<button class="z-10 my-auto" @click="openUpgradeModal"><RocketIcon /> Upgrade</button>
</ButtonStyled>
</div>
<ServersUpgradeModalWrapper
ref="upgradeModal"
:stripe-publishable-key="props.stripePublishableKey ?? ''"
:site-url="props.siteUrl ?? ''"
:products="props.products ?? []"
/>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ClockIcon, RocketIcon } from '@modrinth/assets'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import dayjsDuration from 'dayjs/plugin/duration'
import { type ComponentPublicInstance, computed, onMounted, onUnmounted, ref } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import ServersUpgradeModalWrapper from '#ui/components/billing/ServersUpgradeModalWrapper.vue'
import { injectModrinthClient } from '#ui/providers'
import MedalBackgroundImage from './MedalBackgroundImage.vue'
dayjs.extend(dayjsDuration)
type UpgradeWrapperRef = ComponentPublicInstance<{ open: (id?: string) => void | Promise<void> }>
const upgradeModal = ref<UpgradeWrapperRef | null>(null)
const props = defineProps<{
serverId?: string
stripePublishableKey?: string
siteUrl?: string
products?: Labrinth.Billing.Internal.Product[]
}>()
const client = injectModrinthClient()
const { data: subscriptions } = useQuery({
queryKey: ['billing', 'subscriptions'],
queryFn: () => client.labrinth.billing_internal.getSubscriptions(),
})
const expiryDate = computed(() => {
for (const subscription of subscriptions.value || []) {
if (subscription.metadata?.id === props.serverId) {
return dayjs(subscription.created).add(5, 'days')
}
}
return undefined
})
function openUpgradeModal() {
upgradeModal.value?.open(props.serverId)
}
const timeLeftCountdown = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
function updateCountdown() {
if (!expiryDate.value) {
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
return
}
const now = dayjs()
const diff = expiryDate.value.diff(now)
if (diff <= 0) {
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
return
}
const duration = dayjs.duration(diff)
timeLeftCountdown.value = {
days: duration.days(),
hours: duration.hours(),
minutes: duration.minutes(),
seconds: duration.seconds(),
}
}
updateCountdown()
const intervalId = ref<NodeJS.Timeout | null>(null)
onMounted(() => {
intervalId.value = setInterval(updateCountdown, 1000)
})
onUnmounted(() => {
if (intervalId.value) clearInterval(intervalId.value)
})
</script>
<style scoped lang="scss">
.medal-promotion {
position: relative;
border: 1px solid var(--medal-promotion-bg-orange);
background: inherit; // allows overlay + pattern to take over
overflow: hidden;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--medal-promotion-bg-gradient);
z-index: 1;
border-radius: inherit;
}
.background-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
background-color: var(--medal-promotion-bg);
border-radius: inherit;
color: var(--medal-promotion-text-orange);
}
.clock-glow {
filter: drop-shadow(0 0 72px var(--color-orange)) drop-shadow(0 0 36px var(--color-orange))
drop-shadow(0 0 18px var(--color-orange));
}
.text-medal-orange {
color: var(--medal-promotion-text-orange);
font-weight: bold;
}
</style>
@@ -1,127 +1,143 @@
<template>
<div class="rounded-2xl shadow-xl">
<div
class="transition-all"
:class="{
pressable: !isDisabled,
hoverable: !isDisabled,
'cursor-pointer': !isDisabled,
}"
:role="!isDisabled ? 'link' : undefined"
:tabindex="!isDisabled ? 0 : undefined"
@click="navigateToServer"
@keydown.enter.self="navigateToServer"
@keydown.space.prevent.self="navigateToServer"
>
<div
class="medal-promotion flex flex-row items-center overflow-x-hidden rounded-t-2xl p-4 transition-transform duration-100"
:class="status === 'suspended' ? 'rounded-b-none border-b-0 opacity-75' : 'rounded-b-2xl'"
class="medal-promotion flex flex-row items-center overflow-x-hidden rounded-2xl p-4 transition-all duration-150"
:class="{
'!rounded-b-none border-b-0': hasNotice,
'!bg-surface-2': isDisabled,
}"
data-pyro-server-listing
:data-pyro-server-listing-id="server_id"
>
<MedalBackgroundImage />
<AutoLink
:to="status === 'suspended' ? '' : `/hosting/manage/${props.server_id}`"
class="z-10 flex flex-grow flex-row items-center overflow-x-hidden"
:class="status !== 'suspended' && 'active:scale-95'"
<div
v-if="isDisabled"
class="relative z-10 flex size-16 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
>
<Avatar
v-if="status !== 'suspended'"
src="https://cdn-raw.modrinth.com/medal_icon.webp"
size="64px"
class="z-10"
<Avatar src="https://cdn-raw.modrinth.com/medal_icon.webp" size="64px" class="opacity-50" />
<SpinnerIcon
v-if="isUpgrading"
class="size-8 animate-spin absolute text-contrast"
:class="{ 'opacity-50': isDisabled }"
/>
<LockIcon v-else class="size-8 absolute" :class="{ 'opacity-50': isDisabled }" />
</div>
<Avatar v-else src="https://cdn-raw.modrinth.com/medal_icon.webp" size="64px" class="z-10" />
<div class="z-10 ml-4 flex min-w-0 flex-col gap-1.5">
<div class="flex flex-row items-center gap-2">
<h2
class="m-0 truncate text-xl font-bold text-contrast"
:class="{ 'opacity-50': isDisabled }"
>
{{ name }}
</h2>
<span class="truncate" :class="{ 'opacity-50': isDisabled }">
<IntlFormatted
:message-id="messages.countdownRemaining"
:values="{
days: timeLeftCountdown.days,
hours: timeLeftCountdown.hours,
minutes: timeLeftCountdown.minutes,
seconds: timeLeftCountdown.seconds,
}"
>
<template #days-count="{ children }">
<span class="text-medal-orange"><component :is="() => children" /></span>
</template>
<template #hours-count="{ children }">
<span class="text-medal-orange"><component :is="() => children" /></span>
</template>
<template #minutes-count="{ children }">
<span class="text-medal-orange"><component :is="() => children" /></span>
</template>
<template #seconds-count="{ children }">
<span class="text-medal-orange"><component :is="() => children" /></span>
</template>
</IntlFormatted>
</span>
</div>
<div
v-else
class="bg-bg-secondary z-10 flex size-16 shrink-0 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
v-if="projectData?.title"
class="m-0 flex flex-row items-center gap-2 text-sm font-medium"
:class="{ 'opacity-50': isDisabled }"
>
<LockIcon class="size-12 text-secondary" />
</div>
<div class="z-10 ml-4 flex min-w-0 flex-col gap-2.5">
<div class="flex flex-row items-center gap-2">
<h2 class="m-0 truncate text-xl font-bold text-contrast">{{ name }}</h2>
<ChevronRightIcon />
<span class="truncate">
<span class="text-medal-orange">
{{ timeLeftCountdown.days }}
</span>
days
<span class="text-medal-orange">
{{ timeLeftCountdown.hours }}
</span>
hours
<span class="text-medal-orange">
{{ timeLeftCountdown.minutes }}
</span>
minutes
<span class="text-medal-orange">
{{ timeLeftCountdown.seconds }}
</span>
seconds remaining...
</span>
</div>
<div
v-if="projectData?.title"
class="m-0 flex flex-row items-center gap-2 text-sm font-medium text-secondary"
>
<Avatar
:src="iconUrl"
no-shadow
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
alt="Server Icon"
/>
Using {{ projectData?.title || 'Unknown' }}
</div>
<div
v-if="isConfiguring"
class="text-medal-orange flex min-w-0 items-center gap-2 truncate text-sm font-semibold"
>
<SparklesIcon class="size-5 shrink-0" /> New server
</div>
<ServerInfoLabels
v-else
:server-data="{ game, mc_version, loader, loader_version, net }"
:show-game-label="showGameLabel"
:show-loader-label="showLoaderLabel"
:linked="false"
class="pointer-events-none flex w-full flex-row flex-wrap items-center gap-2 text-secondary *:hidden sm:flex-row sm:*:flex"
<Avatar
:src="iconUrl"
no-shadow
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
:alt="formatMessage(messages.serverIconAlt)"
/>
{{ formatMessage(messages.usingProjectLabel, { projectTitle: projectData?.title }) }}
</div>
</AutoLink>
<div v-if="isNuxt" class="z-10 ml-auto">
<div
v-if="isConfiguring"
class="flex min-w-0 items-center gap-2 truncate text-sm font-medium text-blue h-[28px] w-max"
>
<SparklesIcon class="size-5 shrink-0 font-semibold" />
{{ formatMessage(messages.newServerLabel) }}
</div>
<ServerInfoLabels
v-else
:server-data="{ game, mc_version, loader, loader_version, net }"
:server-id="server_id"
:show-game-label="showGameLabel"
:show-loader-label="showLoaderLabel"
:linked="false"
:class="{ 'opacity-50': isDisabled }"
class="flex w-full flex-row flex-wrap items-center gap-2 text-primary *:hidden sm:flex-row sm:*:flex"
/>
</div>
<div class="z-10 ml-auto">
<ButtonStyled color="medal-promo" type="outlined" size="large">
<button class="my-auto" @click="handleUpgrade"><RocketIcon /> Upgrade</button>
<button class="my-auto" data-server-listing-button @click="handleUpgrade">
<RocketIcon /> {{ formatMessage(messages.upgradeButton) }}
</button>
</ButtonStyled>
</div>
</div>
<div
v-if="status === 'suspended' && suspension_reason === 'upgrading'"
class="relative flex w-full flex-row items-center gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-blue bg-bg-blue p-4 text-sm font-bold text-contrast"
class="server-listing-notice"
>
<LoaderCircleIcon class="size-5 animate-spin" />
Your server's hardware is currently being upgraded and will be back online shortly.
<div class="flex gap-2">
{{ formatMessage(messages.upgradingNotice) }}
</div>
</div>
<div
v-else-if="status === 'suspended' && suspension_reason === 'cancelled'"
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
class="server-listing-notice"
>
<div class="flex flex-row gap-2">
<TriangleAlertIcon class="!size-5" /> Your Medal server trial has ended and your server has
been suspended. Please upgrade to continue to use your server.
</div>
<div>{{ formatMessage(messages.medalTrialEndedNotice) }}</div>
</div>
<div
v-else-if="status === 'suspended' && suspension_reason"
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
>
<div class="flex flex-row gap-2">
<TriangleAlertIcon class="!size-5" /> Your server has been suspended:
{{ suspension_reason }}. Please update your billing information or contact Modrinth Support
for more information.
<div v-else-if="status === 'suspended' && suspension_reason" class="server-listing-notice">
<div>
{{
formatMessage(messages.suspendedWithReasonNotice, {
reason: suspension_reason,
})
}}
</div>
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
</div>
<div
v-else-if="status === 'suspended'"
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
>
<div class="flex flex-row gap-2">
<TriangleAlertIcon class="!size-5" /> Your server has been suspended. Please update your
billing information or contact Modrinth Support for more information.
</div>
<div v-else-if="status === 'suspended'" class="server-listing-notice">
<div>{{ formatMessage(messages.suspendedNotice) }}</div>
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
</div>
</div>
@@ -129,25 +145,19 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { NuxtModrinthClient } from '@modrinth/api-client'
import {
ChevronRightIcon,
LoaderCircleIcon,
LockIcon,
RocketIcon,
SparklesIcon,
TriangleAlertIcon,
} from '@modrinth/assets'
import { LockIcon, RocketIcon, SparklesIcon, SpinnerIcon } from '@modrinth/assets'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import dayjsDuration from 'dayjs/plugin/duration'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { injectModrinthClient } from '../../../providers/api-client'
import AutoLink from '../../base/AutoLink.vue'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import CopyCode from '../../base/CopyCode.vue'
import IntlFormatted from '../../base/IntlFormatted.vue'
import ServerInfoLabels from '../labels/ServerInfoLabels.vue'
import MedalBackgroundImage from './MedalBackgroundImage.vue'
@@ -171,13 +181,21 @@ type MedalServerListingProps = {
const props = defineProps<MedalServerListingProps>()
const emit = defineEmits<{ (e: 'upgrade'): void }>()
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const router = useRouter()
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
// const isNuxt = computed(() => client instanceof NuxtModrinthClient)
const showGameLabel = computed(() => !!props.game)
const showLoaderLabel = computed(() => !!props.loader)
const isConfiguring = computed(() => props.flows?.intro)
const isUpgrading = computed(
() => props.status === 'suspended' && props.suspension_reason === 'upgrading',
)
const isDisabled = computed(() => props.status === 'suspended')
const hasNotice = computed(() => props.status === 'suspended')
const { data: projectData } = useQuery({
queryKey: ['server-project', props.server_id, props.upstream?.project_id],
@@ -189,7 +207,50 @@ const { data: projectData } = useQuery({
})
const iconUrl = computed(() => projectData.value?.icon_url || undefined)
const isConfiguring = computed(() => props.flows?.intro)
const messages = defineMessages({
countdownRemaining: {
id: 'servers.medal-listing.countdown.remaining',
defaultMessage:
'<days-count>{days}</days-count> {days, plural, one {day} other {days}} <hours-count>{hours}</hours-count> {hours, plural, one {hour} other {hours}} <minutes-count>{minutes}</minutes-count> {minutes, plural, one {minute} other {minutes}} <seconds-count>{seconds}</seconds-count> {seconds, plural, one {second} other {seconds}} remaining...',
},
serverIconAlt: {
id: 'servers.medal-listing.server-icon-alt',
defaultMessage: 'Server icon',
},
usingProjectLabel: {
id: 'servers.medal-listing.using-project-label',
defaultMessage: 'Using {projectTitle}',
},
newServerLabel: {
id: 'servers.medal-listing.new-server-label',
defaultMessage: 'New server',
},
upgradeButton: {
id: 'servers.medal-listing.upgrade-button',
defaultMessage: 'Upgrade',
},
upgradingNotice: {
id: 'servers.medal-listing.notice.upgrading',
defaultMessage:
"Your server's hardware is currently being upgraded and will be back online shortly.",
},
medalTrialEndedNotice: {
id: 'servers.medal-listing.notice.medal-trial-ended',
defaultMessage:
'Your Medal server trial has ended and your server has been suspended. Please upgrade to continue using your server.',
},
suspendedWithReasonNotice: {
id: 'servers.medal-listing.notice.suspended-with-reason',
defaultMessage:
'Your server has been suspended: {reason}. Please update your billing information or contact Modrinth Support for more information.',
},
suspendedNotice: {
id: 'servers.medal-listing.notice.suspended',
defaultMessage:
'Your server has been suspended. Please update your billing information or contact Modrinth Support for more information.',
},
})
const timeLeftCountdown = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
const expiryDate = computed(() => (props.medal_expires ? dayjs(props.medal_expires) : null))
@@ -199,6 +260,20 @@ function handleUpgrade(event: Event) {
emit('upgrade')
}
function navigateToServer(event: MouseEvent | KeyboardEvent) {
if (isDisabled.value) return
const target = event.target
if (
target instanceof HTMLElement &&
target.closest('[data-subdomain-label], [data-server-listing-button]')
) {
return
}
router.push(`/hosting/manage/${props.server_id}`)
}
function updateCountdown() {
if (!expiryDate.value) {
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
@@ -250,4 +325,17 @@ onUnmounted(() => {
.border-medal-orange {
border-color: var(--medal-promotion-bg-orange);
}
.server-listing-notice {
@apply relative flex w-full rounded-b-2xl border-[1px] border-t-0 border-solid p-4 flex-col gap-4 border-surface-4 bg-bg-raised text-primary;
}
.hoverable:hover:not(:has([data-subdomain-label]:hover, [data-server-listing-button]:hover))
.medal-promotion {
filter: brightness(1.05) saturate(1.1);
}
.pressable:active:not(:has([data-subdomain-label]:active, [data-server-listing-button]:active)) {
transform: scale(0.985);
}
</style>
@@ -1,2 +1,3 @@
export { default as MedalBackgroundImage } from './MedalBackgroundImage.vue'
export { default as MedalServerCountdown } from './MedalServerCountdown.vue'
export { default as MedalServerListing } from './MedalServerListing.vue'