feat(servers): full custom plan capacity checks & more (#2911)

* feat(servers): improve plan button logic

* feat(servers): custom plan capacity checks

* feat(servers): custom plan dynamic ram values

* feat(servers): add custom plan selector back

* fix(servers): final fixes
This commit is contained in:
he3als
2024-11-05 02:07:40 +00:00
committed by GitHub
parent 2b44b145cb
commit d321843c02
2 changed files with 187 additions and 119 deletions

View File

@@ -22,6 +22,8 @@
:payment-methods="paymentMethods" :payment-methods="paymentMethods"
:return-url="`${config.public.siteUrl}/servers/manage`" :return-url="`${config.public.siteUrl}/servers/manage`"
:server-name="`${auth?.user?.username}'s server`" :server-name="`${auth?.user?.username}'s server`"
:fetch-capacity-statuses="fetchCapacityStatuses"
:out-of-stock-url="outOfStockUrl"
@hidden="handleModalHidden" @hidden="handleModalHidden"
/> />
@@ -523,29 +525,34 @@
$12<span class="text-sm font-normal text-secondary">/month</span> $12<span class="text-sm font-normal text-secondary">/month</span>
</h2> </h2>
<ButtonStyled color="blue" size="large"> <ButtonStyled color="blue" size="large">
<button
v-if="!isSmallAtCapacity"
class="!bg-highlight-blue !font-medium !text-blue"
@click="selectProduct(pyroPlanProducts[0])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-blue" />
</button>
<NuxtLink <NuxtLink
v-else v-if="loggedOut"
:to="loggedOut ? redirectUrl : 'https://support.modrinth.com'" :to="loginUrl"
:target="loggedOut ? '_self' : '_blank'" target="_self"
class="!bg-highlight-blue !font-medium !text-blue" class="!bg-highlight-blue !font-medium !text-blue"
> >
<template v-if="loggedOut"> Login
Login <UserIcon class="!min-h-4 !min-w-4 !text-blue" />
<UserIcon class="!min-h-4 !min-w-4 !text-blue" /> </NuxtLink>
</template> <template v-else>
<template v-else> <NuxtLink
v-if="isSmallAtCapacity"
:to="outOfStockUrl"
target="_blank"
class="!bg-highlight-blue !font-medium !text-blue"
>
Out of Stock Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-blue" /> <ExternalIcon class="!min-h-4 !min-w-4 !text-blue" />
</template> </NuxtLink>
</NuxtLink> <button
v-else
class="!bg-highlight-blue !font-medium !text-blue"
@click="selectProduct(pyroPlanProducts[0])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-blue" />
</button>
</template>
</ButtonStyled> </ButtonStyled>
</li> </li>
@@ -581,29 +588,34 @@
$18<span class="text-sm font-normal text-secondary">/month</span> $18<span class="text-sm font-normal text-secondary">/month</span>
</h2> </h2>
<ButtonStyled color="brand" size="large"> <ButtonStyled color="brand" size="large">
<button
v-if="!isMediumAtCapacity"
class="shadow-xl"
@click="selectProduct(pyroPlanProducts[1])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4" />
</button>
<NuxtLink <NuxtLink
v-else v-if="loggedOut"
:to="loggedOut ? redirectUrl : 'https://support.modrinth.com'" :to="loginUrl"
:target="loggedOut ? '_self' : '_blank'" target="_self"
class="!bg-highlight-green !font-medium !text-green" class="!bg-highlight-green !font-medium !text-green"
> >
<template v-if="loggedOut"> Login
Login <UserIcon class="!min-h-4 !min-w-4 !text-green" />
<UserIcon class="!min-h-4 !min-w-4 !text-green" /> </NuxtLink>
</template> <template v-else>
<template v-else> <NuxtLink
v-if="isMediumAtCapacity"
:to="outOfStockUrl"
target="_blank"
class="!bg-highlight-green !font-medium !text-green"
>
Out of Stock Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-green" /> <ExternalIcon class="!min-h-4 !min-w-4 !text-green" />
</template> </NuxtLink>
</NuxtLink> <button
v-else
class="!bg-highlight-green !font-medium !text-green"
@click="selectProduct(pyroPlanProducts[1])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-green" />
</button>
</template>
</ButtonStyled> </ButtonStyled>
</li> </li>
@@ -628,34 +640,39 @@
$24<span class="text-sm font-normal text-secondary">/month</span> $24<span class="text-sm font-normal text-secondary">/month</span>
</h2> </h2>
<ButtonStyled color="purple" size="large"> <ButtonStyled color="purple" size="large">
<button
v-if="!isLargeAtCapacity"
class="!bg-highlight-purple !font-medium !text-purple"
@click="selectProduct(pyroPlanProducts[2])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-purple" />
</button>
<NuxtLink <NuxtLink
v-else v-if="loggedOut"
:to="loggedOut ? redirectUrl : 'https://support.modrinth.com'" :to="loginUrl"
:target="loggedOut ? '_self' : '_blank'" target="_self"
class="!bg-highlight-purple !font-medium !text-purple" class="!bg-highlight-purple !font-medium !text-purple"
> >
<template v-if="loggedOut"> Login
Login <UserIcon class="!min-h-4 !min-w-4 !text-purple" />
<UserIcon class="!min-h-4 !min-w-4 !text-purple" /> </NuxtLink>
</template> <template v-else>
<template v-else> <NuxtLink
v-if="isLargeAtCapacity"
:to="outOfStockUrl"
target="_blank"
class="!bg-highlight-purple !font-medium !text-purple"
>
Out of Stock Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4 !text-purple" /> <ExternalIcon class="!min-h-4 !min-w-4 !text-purple" />
</template> </NuxtLink>
</NuxtLink> <button
v-else
class="!bg-highlight-purple !font-medium !text-purple"
@click="selectProduct(pyroPlanProducts[2])"
>
Get Started
<RightArrowIcon class="!min-h-4 !min-w-4 !text-purple" />
</button>
</template>
</ButtonStyled> </ButtonStyled>
</li> </li>
</ul> </ul>
<!-- <div <div
class="flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left md:flex-row md:gap-0" class="flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left md:flex-row md:gap-0"
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@@ -668,33 +685,18 @@
<div class="flex w-full flex-col-reverse gap-2 md:w-auto md:flex-col md:items-center"> <div class="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 <NuxtLink v-if="loggedOut" :to="loginUrl" target="_self" class="w-full md:w-fit">
v-if="!isLargeAtCapacity" Login
class="w-full md:w-fit" <UserIcon class="!min-h-4 !min-w-4" />
@click="selectProduct(pyroProducts, true)" </NuxtLink>
> <button v-else class="w-full md:w-fit" @click="selectProduct(pyroProducts, true)">
Build your own Build your own
<RightArrowIcon class="!min-h-4 !min-w-4" /> <RightArrowIcon class="!min-h-4 !min-w-4" />
</button> </button>
<NuxtLink
v-else
:to="loggedOut ? redirectUrl : 'https://support.modrinth.com'"
:target="loggedOut ? '_self' : '_blank'"
class="w-full md:w-fit"
>
<template v-if="loggedOut">
Login
<UserIcon class="!min-h-4 !min-w-4" />
</template>
<template v-else>
Out of Stock
<ExternalIcon class="!min-h-4 !min-w-4" />
</template>
</NuxtLink>
</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>
</div> </div>
</div> --> </div>
</div> </div>
</section> </section>
</div> </div>
@@ -770,7 +772,8 @@ const deletingSpeed = 25;
const pauseTime = 2000; const pauseTime = 2000;
const loggedOut = computed(() => !auth.value.user); const loggedOut = computed(() => !auth.value.user);
const redirectUrl = `/auth/sign-in?redirect=${encodeURIComponent("/servers#plan")}`; const loginUrl = `/auth/sign-in?redirect=${encodeURIComponent("/servers#plan")}`;
const outOfStockUrl = "https://support.modrinth.com";
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => { const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
try { try {
@@ -782,37 +785,47 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
} }
}); });
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData( async function fetchCapacityStatuses(customProduct = null) {
"ServerCapacityAll", try {
async () => { const productsToCheck = customProduct?.metadata ? [customProduct] : pyroPlanProducts;
try { const capacityChecks = productsToCheck.map((product) =>
const capacityChecks = pyroPlanProducts.map((product) => usePyroFetch("capacity", {
usePyroFetch("capacity", { method: "POST",
method: "POST", body: {
body: { cpu: product.metadata.cpu,
cpu: product.metadata.cpu, memory_mb: product.metadata.ram,
memory_mb: product.metadata.ram, swap_mb: product.metadata.swap,
swap_mb: product.metadata.swap, storage_mb: product.metadata.storage,
storage_mb: product.metadata.storage, },
}, }),
}), );
);
const results = await Promise.all(capacityChecks); const results = await Promise.all(capacityChecks);
if (customProduct?.metadata) {
return {
custom: results[0],
};
} else {
return { return {
small: results[0], small: results[0],
medium: results[1], medium: results[1],
large: results[2], large: results[2],
}; };
} catch (error) {
console.error("Error checking server capacities:", error);
return {
small: { available: 0 },
medium: { available: 0 },
large: { available: 0 },
};
} }
}, } catch (error) {
console.error("Error checking server capacities:", error);
return {
custom: { available: 0 },
small: { available: 0 },
medium: { available: 0 },
large: { available: 0 },
};
}
}
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
"ServerCapacityAll",
fetchCapacityStatuses,
); );
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0); const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);
@@ -931,7 +944,7 @@ const selectProduct = async (product, custom) => {
} }
if (!auth.value.user) { if (!auth.value.user) {
data.$router.push(redirectUrl); data.$router.push(loginUrl);
return; return;
} }

View File

@@ -109,14 +109,17 @@
<Slider <Slider
v-model="customServerConfig.ramInGb" v-model="customServerConfig.ramInGb"
class="fix-slider" class="fix-slider"
:min="2" :min="customMinRam"
:max="12" :max="customMaxRam"
:step="2" :step="2"
unit="GB" unit="GB"
/> />
<div class="font-semibold text-nowrap"></div> <div class="font-semibold text-nowrap"></div>
</div> </div>
<div v-if="customMatchingProduct" class="flex sm:flex-row flex-col gap-4 w-full"> <div
v-if="customMatchingProduct && !customOutOfStock"
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">vCPUs</div>
<input v-model="mutatedProduct.metadata.cpu" disabled class="input" /> <input v-model="mutatedProduct.metadata.cpu" disabled class="input" />
@@ -134,7 +137,15 @@
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<InfoIcon class="hidden flex-none h-8 w-8 text-blue sm:block" /> <InfoIcon class="hidden flex-none h-8 w-8 text-blue sm:block" />
<div class="flex flex-col gap-2"> <div v-if="customOutOfStock && customMatchingProduct" class="flex flex-col gap-2">
<div class="font-semibold">This plan is currently out of stock</div>
<div class="font-normal">
We are currently
<a :href="outOfStockUrl" class="underline" target="_blank">out of capacity</a>
for your selected RAM amount. Please try again later, or try a different amount.
</div>
</div>
<div v-else class="flex flex-col gap-2">
<div class="font-semibold">We can't seem to find your selected plan</div> <div class="font-semibold">We can't seem to find your selected plan</div>
<div class="font-normal"> <div class="font-normal">
We are currently unable to find a server for your selected RAM amount. Please We are currently unable to find a server for your selected RAM amount. Please
@@ -383,7 +394,7 @@
:disabled=" :disabled="
paymentLoading || paymentLoading ||
(mutatedProduct.metadata.type === 'pyro' && !projectId && !serverName) || (mutatedProduct.metadata.type === 'pyro' && !projectId && !serverName) ||
(customServer && !customMatchingProduct) customAllowedToContinue
" "
@click="nextStep" @click="nextStep"
> >
@@ -545,6 +556,16 @@ const props = defineProps({
type: Boolean, type: Boolean,
required: false, required: false,
}, },
fetchCapacityStatuses: {
type: Function,
required: false,
default: null,
},
outOfStockUrl: {
type: String,
required: false,
default: '',
},
}) })
const messages = defineMessages({ const messages = defineMessages({
@@ -631,7 +652,16 @@ const serverLoader = ref('Vanilla')
const eulaAccepted = ref(false) const eulaAccepted = ref(false)
const mutatedProduct = ref({ ...props.product }) const mutatedProduct = ref({ ...props.product })
const customMinRam = ref(0)
const customMaxRam = ref(0)
const customMatchingProduct = ref() const customMatchingProduct = ref()
const customOutOfStock = ref(false)
const customLoading = ref(true)
const customAllowedToContinue = computed(
() =>
props.customServer &&
(!customMatchingProduct.value || customLoading.value || customOutOfStock.value),
)
const customServerConfig = reactive({ const customServerConfig = reactive({
ramInGb: 4, ramInGb: 4,
@@ -640,23 +670,48 @@ const customServerConfig = reactive({
}) })
const updateCustomServerProduct = () => { const updateCustomServerProduct = () => {
if (props.customServer) { customMatchingProduct.value = props.product.find(
customMatchingProduct.value = props.product.find( (product) => product.metadata.ram === customServerConfig.ram,
(product) => product.metadata.ram === customServerConfig.ram, )
)
if (customMatchingProduct.value) mutatedProduct.value = { ...customMatchingProduct.value } if (customMatchingProduct.value) mutatedProduct.value = { ...customMatchingProduct.value }
}
let updateCustomServerStockTimeout = null
const updateCustomServerStock = async () => {
if (updateCustomServerStockTimeout) {
clearTimeout(updateCustomServerStockTimeout)
customLoading.value = true
} }
updateCustomServerStockTimeout = setTimeout(async () => {
if (props.fetchCapacityStatuses) {
const capacityStatus = await props.fetchCapacityStatuses(mutatedProduct.value)
if (capacityStatus.custom?.available === 0) {
customOutOfStock.value = true
} else {
customOutOfStock.value = false
}
customLoading.value = false
} else {
console.error('No fetchCapacityStatuses function provided.')
customOutOfStock.value = true
}
}, 300)
} }
if (props.customServer) { if (props.customServer) {
updateCustomServerProduct() const ramValues = props.product.map((product) => product.metadata.ram / 1024)
watch( customMinRam.value = Math.min(...ramValues)
() => customServerConfig.ram, customMaxRam.value = Math.max(...ramValues)
() => {
updateCustomServerProduct() const updateProductAndStock = () => {
}, updateCustomServerProduct()
) updateCustomServerStock()
}
updateProductAndStock()
watch(() => customServerConfig.ram, updateProductAndStock)
} }
const selectedPaymentMethod = ref() const selectedPaymentMethod = ref()