- L
+
+
+ Only {{ capacityStatuses?.large?.available }} left in stock!
+
+
+
+
+
Ideal for larger communities, modpacks, and heavy modding.
+
+
8 GB RAM
+
+
8 vCPUs
+
+
64 GB Storage
+
+
+ $24/month
+
+
+
+ Out of Stock
+
+
+
+ Get Started
+
+
+
- Ideal for larger communities, modpacks, and heavy modding.
-
-
8 GB RAM
-
-
8 vCPUs
-
-
64 GB Storage
-
-
- $24/month
-
-
-
- Out of Stock
-
-
-
- Get Started
-
-
-
@@ -697,6 +821,7 @@ import {
} from "@modrinth/assets";
import { products } from "~/generated/state.json";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
+import Globe from "~/components/ui/servers/Globe.vue";
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
const pyroPlanProducts = pyroProducts.filter(
@@ -760,9 +885,16 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
async function fetchCapacityStatuses(customProduct = null) {
try {
- const productsToCheck = customProduct?.metadata ? [customProduct] : pyroPlanProducts;
+ const productsToCheck = customProduct?.metadata
+ ? [customProduct]
+ : [
+ ...pyroPlanProducts,
+ pyroProducts.reduce((min, product) =>
+ product.metadata.ram < min.metadata.ram ? product : min,
+ ),
+ ];
const capacityChecks = productsToCheck.map((product) =>
- usePyroFetch("capacity", {
+ usePyroFetch("stock", {
method: "POST",
body: {
cpu: product.metadata.cpu,
@@ -774,6 +906,7 @@ async function fetchCapacityStatuses(customProduct = null) {
);
const results = await Promise.all(capacityChecks);
+
if (customProduct?.metadata) {
return {
custom: results[0],
@@ -783,6 +916,7 @@ async function fetchCapacityStatuses(customProduct = null) {
small: results[0],
medium: results[1],
large: results[2],
+ custom: results[3],
};
}
} catch (error) {
@@ -804,6 +938,22 @@ const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);
const isMediumAtCapacity = computed(() => capacityStatuses.value?.medium?.available === 0);
const isLargeAtCapacity = computed(() => capacityStatuses.value?.large?.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 currentWord = words[currentWordIndex.value];
@@ -907,7 +1057,9 @@ const selectProduct = async (product) => {
}
await refreshCapacity();
- if (isAtCapacity.value) {
+ console.log(capacityStatuses.value);
+
+ if ((product === "custom" && isCustomAtCapacity.value) || isAtCapacity.value) {
addNotification({
group: "main",
title: "Server Capacity Full",
diff --git a/apps/frontend/src/public/earth-outline.png b/apps/frontend/src/public/earth-outline.png
new file mode 100644
index 000000000..fd9a71287
Binary files /dev/null and b/apps/frontend/src/public/earth-outline.png differ
diff --git a/package.json b/package.json
index b2c7ca962..575ac166a 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,12 @@
"ui:intl:extract": "pnpm run --filter=@modrinth/ui intl:extract",
"web:dev": "turbo run dev --filter=@modrinth/frontend",
"web:build": "turbo run build --filter=@modrinth/frontend",
+ "web:fix": "turbo run fix --filter=@modrinth/frontend",
"web:intl:extract": "pnpm run --filter=@modrinth/frontend intl:extract",
"app:dev": "turbo run dev --filter=@modrinth/app",
"docs:dev": "turbo run dev --filter=@modrinth/docs",
"app:build": "turbo run build --filter=@modrinth/app",
+ "app:fix": "turbo run fix --filter=@modrinth/app",
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"build": "turbo run build --continue",
diff --git a/packages/ui/src/components/changelog/ChangelogEntry.vue b/packages/ui/src/components/changelog/ChangelogEntry.vue
index ecde7730b..afbc13fe8 100644
--- a/packages/ui/src/components/changelog/ChangelogEntry.vue
+++ b/packages/ui/src/components/changelog/ChangelogEntry.vue
@@ -18,11 +18,16 @@
- {{ entry.version ?? formattedDate }}
+ {{ versionName }}
-
+
{{ relativeDate }}
@@ -61,11 +66,12 @@ const props = withDefaults(
)
const currentDate = ref(dayjs())
-const recent = computed(() => props.entry.date.isAfter(currentDate.value.subtract(1, 'week')))
-const dateTooltip = computed(() => props.entry.date.format('MMMM D, YYYY [at] h:mm A'))
-const formattedDate = computed(() =>
- props.entry.version ? props.entry.date.fromNow() : props.entry.date.format('MMMM D, YYYY'),
+const recent = computed(
+ () =>
+ props.entry.date.isAfter(currentDate.value.subtract(1, 'week')) &&
+ props.entry.date.isBefore(currentDate.value),
)
+const dateTooltip = computed(() => props.entry.date.format('MMMM D, YYYY [at] h:mm A'))
const relativeDate = computed(() => props.entry.date.fromNow())
const longDate = computed(() => props.entry.date.format('MMMM D, YYYY'))
diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts
index d595e9ca6..27c45bb5c 100644
--- a/packages/utils/changelog.ts
+++ b/packages/utils/changelog.ts
@@ -10,6 +10,16 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
+ {
+ date: `2025-02-12T12:10:00-08:00`,
+ product: 'web',
+ body: `### Added
+- Added a 3D globe to visualize node locations to Modrinth Servers marketing page.
+- Added an indicator to show when certain server plans are running low on availability.
+
+### Improvements
+- Improved out-of-stock notifications on Modrinth Servers page to be more accurate.`,
+ },
{
date: `2025-02-11T13:00:00-08:00`,
product: 'web',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 708fb7d99..44d79b84e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -209,6 +209,9 @@ importers:
'@pinia/nuxt':
specifier: ^0.5.1
version: 0.5.1(magicast@0.3.5)(rollup@4.28.1)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
+ '@types/three':
+ specifier: ^0.172.0
+ version: 0.172.0
'@vintl/vintl':
specifier: ^4.4.1
version: 4.4.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
@@ -260,6 +263,9 @@ importers:
semver:
specifier: ^7.5.4
version: 7.6.3
+ three:
+ specifier: ^0.172.0
+ version: 0.172.0
vue-multiselect:
specifier: 3.0.0-alpha.2
version: 3.0.0-alpha.2
@@ -2146,6 +2152,9 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@tweenjs/tween.js@23.1.3':
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
+
'@types/acorn@4.0.6':
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
@@ -2242,6 +2251,12 @@ packages:
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
+ '@types/stats.js@0.17.3':
+ resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
+
+ '@types/three@0.172.0':
+ resolution: {integrity: sha512-LrUtP3FEG26Zg5WiF0nbg8VoXiKokBLTcqM2iLvM9vzcfEiYmmBAPGdBgV0OYx9fvWlY3R/3ERTZcD9X5sc0NA==}
+
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -2257,6 +2272,9 @@ packages:
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
+ '@types/webxr@0.5.21':
+ resolution: {integrity: sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA==}
+
'@typescript-eslint/eslint-plugin@6.21.0':
resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2708,6 +2726,9 @@ packages:
'@webassemblyjs/wast-printer@1.12.1':
resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==}
+ '@webgpu/types@0.1.54':
+ resolution: {integrity: sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg==}
+
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -4026,6 +4047,9 @@ packages:
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -5008,6 +5032,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ meshoptimizer@0.18.1:
+ resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
+
micromark-core-commonmark@2.0.1:
resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==}
@@ -6613,6 +6640,9 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ three@0.172.0:
+ resolution: {integrity: sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==}
+
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -8913,7 +8943,7 @@ snapshots:
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)':
dependencies:
- '@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
+ '@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
'@typescript-eslint/parser': 6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
eslint: 9.13.0(jiti@2.4.1)
@@ -8926,10 +8956,10 @@ snapshots:
- supports-color
- typescript
- '@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))':
+ '@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))':
dependencies:
eslint: 9.13.0(jiti@2.4.1)
- eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
+ eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-node: 11.1.0(eslint@9.13.0(jiti@2.4.1))
@@ -9369,6 +9399,8 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@tweenjs/tween.js@23.1.3': {}
+
'@types/acorn@4.0.6':
dependencies:
'@types/estree': 1.0.6
@@ -9482,6 +9514,17 @@ snapshots:
'@types/semver@7.5.8': {}
+ '@types/stats.js@0.17.3': {}
+
+ '@types/three@0.172.0':
+ dependencies:
+ '@tweenjs/tween.js': 23.1.3
+ '@types/stats.js': 0.17.3
+ '@types/webxr': 0.5.21
+ '@webgpu/types': 0.1.54
+ fflate: 0.8.2
+ meshoptimizer: 0.18.1
+
'@types/trusted-types@2.0.7': {}
'@types/unist@2.0.11': {}
@@ -9492,6 +9535,8 @@ snapshots:
'@types/web-bluetooth@0.0.20': {}
+ '@types/webxr@0.5.21': {}
+
'@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)':
dependencies:
'@eslint-community/regexpp': 4.11.0
@@ -10260,6 +10305,8 @@ snapshots:
'@xtuc/long': 4.2.2
optional: true
+ '@webgpu/types@0.1.54': {}
+
'@xtuc/ieee754@1.2.0':
optional: true
@@ -11376,10 +11423,10 @@ snapshots:
dependencies:
eslint: 9.13.0(jiti@2.4.1)
- eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
+ eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
dependencies:
eslint: 9.13.0(jiti@2.4.1)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-promise: 6.4.0(eslint@9.13.0(jiti@2.4.1))
@@ -11405,7 +11452,7 @@ snapshots:
debug: 4.4.0(supports-color@9.4.0)
enhanced-resolve: 5.17.1
eslint: 9.13.0(jiti@2.4.1)
- eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
+ eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@@ -11417,7 +11464,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)):
+ eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -11932,6 +11979,8 @@ snapshots:
fflate@0.4.8: {}
+ fflate@0.8.2: {}
+
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -13156,6 +13205,8 @@ snapshots:
merge2@1.4.1: {}
+ meshoptimizer@0.18.1: {}
+
micromark-core-commonmark@2.0.1:
dependencies:
decode-named-character-reference: 1.0.2
@@ -15226,6 +15277,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
+ three@0.172.0: {}
+
tiny-invariant@1.3.3: {}
tinyexec@0.3.1: {}