diff --git a/apps/frontend/src/pages/servers/index.vue b/apps/frontend/src/pages/servers/index.vue
index f5dbb3171..371538c19 100644
--- a/apps/frontend/src/pages/servers/index.vue
+++ b/apps/frontend/src/pages/servers/index.vue
@@ -22,6 +22,8 @@
:payment-methods="paymentMethods"
:return-url="`${config.public.siteUrl}/servers/manage`"
:server-name="`${auth?.user?.username}'s server`"
+ :fetch-capacity-statuses="fetchCapacityStatuses"
+ :out-of-stock-url="outOfStockUrl"
@hidden="handleModalHidden"
/>
@@ -523,29 +525,34 @@
$12/month
-
-
- Login
-
-
-
+ Login
+
+
+
+
Out of Stock
-
-
+
+
+
@@ -581,29 +588,34 @@
$18/month
-
-
- Login
-
-
-
+ Login
+
+
+
+
Out of Stock
-
-
+
+
+
@@ -628,34 +640,39 @@
$24/month
-
-
- Login
-
-
-
+ Login
+
+
+
+
Out of Stock
-
-
+
+
+
-
+
@@ -770,7 +772,8 @@ const deletingSpeed = 25;
const pauseTime = 2000;
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 () => {
try {
@@ -782,37 +785,47 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
}
});
-const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
- "ServerCapacityAll",
- async () => {
- try {
- const capacityChecks = pyroPlanProducts.map((product) =>
- usePyroFetch("capacity", {
- method: "POST",
- body: {
- cpu: product.metadata.cpu,
- memory_mb: product.metadata.ram,
- swap_mb: product.metadata.swap,
- storage_mb: product.metadata.storage,
- },
- }),
- );
+async function fetchCapacityStatuses(customProduct = null) {
+ try {
+ const productsToCheck = customProduct?.metadata ? [customProduct] : pyroPlanProducts;
+ const capacityChecks = productsToCheck.map((product) =>
+ usePyroFetch("capacity", {
+ method: "POST",
+ body: {
+ cpu: product.metadata.cpu,
+ memory_mb: product.metadata.ram,
+ swap_mb: product.metadata.swap,
+ 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 {
small: results[0],
medium: results[1],
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);
@@ -931,7 +944,7 @@ const selectProduct = async (product, custom) => {
}
if (!auth.value.user) {
- data.$router.push(redirectUrl);
+ data.$router.push(loginUrl);
return;
}
diff --git a/packages/ui/src/components/billing/PurchaseModal.vue b/packages/ui/src/components/billing/PurchaseModal.vue
index 9b0b901ca..ebacc207f 100644
--- a/packages/ui/src/components/billing/PurchaseModal.vue
+++ b/packages/ui/src/components/billing/PurchaseModal.vue
@@ -109,14 +109,17 @@
-
+
vCPUs
@@ -134,7 +137,15 @@
-
+
+
This plan is currently out of stock
+
+ We are currently
+
out of capacity
+ for your selected RAM amount. Please try again later, or try a different amount.
+
+
+
We can't seem to find your selected plan
We are currently unable to find a server for your selected RAM amount. Please
@@ -383,7 +394,7 @@
:disabled="
paymentLoading ||
(mutatedProduct.metadata.type === 'pyro' && !projectId && !serverName) ||
- (customServer && !customMatchingProduct)
+ customAllowedToContinue
"
@click="nextStep"
>
@@ -545,6 +556,16 @@ const props = defineProps({
type: Boolean,
required: false,
},
+ fetchCapacityStatuses: {
+ type: Function,
+ required: false,
+ default: null,
+ },
+ outOfStockUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
})
const messages = defineMessages({
@@ -631,7 +652,16 @@ const serverLoader = ref('Vanilla')
const eulaAccepted = ref(false)
const mutatedProduct = ref({ ...props.product })
+const customMinRam = ref(0)
+const customMaxRam = ref(0)
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({
ramInGb: 4,
@@ -640,23 +670,48 @@ const customServerConfig = reactive({
})
const updateCustomServerProduct = () => {
- if (props.customServer) {
- customMatchingProduct.value = props.product.find(
- (product) => product.metadata.ram === customServerConfig.ram,
- )
+ customMatchingProduct.value = props.product.find(
+ (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) {
- updateCustomServerProduct()
- watch(
- () => customServerConfig.ram,
- () => {
- updateCustomServerProduct()
- },
- )
+ const ramValues = props.product.map((product) => product.metadata.ram / 1024)
+ customMinRam.value = Math.min(...ramValues)
+ customMaxRam.value = Math.max(...ramValues)
+
+ const updateProductAndStock = () => {
+ updateCustomServerProduct()
+ updateCustomServerStock()
+ }
+
+ updateProductAndStock()
+ watch(() => customServerConfig.ram, updateProductAndStock)
}
const selectedPaymentMethod = ref()