Update Servers marketing page (#3535)

* Update Servers marketing page

* Add burst FAQ

* Updated phrasing again

* Fix servers page when not logged in

* Update changelog
This commit is contained in:
Prospector 2025-04-18 22:23:30 -07:00 committed by GitHub
parent 84a28e045b
commit 1903980b71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 393 additions and 256 deletions

View File

@ -0,0 +1,187 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { RightArrowIcon, SparklesIcon, UnknownIcon } from "@modrinth/assets";
import type { MessageDescriptor } from "@vintl/vintl";
const { formatMessage } = useVIntl();
const emit = defineEmits<{
(e: "select" | "scroll-to-faq"): void;
}>();
type Plan = "small" | "medium" | "large";
const plans: Record<
Plan,
{
buttonColor: "blue" | "green" | "purple";
accentText: string;
accentBg: string;
name: MessageDescriptor;
symbol: MessageDescriptor;
description: MessageDescriptor;
}
> = {
small: {
buttonColor: "blue",
accentText: "text-blue",
accentBg: "bg-bg-blue",
name: defineMessage({
id: "servers.plan.small.name",
defaultMessage: "Small",
}),
symbol: defineMessage({
id: "servers.plan.small.symbol",
defaultMessage: "S",
}),
description: defineMessage({
id: "servers.plan.small.description",
defaultMessage:
"Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.",
}),
},
medium: {
buttonColor: "green",
accentText: "text-green",
accentBg: "bg-bg-green",
name: defineMessage({
id: "servers.plan.medium.name",
defaultMessage: "Medium",
}),
symbol: defineMessage({
id: "servers.plan.medium.symbol",
defaultMessage: "M",
}),
description: defineMessage({
id: "servers.plan.medium.description",
defaultMessage: "Great for modded multiplayer and small communities.",
}),
},
large: {
buttonColor: "purple",
accentText: "text-purple",
accentBg: "bg-bg-purple",
name: defineMessage({
id: "servers.plan.large.name",
defaultMessage: "Large",
}),
symbol: defineMessage({
id: "servers.plan.large.symbol",
defaultMessage: "L",
}),
description: defineMessage({
id: "servers.plan.large.description",
defaultMessage: "Ideal for larger communities, modpacks, and heavy modding.",
}),
},
};
const props = defineProps<{
capacity?: number;
plan: Plan;
ram: number;
storage: number;
cpus: number;
price: number;
}>();
const outOfStock = computed(() => {
return !props.capacity || props.capacity === 0;
});
const lowStock = computed(() => {
return !props.capacity || props.capacity < 8;
});
const formattedRam = computed(() => {
return props.ram / 1024;
});
const formattedStorage = computed(() => {
return props.storage / 1024;
});
const sharedCpus = computed(() => {
return props.cpus / 2;
});
</script>
<template>
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
<div
v-if="lowStock"
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl p-4 text-center font-bold"
:class="outOfStock ? 'bg-bg-red' : 'bg-bg-orange'"
>
<template v-if="outOfStock"> Out of stock! </template>
<template v-else> Only {{ capacity }} left in stock! </template>
</div>
<div
:style="
plan === 'medium'
? {
background: `radial-gradient(
86.12% 101.64% at 95.97% 94.07%,
rgba(27, 217, 106, 0.23) 0%,
rgba(14, 115, 56, 0.2) 100%
)`,
border: `1px solid rgba(12, 107, 52, 0.55)`,
'box-shadow': `0px 12px 38.1px rgba(27, 217, 106, 0.13)`,
}
: undefined
"
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
:class="{ '!rounded-t-none': lowStock }"
>
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
<div
class="grid size-8 place-content-center rounded-full text-xs font-bold"
:class="`${plans[plan].accentBg} ${plans[plan].accentText}`"
>
{{ formatMessage(plans[plan].symbol) }}
</div>
</div>
<p class="m-0">{{ formatMessage(plans[plan].description) }}</p>
<div
class="flex flex-row flex-wrap items-center gap-2 text-nowrap text-secondary xl:justify-between"
>
<p class="m-0">{{ formattedRam }} GB RAM</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">{{ formattedStorage }} GB SSD</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">{{ sharedCpus }} Shared CPUs</p>
</div>
<div class="flex items-center gap-2 text-secondary">
<SparklesIcon /> Bursts up to {{ cpus }} CPUs
<nuxt-link
v-tooltip="
`CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. Click for more info.`
"
to="/servers#cpu-burst"
@click="() => emit('scroll-to-faq')"
>
<UnknownIcon class="h-4 w-4 text-secondary opacity-80" />
</nuxt-link>
</div>
<span class="m-0 text-2xl font-bold text-contrast">
${{ price / 100 }}<span class="text-lg font-semibold text-secondary">/month</span>
</span>
</div>
<ButtonStyled
:color="plans[plan].buttonColor"
:type="plan === 'medium' ? 'standard' : 'highlight-colored-text'"
size="large"
>
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
<button v-else @click="() => emit('select')">
Get Started
<RightArrowIcon class="shrink-0" />
</button>
</ButtonStyled>
</div>
</li>
</template>
<style scoped lang="scss"></style>

View File

@ -10,6 +10,7 @@ interface PyroFetchOptions {
token?: string; token?: string;
}; };
retry?: boolean; retry?: boolean;
bypassAuth?: boolean;
} }
export class PyroFetchError extends Error { export class PyroFetchError extends Error {
@ -28,7 +29,7 @@ export async function usePyroFetch<T>(path: string, options: PyroFetchOptions =
const auth = await useAuth(); const auth = await useAuth();
const authToken = auth.value?.token; const authToken = auth.value?.token;
if (!authToken) { if (!authToken && !options.bypassAuth) {
throw new PyroFetchError("Cannot pyrofetch without auth", 10000); throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
} }
@ -52,9 +53,15 @@ export async function usePyroFetch<T>(path: string, options: PyroFetchOptions =
type HeadersRecord = Record<string, string>; type HeadersRecord = Record<string, string>;
const headers: HeadersRecord = { const authHeader: HeadersRecord = options.bypassAuth
? {}
: {
Authorization: `Bearer ${override?.token ?? authToken}`, Authorization: `Bearer ${override?.token ?? authToken}`,
"Access-Control-Allow-Headers": "Authorization", "Access-Control-Allow-Headers": "Authorization",
};
const headers: HeadersRecord = {
...authHeader,
"User-Agent": "Pyro/1.0 (https://pyro.host)", "User-Agent": "Pyro/1.0 (https://pyro.host)",
Vary: "Accept, Origin", Vary: "Accept, Origin",
"Content-Type": contentType, "Content-Type": contentType,

View File

@ -1043,6 +1043,33 @@
"servers.notices.no-notices": { "servers.notices.no-notices": {
"message": "No notices" "message": "No notices"
}, },
"servers.plan.large.description": {
"message": "Ideal for larger communities, modpacks, and heavy modding."
},
"servers.plan.large.name": {
"message": "Large"
},
"servers.plan.large.symbol": {
"message": "L"
},
"servers.plan.medium.description": {
"message": "Great for modded multiplayer and small communities."
},
"servers.plan.medium.name": {
"message": "Medium"
},
"servers.plan.medium.symbol": {
"message": "M"
},
"servers.plan.small.description": {
"message": "Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding."
},
"servers.plan.small.name": {
"message": "Small"
},
"servers.plan.small.symbol": {
"message": "S"
},
"settings.billing.modal.cancel.action": { "settings.billing.modal.cancel.action": {
"message": "Cancel subscription" "message": "Cancel subscription"
}, },

View File

@ -211,7 +211,7 @@
<h2 class="m-0 text-lg font-bold">Experience modern, reliable hosting</h2> <h2 class="m-0 text-lg font-bold">Experience modern, reliable hosting</h2>
<h3 class="m-0 text-base font-normal text-secondary"> <h3 class="m-0 text-base font-normal text-secondary">
Modrinth Servers are hosted on Modrinth Servers are hosted on
<span class="text-contrast">2023 Ryzen 7/9 CPUs with DDR5 RAM</span>, running on <span class="text-contrast">high-performance AMD CPUs with DDR5 RAM</span>, running on
custom-built software to ensure your server performs smoothly. custom-built software to ensure your server performs smoothly.
</h3> </h3>
</div> </div>
@ -329,27 +329,6 @@
alt="" alt=""
class="absolute -bottom-12 -right-[15%] hidden max-w-2xl rounded-2xl bg-brand p-4 lg:block" class="absolute -bottom-12 -right-[15%] hidden max-w-2xl rounded-2xl bg-brand p-4 lg:block"
/> />
<div class="flex flex-row items-center gap-3">
<div
aria-hidden="true"
class="max-w-fit rounded-full bg-brand p-4 text-sm font-bold text-[var(--color-accent-contrast)] lg:absolute lg:bottom-8 lg:right-8 lg:block"
>
8.49 GB used
</div>
<div
aria-hidden="true"
class="flex w-fit items-center gap-2 rounded-full bg-button-bg p-3 lg:hidden"
>
<SortAscendingIcon class="h-6 w-6" />
Sort
</div>
<div
aria-hidden="true"
class="flex w-fit items-center rounded-full bg-button-bg p-3 lg:hidden"
>
<SearchIcon class="h-6 w-6" />
</div>
</div>
</div> </div>
</div> </div>
<div class="grid w-full grid-cols-1 gap-8 lg:grid-cols-2"> <div class="grid w-full grid-cols-1 gap-8 lg:grid-cols-2">
@ -418,9 +397,24 @@
</span> </span>
What kind of CPUs do Modrinth Servers run on? What kind of CPUs do Modrinth Servers run on?
</summary> </summary>
<p class="m-0 !leading-[190%]"> <p class="m-0 ml-6 leading-[160%]">
Modrinth Servers use 2023 Ryzen 7 and Ryzen 9 CPUs at 4+ GHz, paired with DDR5 Modrinth Servers are powered by AMD Ryzen 7900 and 7950X3D equivalent CPUs at 5+
memory. GHz, paired with with DDR5 memory.
</p>
</details>
<details pyro-hash="cpu-burst" class="group" :open="$route.hash === '#cpu-burst'">
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
<RightArrowIcon />
</span>
How do CPU burst threads work?
</summary>
<p class="m-0 ml-6 leading-[160%]">
When your server is under heavy load, we temporarily give it access to additional
CPU threads to help mitigate lag spikes and instability. This helps prevent the TPS
from going below 20, ensuring the smoothest experience possible. Since those extra
CPU threads are only shortly available during high load periods, they might not show
up in Spark reports or other profiling tools.
</p> </p>
</details> </details>
@ -431,10 +425,12 @@
</span> </span>
Do Modrinth Servers have DDoS protection? Do Modrinth Servers have DDoS protection?
</summary> </summary>
<p class="m-0 !leading-[190%]"> <p class="m-0 ml-6 leading-[160%]">
Yes. All Modrinth Servers come with DDoS protection. Protection is powered by a Yes. All Modrinth Servers come with DDoS protection powered by
combination of in-house network filtering as well as with our data center provider. <a href="https://us.ovhcloud.com/security/anti-ddos/" target="_blank"
Your server is safe on Modrinth. >OVHcloud® Anti-DDoS infrastructure</a
>
which has over 17Tbps capacity. Your server is safe on Modrinth.
</p> </p>
</details> </details>
@ -445,11 +441,9 @@
</span> </span>
Where are Modrinth Servers located? Can I choose a region? Where are Modrinth Servers located? Can I choose a region?
</summary> </summary>
<p class="m-0 !leading-[190%]"> <p class="m-0 ml-6 leading-[160%]">
Currently, Modrinth Servers are located throughout the United States in New York, Currently, Modrinth Servers are located on the east coast of the United States in
Los Angelas, Dallas, Miami, and Spokane. More regions are coming soon! Your server's Vint Hill, Virginia. More regions to come in the future!
location is currently chosen algorithmically, but you will be able to choose a
region in the future.
</p> </p>
</details> </details>
@ -460,7 +454,7 @@
</span> </span>
Can I increase the storage on my server? Can I increase the storage on my server?
</summary> </summary>
<p class="m-0 !leading-[190%]"> <p class="m-0 ml-6 leading-[160%]">
Yes, storage can be increased on your server at no additional cost. If you need more Yes, storage can be increased on your server at no additional cost. If you need more
storage, reach out to Modrinth Support. storage, reach out to Modrinth Support.
</p> </p>
@ -471,13 +465,19 @@
<span class="mr-2 transition-transform duration-200 group-open:rotate-90"> <span class="mr-2 transition-transform duration-200 group-open:rotate-90">
<RightArrowIcon /> <RightArrowIcon />
</span> </span>
How fast are Modrinth Servers? How many players can they handle? How fast are Modrinth Servers?
</summary> </summary>
<p class="m-0 !leading-[190%]"> <p class="m-0 ml-6 leading-[160%]">
During the Modrinth "Emergency SMP" test, we had over 80 players on a server running Modrinth Servers are hosted on very modern high-performance hardware, but it's tough
on the Large plan. The server ran smoothly and was only limited by RAM. We're to say how exactly that will translate into how fast your server will run because
confident that Modrinth Servers can handle a large number of players, with any kind there are so many factors that affect it, such as the mods, data packs, or plugins
of modpack. you're running on your server, and even user behavior.
</p>
<p class="mb-0 ml-6 mt-3 leading-[160%]">
Most performance issues that arise tend to be the fault of an unoptimized modpack,
mod, data pack, or plugin that causes the server to lag. Since our servers are very
high-end, you shouldn't run into much trouble as long as you pick an appropriate
plan for the content you're running on the server.
</p> </p>
</details> </details>
</div> </div>
@ -486,6 +486,7 @@
</section> </section>
<section <section
v-if="false"
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48" class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
> >
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div> <div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
@ -596,182 +597,49 @@
</h2> </h2>
<ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row"> <ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row">
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3"> <ServerPlanSelector
<div :capacity="capacityStatuses?.small?.available"
v-if="isSmallLowStock" plan="small"
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl bg-yellow-500/20 p-4 text-center font-bold" :ram="plans.small.metadata.ram"
> :storage="plans.small.metadata.storage"
Only {{ capacityStatuses?.small?.available }} left in stock! :cpus="plans.small.metadata.cpu"
</div> :price="
<div plans.small?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left" ?.monthly
:class="{ '!rounded-t-none': isSmallLowStock }"
>
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<h1 class="m-0">Small</h1>
<div
class="grid size-8 place-content-center rounded-full bg-highlight-blue text-xs font-bold text-blue"
>
S
</div>
</div>
<p class="m-0">
Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.
</p>
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap">
<p class="m-0">4 GB RAM</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">4 vCPUs</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">32 GB Storage</p>
</div>
<h2 class="m-0 text-3xl text-contrast">
$12<span class="text-sm font-normal text-secondary">/month</span>
</h2>
</div>
<ButtonStyled color="blue" size="large">
<a
v-if="!loggedOut && isSmallAtCapacity"
:href="outOfStockUrl"
target="_blank"
class="flex items-center gap-2 !bg-highlight-blue !font-medium !text-blue"
>
Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-blue" />
</a>
<button
v-else
class="!bg-highlight-blue !font-medium !text-blue"
@click="selectProduct('small')"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-blue" />
</button>
</ButtonStyled>
</div>
</li>
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
<div
v-if="isMediumLowStock"
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl bg-yellow-500/20 p-4 text-center font-bold"
>
Only {{ capacityStatuses?.medium?.available }} left in stock!
</div>
<div
style="
background: radial-gradient(
86.12% 101.64% at 95.97% 94.07%,
rgba(27, 217, 106, 0.23) 0%,
rgba(14, 115, 56, 0.2) 100%
);
border: 1px solid rgba(12, 107, 52, 0.55);
box-shadow: 0px 12px 38.1px rgba(27, 217, 106, 0.13);
" "
class="flex w-full flex-col justify-between gap-4 rounded-2xl p-8 text-left" @select="selectProduct('small')"
:class="{ '!rounded-t-none': isMediumLowStock }" @scroll-to-faq="scrollToFaq()"
> />
<div class="flex flex-col gap-4"> <ServerPlanSelector
<div class="flex flex-row items-center justify-between"> :capacity="capacityStatuses?.medium?.available"
<h1 class="m-0">Medium</h1> plan="medium"
<div :ram="plans.medium.metadata.ram"
class="grid size-8 place-content-center rounded-full bg-highlight-green text-xs font-bold text-brand" :storage="plans.medium.metadata.storage"
> :cpus="plans.medium.metadata.cpu"
M :price="
</div> plans.medium?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
</div> ?.monthly
<p class="m-0">Great for modded multiplayer and small communities.</p> "
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap"> @select="selectProduct('medium')"
<p class="m-0">6 GB RAM</p> @scroll-to-faq="scrollToFaq()"
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div> />
<p class="m-0">6 vCPUs</p> <ServerPlanSelector
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div> :capacity="capacityStatuses?.large?.available"
<p class="m-0">48 GB Storage</p> :ram="plans.large.metadata.ram"
</div> :storage="plans.large.metadata.storage"
<h2 class="m-0 text-3xl text-contrast"> :cpus="plans.large.metadata.cpu"
$18<span class="text-sm font-normal text-secondary">/month</span> :price="
</h2> plans.large?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
</div> ?.monthly
<ButtonStyled color="brand" size="large"> "
<a plan="large"
v-if="!loggedOut && isMediumAtCapacity" @select="selectProduct('large')"
:href="outOfStockUrl" @scroll-to-faq="scrollToFaq()"
target="_blank" />
class="flex items-center gap-2 !bg-highlight-green !font-medium !text-green"
>
Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-green" />
</a>
<button
v-else
class="!bg-highlight-green !font-medium !text-green"
@click="selectProduct('medium')"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-green" />
</button>
</ButtonStyled>
</div>
</li>
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
<div
v-if="isLargeLowStock"
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl bg-yellow-500/20 p-4 text-center font-bold"
>
Only {{ capacityStatuses?.large?.available }} left in stock!
</div>
<div
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
:class="{ '!rounded-t-none': isLargeLowStock }"
>
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<h1 class="m-0">Large</h1>
<div
class="grid size-8 place-content-center rounded-full bg-highlight-purple text-xs font-bold text-purple"
>
L
</div>
</div>
<p class="m-0">Ideal for larger communities, modpacks, and heavy modding.</p>
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap">
<p class="m-0">8 GB RAM</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">8 vCPUs</p>
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
<p class="m-0">64 GB Storage</p>
</div>
<h2 class="m-0 text-3xl text-contrast">
$24<span class="text-sm font-normal text-secondary">/month</span>
</h2>
</div>
<ButtonStyled color="brand" size="large">
<a
v-if="!loggedOut && isLargeAtCapacity"
:href="outOfStockUrl"
target="_blank"
class="flex items-center gap-2 !bg-highlight-purple !font-medium !text-purple"
>
Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-purple" />
</a>
<button
v-else
class="!bg-highlight-purple !font-medium !text-purple"
@click="selectProduct('large')"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-purple" />
</button>
</ButtonStyled>
</div>
</li>
</ul> </ul>
<div <div
class="mb-4 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0" class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<h1 class="m-0">Build your own</h1> <h1 class="m-0">Build your own</h1>
@ -781,11 +649,13 @@
</h2> </h2>
</div> </div>
<div class="flex w-full flex-col-reverse gap-2 md:w-auto md:flex-col md:items-center"> <div
class="experimental-styles-within flex w-full flex-col-reverse gap-2 md:w-auto md:flex-col md:items-center"
>
<ButtonStyled color="standard" size="large"> <ButtonStyled color="standard" size="large">
<button class="w-full md:w-fit" @click="selectProduct('custom')"> <button class="w-full md:w-fit" @click="selectProduct('custom')">
Build your own Build your own
<RightArrowIcon class="!min-h-4 !min-w-4" /> <RightArrowIcon class="shrink-0" />
</button> </button>
</ButtonStyled> </ButtonStyled>
<p class="m-0 text-sm">Starting at $3/GB RAM</p> <p class="m-0 text-sm">Starting at $3/GB RAM</p>
@ -802,9 +672,6 @@ import {
BoxIcon, BoxIcon,
GameIcon, GameIcon,
RightArrowIcon, RightArrowIcon,
SearchIcon,
SortAscendingIcon,
ExternalIcon,
TerminalSquareIcon, TerminalSquareIcon,
TransferIcon, TransferIcon,
VersionIcon, VersionIcon,
@ -813,6 +680,7 @@ import {
import { products } from "~/generated/state.json"; import { products } from "~/generated/state.json";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue"; import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
import Globe from "~/components/ui/servers/Globe.vue"; import Globe from "~/components/ui/servers/Globe.vue";
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
const pyroProducts = products.filter((p) => p.metadata.type === "pyro"); const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
const pyroPlanProducts = pyroProducts.filter( const pyroPlanProducts = pyroProducts.filter(
@ -893,6 +761,7 @@ async function fetchCapacityStatuses(customProduct = null) {
swap_mb: product.metadata.swap, swap_mb: product.metadata.swap,
storage_mb: product.metadata.storage, storage_mb: product.metadata.storage,
}, },
bypassAuth: true,
}), }),
); );
@ -931,21 +800,6 @@ const isMediumAtCapacity = computed(() => capacityStatuses.value?.medium?.availa
const isLargeAtCapacity = computed(() => capacityStatuses.value?.large?.available === 0); const isLargeAtCapacity = computed(() => capacityStatuses.value?.large?.available === 0);
const isCustomAtCapacity = computed(() => capacityStatuses.value?.custom?.available === 0); const isCustomAtCapacity = computed(() => capacityStatuses.value?.custom?.available === 0);
const isSmallLowStock = computed(() => {
const available = capacityStatuses.value?.small?.available;
return available !== undefined && available > 0 && available < 8;
});
const isMediumLowStock = computed(() => {
const available = capacityStatuses.value?.medium?.available;
return available !== undefined && available > 0 && available < 8;
});
const isLargeLowStock = computed(() => {
const available = capacityStatuses.value?.large?.available;
return available !== undefined && available > 0 && available < 8;
});
const startTyping = () => { const startTyping = () => {
const currentWord = words[currentWordIndex.value]; const currentWord = words[currentWordIndex.value];
if (isDeleting.value) { if (isDeleting.value) {

View File

@ -196,7 +196,10 @@
<div class="mt-2 flex flex-col gap-2"> <div class="mt-2 flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 text-brand" /> <CheckCircleIcon class="h-5 w-5 text-brand" />
<span> {{ getPyroProduct(subscription)?.metadata?.cpu }} vCores (CPU) </span> <span>
{{ getPyroProduct(subscription)?.metadata?.cpu / 2 }} Shared CPUs (Bursts up
to {{ getPyroProduct(subscription)?.metadata?.cpu }} CPUs)
</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 text-brand" /> <CheckCircleIcon class="h-5 w-5 text-brand" />

View File

@ -6,7 +6,7 @@ const props = withDefaults(
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple' color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
size?: 'standard' | 'large' size?: 'standard' | 'large'
circular?: boolean circular?: boolean
type?: 'standard' | 'outlined' | 'transparent' | 'highlight' type?: 'standard' | 'outlined' | 'transparent' | 'highlight' | 'highlight-colored-text'
colorFill?: 'auto' | 'background' | 'text' | 'none' colorFill?: 'auto' | 'background' | 'text' | 'none'
hoverColorFill?: 'auto' | 'background' | 'text' | 'none' hoverColorFill?: 'auto' | 'background' | 'text' | 'none'
highlightedStyle?: 'main-nav-primary' | 'main-nav-secondary' highlightedStyle?: 'main-nav-primary' | 'main-nav-secondary'
@ -24,20 +24,40 @@ const props = withDefaults(
}, },
) )
const highlightedColorVar = computed(() => {
switch (props.color) {
case 'brand':
return 'var(--color-brand-highlight)'
case 'red':
return 'var(--color-red-highlight)'
case 'orange':
return 'var(--color-orange-highlight)'
case 'green':
return 'var(--color-green-highlight)'
case 'blue':
return 'var(--color-blue-highlight)'
case 'purple':
return 'var(--color-purple-highlight)'
case 'standard':
default:
return null
}
})
const colorVar = computed(() => { const colorVar = computed(() => {
switch (props.color) { switch (props.color) {
case 'brand': case 'brand':
return props.type === 'highlight' ? 'var(--color-brand-highlight)' : 'var(--color-brand)' return 'var(--color-brand)'
case 'red': case 'red':
return props.type === 'highlight' ? 'var(--color-red-highlight)' : 'var(--color-red)' return 'var(--color-red)'
case 'orange': case 'orange':
return props.type === 'highlight' ? 'var(--color-orange-highlight)' : 'var(--color-orange)' return 'var(--color-orange)'
case 'green': case 'green':
return props.type === 'highlight' ? 'var(--color-green-highlight)' : 'var(--color-green)' return 'var(--color-green)'
case 'blue': case 'blue':
return props.type === 'highlight' ? 'var(--color-blue-highlight)' : 'var(--color-blue)' return 'var(--color-blue)'
case 'purple': case 'purple':
return props.type === 'highlight' ? 'var(--color-purple-highlight)' : 'var(--color-purple)' return 'var(--color-purple)'
case 'standard': case 'standard':
default: default:
return null return null
@ -111,10 +131,14 @@ function setColorFill(
): { bg: string; text: string } { ): { bg: string; text: string } {
if (colorVar.value) { if (colorVar.value) {
if (fill === 'background') { if (fill === 'background') {
colors.bg = colorVar.value if (props.type === 'highlight' && highlightedColorVar.value) {
if (props.type === 'highlight') { colors.bg = highlightedColorVar.value
colors.text = 'var(--color-contrast)' colors.text = 'var(--color-contrast)'
} else if (props.type === 'highlight-colored-text' && highlightedColorVar.value) {
colors.bg = highlightedColorVar.value
colors.text = colorVar.value
} else { } else {
colors.bg = colorVar.value
colors.text = 'var(--color-accent-contrast)' colors.text = 'var(--color-accent-contrast)'
} }
} else if (fill === 'text') { } else if (fill === 'text') {

View File

@ -106,13 +106,16 @@
</p> </p>
<IssuesIcon <IssuesIcon
v-if="customServerConfig.ramInGb < 4" v-if="customServerConfig.ramInGb < 4"
v-tooltip="'This might not be enough resources for your Minecraft server.'" v-tooltip="'This might not be powerful enough for your Minecraft server.'"
class="h-6 w-6 text-orange" class="h-6 w-6 text-orange"
/> />
</div> </div>
<p v-if="existingPlan" class="mt-1 mb-2 text-secondary"> <p v-if="existingPlan" class="mt-1 mb-2 text-secondary">
Your current plan has <strong>{{ existingPlan.metadata.ram / 1024 }} GB RAM</strong> and Your current plan has <strong>{{ existingPlan.metadata.ram / 1024 }} GB RAM</strong> and
<strong>{{ existingPlan.metadata.cpu }} vCPUs</strong>. <strong
>{{ existingPlan.metadata.cpu / 2 }} shared CPUs (bursts up to
{{ existingPlan.metadata.cpu }} CPUs)</strong
>.
</p> </p>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex w-full gap-2 items-center"> <div class="flex w-full gap-2 items-center">
@ -131,12 +134,28 @@
class="flex sm:flex-row flex-col gap-4 w-full" class="flex sm:flex-row flex-col gap-4 w-full"
> >
<div class="flex flex-col w-full gap-2"> <div class="flex flex-col w-full gap-2">
<div class="font-semibold">vCPUs</div> <div class="font-semibold">Shared CPUs</div>
<input v-model="mutatedProduct.metadata.cpu" disabled class="input" /> <input :value="sharedCpus" disabled class="input w-full" />
</div>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold flex items-center gap-1">
Max Burst CPUs
<UnknownIcon
v-tooltip="
'CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. See Modrinth Servers FAQ for more info.'
"
class="h-4 w-4text-secondary opacity-60"
/>
</div>
<input :value="mutatedProduct.metadata.cpu" disabled class="input w-full" />
</div> </div>
<div class="flex flex-col w-full gap-2"> <div class="flex flex-col w-full gap-2">
<div class="font-semibold">Storage</div> <div class="font-semibold">Storage</div>
<input v-model="customServerConfig.storageGbFormatted" disabled class="input" /> <input
v-model="customServerConfig.storageGbFormatted"
disabled
class="input w-full"
/>
</div> </div>
</div> </div>
<Admonition <Admonition
@ -153,10 +172,11 @@
later, or try a different amount. later, or try a different amount.
</Admonition> </Admonition>
<div class="flex items-center gap-2"> <div class="flex gap-2">
<InfoIcon class="hidden sm:block" /> <InfoIcon class="hidden sm:block shrink-0 mt-1" />
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
Storage and vCPUs are currently not configurable. Storage and shared CPU count are currently not configurable independently, and are
based on the amount of RAM you select.
</span> </span>
</div> </div>
</div> </div>
@ -500,6 +520,7 @@
import { ref, computed, nextTick, reactive, watch } from 'vue' import { ref, computed, nextTick, reactive, watch } from 'vue'
import NewModal from '../modal/NewModal.vue' import NewModal from '../modal/NewModal.vue'
import { import {
UnknownIcon,
SpinnerIcon, SpinnerIcon,
CardIcon, CardIcon,
CheckCircleIcon, CheckCircleIcon,
@ -765,7 +786,11 @@ function updateRamValues() {
customMinRam.value = Math.min(...ramValues) customMinRam.value = Math.min(...ramValues)
customMaxRam.value = Math.max(...ramValues) customMaxRam.value = Math.max(...ramValues)
if (props.product.some((product) => product.metadata.ram / 1024 === 4)) {
customServerConfig.ramInGb = 4
} else {
customServerConfig.ramInGb = customMinRam.value customServerConfig.ramInGb = customMinRam.value
}
} }
if (props.customServer) { if (props.customServer) {
@ -832,6 +857,10 @@ const metadata = computed(() => {
return null return null
}) })
const sharedCpus = computed(() => {
return (mutatedProduct.value?.metadata?.cpu ?? 0) / 2
})
function nextStep() { function nextStep() {
if ( if (
mutatedProduct.value.metadata.type === 'pyro' && mutatedProduct.value.metadata.type === 'pyro' &&

View File

@ -10,6 +10,12 @@ export type VersionEntry = {
} }
const VERSIONS: VersionEntry[] = [ const VERSIONS: VersionEntry[] = [
{
date: `2025-04-18T22:30:00-07:00`,
product: 'web',
body: `### Improvements
- Updated Modrinth Servers marketing page to be accurate to post-Pyro infrastructure.`,
},
{ {
date: `2025-04-17T02:25:00-07:00`, date: `2025-04-17T02:25:00-07:00`,
product: 'servers', product: 'servers',