Merge commit 'dbde3c4669af10dd577590ed6980e5bd4552d13c' into feature-clean

This commit is contained in:
2025-06-19 03:45:56 +03:00
364 changed files with 8892 additions and 7026 deletions

View File

@@ -32,7 +32,7 @@
<h1 class="wrap-as-needed">
{{ project.title }}
</h1>
<Badge :type="project.status" />
<ProjectStatusBadge :status="project.status" />
</div>
</div>
<h2>Project settings</h2>
@@ -870,6 +870,7 @@ import {
ProjectSidebarCreators,
ProjectSidebarDetails,
ProjectSidebarLinks,
ProjectStatusBadge,
ScrollablePanel,
useRelativeTime,
} from "@modrinth/ui";
@@ -880,7 +881,6 @@ import dayjs from "dayjs";
import Accordion from "~/components/ui/Accordion.vue";
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import Badge from "~/components/ui/Badge.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import MessageBanner from "~/components/ui/MessageBanner.vue";

View File

@@ -242,8 +242,7 @@
import { formatProjectStatus } from "@modrinth/utils";
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
import { Multiselect } from "vue-multiselect";
import { ConfirmModal } from "@modrinth/ui";
import Avatar from "~/components/ui/Avatar.vue";
import { ConfirmModal, Avatar } from "@modrinth/ui";
import FileInput from "~/components/ui/FileInput.vue";
const props = defineProps({

View File

@@ -630,7 +630,15 @@
</div>
</template>
<script>
import { ButtonStyled, ConfirmModal, MarkdownEditor } from "@modrinth/ui";
import {
Avatar,
Badge,
CopyCode,
Checkbox,
ButtonStyled,
ConfirmModal,
MarkdownEditor,
} from "@modrinth/ui";
import {
FileIcon,
TrashIcon,
@@ -656,13 +664,9 @@ import { renderHighlightedString } from "~/helpers/highlight.js";
import { reportVersion } from "~/utils/report-helpers.ts";
import { useImageUpload } from "~/composables/image-upload.ts";
import Avatar from "~/components/ui/Avatar.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import Badge from "~/components/ui/Badge.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Categories from "~/components/ui/search/Categories.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import FileInput from "~/components/ui/FileInput.vue";
import Modal from "~/components/ui/Modal.vue";

View File

@@ -275,7 +275,7 @@ import { useVIntl } from "@vintl/vintl";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { computed } from "vue";
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
import { usePyroFetch } from "~/composables/pyroFetch.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
const { formatMessage } = useVIntl();
@@ -290,7 +290,7 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
await refreshNotices();
async function refreshNotices() {
await usePyroFetch("notices").then((res) => {
await useServersFetch("notices").then((res) => {
notices.value = res as ServerNoticeType[];
notices.value.sort((a, b) => {
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
@@ -347,7 +347,7 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
}
async function deleteNotice(notice: ServerNoticeType) {
await usePyroFetch(`notices/${notice.id}`, {
await useServersFetch(`notices/${notice.id}`, {
method: "DELETE",
})
.then(() => {
@@ -401,7 +401,7 @@ async function saveChanges() {
return;
}
await usePyroFetch(`notices/${editingNotice.value?.id}`, {
await useServersFetch(`notices/${editingNotice.value?.id}`, {
method: "PATCH",
body: {
message: newNoticeMessage.value,
@@ -432,7 +432,7 @@ async function createNotice() {
return;
}
await usePyroFetch("notices", {
await useServersFetch("notices", {
method: "POST",
body: {
message: newNoticeMessage.value,

View File

@@ -8,13 +8,11 @@ import {
DownloadIcon,
LinkIcon,
} from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue";
import { Avatar, Checkbox, Badge } from "@modrinth/ui";
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
import Badge from "~/components/ui/Badge.vue";
import PrismIcon from "~/assets/images/external/prism.svg?component";
import ATLauncher from "~/assets/images/external/atlauncher.svg?component";
import CurseForge from "~/assets/images/external/curseforge.svg?component";
import Checkbox from "~/components/ui/Checkbox.vue";
import { homePageProjects } from "~/generated/state.json";

View File

@@ -97,7 +97,7 @@
</template>
<script setup>
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue";
import { Avatar } from "@modrinth/ui";
import NotificationItem from "~/components/ui/NotificationItem.vue";
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";

View File

@@ -49,12 +49,14 @@
/>
</template>
<p v-else>You don't have any unread notifications.</p>
<Pagination :page="page" :count="pages" @switch-page="changePage" />
<div class="flex justify-end">
<Pagination :page="page" :count="pages" @switch-page="changePage" />
</div>
</section>
</div>
</template>
<script setup>
import { Button, Chips } from "@modrinth/ui";
import { Button, Pagination, Chips } from "@modrinth/ui";
import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets";
import {
fetchExtraNotificationData,
@@ -63,7 +65,6 @@ import {
} from "~/helpers/notifications.ts";
import NotificationItem from "~/components/ui/NotificationItem.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import Pagination from "~/components/ui/Pagination.vue";
useHead({
title: "Notifications - Modrinth",

View File

@@ -14,7 +14,7 @@
<template v-if="orgs?.length > 0">
<div class="orgs-grid">
<nuxt-link
v-for="org in orgs"
v-for="org in sortedOrgs"
:key="org.id"
:to="`/organization/${org.slug}`"
class="universal-card button-base recessed org"
@@ -67,6 +67,8 @@ const { data: orgs, error } = useAsyncData("organizations", () => {
});
});
const sortedOrgs = computed(() => orgs.value.sort((a, b) => a.name.localeCompare(b.name)));
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
if (error.value) {

View File

@@ -279,18 +279,19 @@
</div>
<div>
<Badge v-if="project.status" :type="project.status" class="status" />
<ProjectStatusBadge v-if="project.status" :status="project.status" />
</div>
<div>
<nuxt-link
class="square-button"
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id
}/settings`"
>
<SettingsIcon />
</nuxt-link>
<ButtonStyled circular>
<nuxt-link
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id
}/settings`"
>
<SettingsIcon />
</nuxt-link>
</ButtonStyled>
</div>
</div>
</div>
@@ -312,19 +313,23 @@ import {
SortAscendingIcon as AscendingIcon,
SortDescendingIcon as DescendingIcon,
} from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui";
import {
Avatar,
ButtonStyled,
Checkbox,
CopyCode,
ProjectStatusBadge,
commonMessages,
} from "@modrinth/ui";
import Badge from "~/components/ui/Badge.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import Modal from "~/components/ui/Modal.vue";
import Avatar from "~/components/ui/Avatar.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
export default defineNuxtComponent({
components: {
Avatar,
Badge,
ButtonStyled,
ProjectStatusBadge,
SettingsIcon,
TrashIcon,
Checkbox,

View File

@@ -527,7 +527,7 @@
</template>
<script setup>
import { Multiselect } from "vue-multiselect";
import { ButtonStyled, useRelativeTime } from "@modrinth/ui";
import { Avatar, ButtonStyled, useRelativeTime } from "@modrinth/ui";
import {
CompassIcon,
LogInIcon,
@@ -539,7 +539,6 @@ import {
} from "@modrinth/assets";
import PrismLauncherLogo from "~/assets/images/external/prism.svg?component";
import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component";
import Avatar from "~/components/ui/Avatar.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";

View File

@@ -81,7 +81,9 @@
</div>
<div class="mobile-row">
is requesting to be
<Badge :type="project.requested_status ? project.requested_status : 'approved'" />
<ProjectStatusBadge
:status="project.requested_status ? project.requested_status : 'approved'"
/>
</div>
</div>
<div class="input-group">
@@ -103,7 +105,7 @@
</template>
<script setup>
import { Chips, useRelativeTime } from "@modrinth/ui";
import { Avatar, ProjectStatusBadge, Chips, useRelativeTime } from "@modrinth/ui";
import {
UnknownIcon,
EyeIcon,
@@ -112,9 +114,8 @@ import {
IssuesIcon,
ScaleIcon,
} from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import { formatProjectType } from "~/plugins/shorthands.js";
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
useHead({
title: "Review projects - Modrinth",
@@ -170,28 +171,6 @@ const projectTypes = computed(() => {
return [...set];
});
function segmentData(data, segmentSize = 800) {
return data.reduce((acc, curr, index) => {
const segment = Math.floor(index / segmentSize);
if (!acc[segment]) {
acc[segment] = [];
}
acc[segment].push(curr);
return acc;
}, []);
}
function fetchSegmented(data, createUrl, options = {}) {
return Promise.all(segmentData(data).map((ids) => useBaseFetch(createUrl(ids), options))).then(
(results) => results.flat(),
);
}
function asEncodedJsonArray(data) {
return encodeURIComponent(JSON.stringify(data));
}
if (projects.value) {
const teamIds = projects.value.map((x) => x.team_id);
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);

View File

@@ -334,6 +334,7 @@ import {
ImageIcon,
} from "@modrinth/assets";
import { computed } from "vue";
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
@@ -388,7 +389,7 @@ async function updateServerContext() {
if (!auth.value.user) {
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
} else if (route.query.sid !== null) {
server.value = await usePyroServer(route.query.sid, ["general", "content"], {
server.value = await useModrinthServers(route.query.sid, ["general", "content"], {
waitForModules: true,
});
}
@@ -519,7 +520,6 @@ async function serverInstall(project) {
if (projectType.value.id === "modpack") {
await server.value.general.reinstall(
server.value.serverId,
false,
project.project_id,
version.id,

View File

@@ -4,27 +4,28 @@
data-pyro
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
>
<PurchaseModal
v-if="showModal && selectedProduct && customer"
:key="selectedProduct.id"
<ModrinthServersPurchaseModal
v-if="customer"
:key="`purchase-modal-${customer.id}`"
ref="purchaseModal"
:product="selectedProduct"
:country="country"
:custom-server="customServer"
:publishable-key="config.public.stripePublishableKey"
:send-billing-request="
:initiate-payment="
async (body) =>
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
"
:fetch-payment-data="fetchPaymentData"
:available-products="pyroProducts"
:on-error="handleError"
:customer="customer"
:payment-methods="paymentMethods"
:currency="selectedCurrency"
: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"
:fetch-capacity-statuses="fetchCapacityStatuses"
:pings="regionPings"
:regions="regions"
:refresh-payment-methods="fetchPaymentData"
:fetch-stock="fetchStock"
/>
<section
@@ -442,8 +443,8 @@
Where are Modrinth Servers located? Can I choose a region?
</summary>
<p class="m-0 ml-6 leading-[160%]">
Currently, Modrinth Servers are located on the east coast of the United States in
Vint Hill, Virginia. More regions to come in the future!
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
Germany. More regions to come in the future!
</p>
</details>
@@ -497,98 +498,6 @@
</div>
</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"
>
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
<div class="mx-auto flex w-full max-w-7xl flex-col gap-8">
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
>
Server Locations
</div>
<h1 class="relative m-0 max-w-2xl text-4xl leading-[120%] md:text-7xl">
Coast-to-Coast Coverage
</h1>
</div>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<div class="grid size-8 place-content-center rounded-full bg-highlight-green">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-brand"
>
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>
</div>
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
US Coverage
</h2>
</div>
<p
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
>
With strategically placed servers in New York, California, Texas, Florida, and
Washington, we ensure low latency connections for players across North America.
Each location is equipped with high-performance hardware and DDoS protection.
</p>
</div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<div class="grid size-8 place-content-center rounded-full bg-highlight-blue">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-blue"
>
<path d="M12 2a10 10 0 1 0 10 10" />
<path d="M18 13a6 6 0 0 0-6-6" />
<path d="M13 2.05a10 10 0 0 1 2 2" />
<path d="M19.5 8.5a10 10 0 0 1 2 2" />
</svg>
</div>
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
Global Expansion
</h2>
</div>
<p
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
>
We're expanding to Europe and Asia-Pacific regions soon, bringing Modrinth's
seamless hosting experience worldwide. Join our Discord to stay updated on new
region launches.
</p>
</div>
</div>
</div>
<Globe />
</div>
</div>
</section>
<section
id="plan"
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"
@@ -596,19 +505,35 @@
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
Start your server on Modrinth
There's a server for everyone
</h1>
<h2
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
>
{{
isAtCapacity && !loggedOut
? "We are currently at capacity. Please try again later."
: "There's a plan for everyone! Choose the one that fits your needs."
}}
</h2>
<p class="m-0 flex items-center gap-1">
Available in North America and Europe for wide coverage.
</p>
<ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row">
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
<span></span>
<OptionGroup v-slot="{ option }" v-model="billingPeriod" :options="billingPeriods">
<template v-if="option === 'monthly'"> Pay monthly </template>
<span v-else-if="option === 'quarterly'"> Pay quarterly </span>
<span v-else-if="option === 'yearly'"> Pay yearly </span>
</OptionGroup>
<template v-if="billingPeriods.includes('quarterly')">
<button
v-if="billingPeriod !== 'quarterly'"
class="bg-transparent p-0 text-sm font-medium text-brand hover:underline active:scale-95"
@click="billingPeriod = 'quarterly'"
>
Save 16% with quarterly billing!
</button>
<span v-else class="bg-transparent p-0 text-sm font-medium text-brand">
Save 16% with quarterly billing!
</span>
</template>
<span v-else></span>
</div>
<ul class="m-0 flex w-full grid-cols-3 flex-col gap-8 p-0 lg:grid">
<ServerPlanSelector
:capacity="capacityStatuses?.small?.available"
plan="small"
@@ -616,9 +541,12 @@
:storage="plans.small.metadata.storage"
:cpus="plans.small.metadata.cpu"
:price="
plans.small?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
?.monthly
plans.small?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
?.intervals?.[billingPeriod]
"
:interval="billingPeriod"
:currency="selectedCurrency"
:is-usa="country.toLowerCase() === 'us'"
@select="selectProduct('small')"
@scroll-to-faq="scrollToFaq()"
/>
@@ -629,9 +557,12 @@
:storage="plans.medium.metadata.storage"
:cpus="plans.medium.metadata.cpu"
:price="
plans.medium?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
?.monthly
plans.medium?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
?.intervals?.[billingPeriod]
"
:interval="billingPeriod"
:currency="selectedCurrency"
:is-usa="country.toLowerCase() === 'us'"
@select="selectProduct('medium')"
@scroll-to-faq="scrollToFaq()"
/>
@@ -641,10 +572,13 @@
:storage="plans.large.metadata.storage"
:cpus="plans.large.metadata.cpu"
:price="
plans.large?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
?.monthly
plans.large?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
?.intervals?.[billingPeriod]
"
:currency="selectedCurrency"
:is-usa="country.toLowerCase() === 'us'"
plan="large"
:interval="billingPeriod"
@select="selectProduct('large')"
@scroll-to-faq="scrollToFaq()"
/>
@@ -654,10 +588,9 @@
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">
<h1 class="m-0">Build your own</h1>
<h1 class="m-0">Know exactly what you need?</h1>
<h2 class="m-0 text-base font-normal text-primary">
If you're a more technical server administrator, you can pick your own RAM and storage
options.
Pick a customized plan with just the specs you need.
</h2>
</div>
@@ -666,7 +599,7 @@
>
<ButtonStyled color="standard" size="large">
<button class="w-full md:w-fit" @click="selectProduct('custom')">
Build your own
Get started
<RightArrowIcon class="shrink-0" />
</button>
</ButtonStyled>
@@ -679,7 +612,7 @@
</template>
<script setup>
import { ButtonStyled, PurchaseModal } from "@modrinth/ui";
import { ButtonStyled, ModrinthServersPurchaseModal } from "@modrinth/ui";
import {
BoxIcon,
GameIcon,
@@ -690,9 +623,13 @@ import {
ServerIcon,
} from "@modrinth/assets";
import { products } from "~/generated/state.json";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
import Globe from "~/components/ui/servers/Globe.vue";
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
import OptionGroup from "~/components/ui/OptionGroup.vue";
const billingPeriods = ref(["monthly", "quarterly"]);
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
const pyroPlanProducts = pyroProducts.filter(
@@ -711,16 +648,6 @@ useSeoMeta({
ogDescription: description,
});
useHead({
script: [
{
src: "https://js.stripe.com/v3/",
defer: true,
async: true,
},
],
});
const auth = await useAuth();
const data = useNuxtApp();
const config = useRuntimeConfig();
@@ -740,6 +667,7 @@ const isDeleting = ref(false);
const typingSpeed = 75;
const deletingSpeed = 25;
const pauseTime = 2000;
const selectedCurrency = ref("USD");
const loggedOut = computed(() => !auth.value.user);
const outOfStockUrl = "https://discord.modrinth.com";
@@ -747,13 +675,23 @@ const outOfStockUrl = "https://discord.modrinth.com";
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
try {
if (!auth.value.user) return false;
const response = await usePyroFetch("servers");
const response = await useServersFetch("servers");
return response.servers && response.servers.length > 0;
} catch {
return false;
}
});
function fetchStock(region, request) {
return useServersFetch(`stock?region=${region.shortcode}`, {
method: "POST",
body: {
...request,
},
bypassAuth: true,
}).then((res) => res.available);
}
async function fetchCapacityStatuses(customProduct = null) {
try {
const productsToCheck = customProduct?.metadata
@@ -765,7 +703,7 @@ async function fetchCapacityStatuses(customProduct = null) {
),
];
const capacityChecks = productsToCheck.map((product) =>
usePyroFetch("stock", {
useServersFetch("stock", {
method: "POST",
body: {
cpu: product.metadata.cpu,
@@ -841,23 +779,6 @@ const handleError = (err) => {
});
};
const handleModalHidden = () => {
showModal.value = false;
};
watch(selectedProduct, async (newProduct) => {
if (newProduct) {
showModal.value = false;
await nextTick();
showModal.value = true;
modalKey.value++;
await nextTick();
if (purchaseModal.value && purchaseModal.value.show) {
purchaseModal.value.show();
}
}
});
async function fetchPaymentData() {
if (!auth.value.user) return;
try {
@@ -954,8 +875,10 @@ const selectProduct = async (product) => {
modalKey.value++;
await nextTick();
if (purchaseModal.value && purchaseModal.value.show) {
purchaseModal.value.show();
if (product === "custom") {
purchaseModal.value?.show(billingPeriod.value);
} else {
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value);
}
};
@@ -966,9 +889,82 @@ const planQuery = () => {
}
};
const regions = ref([]);
const regionPings = ref([]);
function pingRegions() {
useServersFetch("regions", {
method: "GET",
version: 1,
bypassAuth: true,
}).then((res) => {
regions.value = res;
regions.value.forEach((region) => {
runPingTest(region);
});
});
}
const PING_COUNT = 20;
const PING_INTERVAL = 200;
const MAX_PING_TIME = 1000;
function runPingTest(region, index = 1) {
if (index > 10) {
regionPings.value.push({
region: region.shortcode,
ping: -1,
});
return;
}
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`;
try {
const socket = new WebSocket(wsUrl);
const pings = [];
socket.onopen = () => {
for (let i = 0; i < PING_COUNT; i++) {
setTimeout(() => {
socket.send(performance.now());
}, i * PING_INTERVAL);
}
setTimeout(
() => {
socket.close();
const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)]);
if (median) {
regionPings.value.push({
region: region.shortcode,
ping: median,
});
}
},
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
);
};
socket.onmessage = (event) => {
pings.push(performance.now() - event.data);
};
socket.onerror = (event) => {
console.error(
`Failed to connect pingtest WebSocket with ${wsUrl}, trying index ${index + 1}:`,
event,
);
runPingTest(region, index + 1);
};
} catch (error) {
console.error(`Failed to connect pingtest WebSocket with ${wsUrl}:`, error);
}
}
onMounted(() => {
startTyping();
planQuery();
pingRegions();
});
watch(customer, (newCustomer) => {

View File

@@ -18,160 +18,96 @@
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
<TransferIcon class="size-12 text-blue" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1>
</div>
<p class="text-lg text-secondary">
Your server's hardware is currently being upgraded and will be back online shortly!
</p>
</div>
</div>
<ErrorInformationCard
title="Server upgrading"
description="Your server's hardware is currently being upgraded and will be back online shortly!"
:icon="TransferIcon"
icon-color="blue"
:action="generalErrorAction"
/>
</div>
<div
v-else-if="serverData?.status === 'suspended'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
<LockIcon class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1>
</div>
<p class="text-lg text-secondary">
{{
serverData.suspension_reason === "cancelled"
? "Your subscription has been cancelled."
: serverData.suspension_reason
? `Your server has been suspended: ${serverData.suspension_reason}`
: "Your server has been suspended."
}}
<br />
Contact Modrinth Support if you believe this is an error.
</p>
</div>
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
<button class="mt-6 !w-full">Go to billing settings</button>
</ButtonStyled>
</div>
<ErrorInformationCard
title="Server suspended"
:description="suspendedDescription"
:icon="LockIcon"
icon-color="orange"
:action="suspendedAction"
/>
</div>
<div
v-else-if="
server.general?.error?.error.statusCode === 403 ||
server.general?.error?.error.statusCode === 404
server.moduleErrors?.general?.error.statusCode === 403 ||
server.moduleErrors?.general?.error.statusCode === 404
"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
<TransferIcon class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1>
</div>
<p class="text-lg text-secondary">
You don't have permission to view this server or it no longer exists. If you believe this
is an error, please contact Modrinth Support.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
<button class="mt-6 !w-full">Go back to all servers</button>
</ButtonStyled>
</div>
<ErrorInformationCard
title="An error occured."
description="Please contact Modrinth Support."
:icon="TransferIcon"
icon-color="orange"
:error-details="generalErrorDetails"
:action="generalErrorAction"
/>
</div>
<div
v-else-if="server.general?.error?.error.statusCode === 503"
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-red p-4">
<UiServersIconsPanelErrorIcon class="size-12 text-red" />
</div>
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
<ErrorInformationCard
title="Server Node Unavailable"
:icon="PanelErrorIcon"
icon-color="red"
:action="nodeUnavailableAction"
:error-details="nodeUnavailableDetails"
>
<template #description>
<div class="text-md space-y-4">
<p class="leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as
the issue is resolved.
</p>
<p class="leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
</div>
<p class="m-0 mb-4 leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as the
issue is resolved.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
<div class="flex flex-col gap-2">
<UiCopyCode :text="'Server ID: ' + server.serverId" />
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
</div>
</div>
<ButtonStyled
size="large"
color="standard"
@click="
() =>
navigateTo('https://discord.modrinth.com', {
external: true,
})
"
>
<button class="mt-6 !w-full">Join Modrinth Discord</button>
</ButtonStyled>
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="standard"
@click="() => reloadNuxtApp()"
>
<button class="mt-3 !w-full">Reload</button>
</ButtonStyled>
</div>
</template>
</ErrorInformationCard>
</div>
<div
v-else-if="server.general?.error"
v-else-if="server.moduleErrors?.general?.error"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
<TransferIcon class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1>
<ErrorInformationCard
title="Connection lost"
description=""
:icon="TransferIcon"
icon-color="orange"
:action="connectionLostAction"
>
<template #description>
<div class="space-y-4">
<div class="text-center text-secondary">
{{
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
}}
</div>
<p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a
temporary network issue. You'll be reconnected automatically.
</p>
</div>
<p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a
temporary network issue. You'll be reconnected automatically.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="brand"
@click="() => reloadNuxtApp()"
>
<button class="mt-6 !w-full">Reload</button>
</ButtonStyled>
</div>
</template>
</ErrorInformationCard>
</div>
<!-- SERVER START -->
<div
@@ -207,6 +143,7 @@
class="server-action-buttons-anim flex w-fit flex-shrink-0"
>
<UiServersPanelServerActionButton
v-if="!serverData.flows?.intro"
class="flex-shrink-0"
:is-online="isServerRunning"
:is-actioning="isActioning"
@@ -220,7 +157,14 @@
</div>
</div>
<div
v-if="serverData.flows?.intro"
class="flex items-center gap-2 font-semibold text-secondary"
>
<SettingsIcon /> Configuring server...
</div>
<UiServersServerInfoLabels
v-else
:server-data="serverData"
:show-game-label="showGameLabel"
:show-loader-label="showLoaderLabel"
@@ -231,149 +175,189 @@
</div>
</div>
<div
data-pyro-navigation
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
>
<UiNavTabs :links="navLinks" />
</div>
<div data-pyro-mount class="h-full w-full flex-1">
<template v-if="serverData.flows?.intro">
<div
v-if="error"
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
v-if="serverData?.status === 'installing'"
class="w-50 h-50 flex items-center justify-center gap-2 text-center text-lg font-bold"
>
<div class="flex flex-row gap-4">
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
<div class="flex flex-col gap-2 leading-[150%]">
<div class="flex items-center gap-3">
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
</div>
<LazyUiServersPanelSpinner class="size-10 animate-spin" /> Setting up your server...
</div>
<div v-else>
<h2 class="my-4 text-xl font-extrabold">
What would you like to install on your new server?
</h2>
<div v-if="errorTitle.toLocaleLowerCase() === 'installation error'" class="font-normal">
<div
v-if="errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'"
>
An invalid loader or Minecraft version was specified and could not be installed.
<ul class="m-0 mt-4 p-0 pl-4">
<li>
If this version of Minecraft was released recently, please check if Modrinth
Servers supports it.
</li>
<li>
If you've installed a modpack, it may have been packaged incorrectly or may not
be compatible with the loader.
</li>
<li>
Your server may need to be reinstalled with a valid mod loader and version. You
can change the loader by clicking the "Change Loader" button.
</li>
<li>
If you're stuck, please contact Modrinth Support with the information below:
</li>
</ul>
<ButtonStyled>
<button class="mt-2" @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
</div>
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
An internal error occurred while installing your server. Don't fret try
reinstalling your server, and if the problem persists, please contact Modrinth
support with your server's debug information.
</div>
<div v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'">
An error occurred while installing your server because Modrinth Servers does not
support the version of Minecraft or the loader you specified. Try reinstalling your
server with a different version or loader, and if the problem persists, please
contact Modrinth Support with your server's debug information.
<ServerInstallation
:server="server as ModrinthServer"
:backup-in-progress="backupInProgress"
ignore-current-installation
@reinstall="onReinstall"
/>
</div>
</template>
<template v-else>
<div
data-pyro-navigation
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
>
<UiNavTabs :links="navLinks" />
</div>
<div data-pyro-mount class="h-full w-full flex-1">
<div
v-if="error"
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
>
<div class="flex flex-row gap-4">
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
<div class="flex flex-col gap-2 leading-[150%]">
<div class="flex items-center gap-3">
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
</div>
<div
v-if="errorTitle === 'Installation error'"
class="mt-2 flex flex-col gap-4 sm:flex-row"
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
class="font-normal"
>
<ButtonStyled v-if="errorLog">
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
</ButtonStyled>
<ButtonStyled>
<button @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
<ButtonStyled color="red" type="standard">
<NuxtLink
class="whitespace-pre"
:to="`/servers/manage/${serverId}/options/loader`"
>
<RightArrowIcon />
Change Loader
</NuxtLink>
</ButtonStyled>
<div
v-if="
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
"
>
An invalid loader or Minecraft version was specified and could not be installed.
<ul class="m-0 mt-4 p-0 pl-4">
<li>
If this version of Minecraft was released recently, please check if Modrinth
Servers supports it.
</li>
<li>
If you've installed a modpack, it may have been packaged incorrectly or may
not be compatible with the loader.
</li>
<li>
Your server may need to be reinstalled with a valid mod loader and version.
You can change the loader by clicking the "Change Loader" button.
</li>
<li>
If you're stuck, please contact Modrinth Support with the information below:
</li>
</ul>
<ButtonStyled>
<button class="mt-2" @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
</div>
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
An internal error occurred while installing your server. Don't fret — try
reinstalling your server, and if the problem persists, please contact Modrinth
support with your server's debug information.
</div>
<div
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
>
An error occurred while installing your server because Modrinth Servers does not
support the version of Minecraft or the loader you specified. Try reinstalling
your server with a different version or loader, and if the problem persists,
please contact Modrinth Support with your server's debug information.
</div>
<div
v-if="errorTitle === 'Installation error'"
class="mt-2 flex flex-col gap-4 sm:flex-row"
>
<ButtonStyled v-if="errorLog">
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
</ButtonStyled>
<ButtonStyled>
<button @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
<ButtonStyled color="red" type="standard">
<NuxtLink
class="whitespace-pre"
:to="`/servers/manage/${serverId}/options/loader`"
>
<RightArrowIcon />
Change Loader
</NuxtLink>
</ButtonStyled>
</div>
</div>
</div>
</div>
</div>
</div>
<div
v-if="!isConnected && !isReconnecting && !isLoading"
data-pyro-server-ws-error
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
>
<IssuesIcon class="size-5 text-red" />
Something went wrong...
</div>
<div
v-if="!isConnected && !isReconnecting && !isLoading"
data-pyro-server-ws-error
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
>
<IssuesIcon class="size-5 text-red" />
Something went wrong...
</div>
<div
v-if="isReconnecting"
data-pyro-server-ws-reconnecting
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
>
<UiServersPanelSpinner />
Hang on, we're reconnecting to your server.
</div>
<div
v-if="isReconnecting"
data-pyro-server-ws-reconnecting
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
>
<UiServersPanelSpinner />
Hang on, we're reconnecting to your server.
</div>
<div
v-if="serverData.status === 'installing'"
data-pyro-server-installing
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
>
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
<div
v-if="serverData.status === 'installing'"
data-pyro-server-installing
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
>
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
<div class="flex flex-col gap-1">
<span class="text-lg font-bold"> We're preparing your server! </span>
<div class="flex flex-row items-center gap-2">
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
<div class="flex flex-col gap-1">
<span class="text-lg font-bold"> We're preparing your server! </span>
<div class="flex flex-row items-center gap-2">
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
</div>
</div>
</div>
<NuxtPage
:route="route"
:is-connected="isConnected"
:is-ws-auth-incorrect="isWSAuthIncorrect"
:is-server-running="isServerRunning"
:stats="stats"
:server-power-state="serverPowerState"
:power-state-details="powerStateDetails"
:socket="socket"
:server="server"
:backup-in-progress="backupInProgress"
@reinstall="onReinstall"
/>
</div>
<NuxtPage
:route="route"
:is-connected="isConnected"
:is-ws-auth-incorrect="isWSAuthIncorrect"
:is-server-running="isServerRunning"
:stats="stats"
:server-power-state="serverPowerState"
:power-state-details="powerStateDetails"
:socket="socket"
:server="server"
:backup-in-progress="backupInProgress"
@reinstall="onReinstall"
/>
</div>
</template>
</div>
<div
v-if="flags.advancedDebugInfo"
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
>
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
JSON.stringify(server, null, " ")
}}</pre>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
import {
SettingsIcon,
CopyIcon,
IssuesIcon,
LeftArrowIcon,
@@ -384,14 +368,23 @@ import {
LockIcon,
} from "@modrinth/assets";
import DOMPurify from "dompurify";
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
import { reloadNuxtApp, navigateTo } from "#app";
import type { MessageDescriptor } from "@vintl/vintl";
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
import { usePyroConsole } from "~/store/console.ts";
import { type Backup } from "~/composables/pyroServers.ts";
import { usePyroFetch } from "~/composables/pyroFetch.ts";
import type {
ServerState,
Stats,
WSEvent,
WSInstallationResultEvent,
Backup,
PowerAction,
} from "@modrinth/utils";
import { reloadNuxtApp, navigateTo } from "#app";
import { useModrinthServersConsole } from "~/store/console.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
import PanelErrorIcon from "~/components/ui/servers/icons/PanelErrorIcon.vue";
const app = useNuxtApp() as unknown as { $notify: any };
@@ -401,6 +394,7 @@ const isLoading = ref(true);
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
const isFirstMount = ref(true);
const isMounted = ref(true);
const flags = useFeatureFlags();
const INTERCOM_APP_ID = ref("ykeritl9");
const auth = (await useAuth()) as unknown as {
@@ -417,19 +411,19 @@ const route = useNativeRoute();
const router = useRouter();
const serverId = route.params.id as string;
const server = await usePyroServer(serverId, ["general", "ws"]);
const server: Reactive<ModrinthServer> = await useModrinthServers(serverId, ["general", "ws"]);
const loadModulesPromise = Promise.resolve().then(() => {
if (server.general?.status === "suspended") {
return;
}
return server.loadModules(["content", "backups", "network", "startup", "fs"]);
return server.refresh(["content", "backups", "network", "startup", "fs"]);
});
provide("modulesLoaded", loadModulesPromise);
watch(
() => [server.general?.error, server.ws?.error],
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
([generalError, wsError]) => {
if (server.general?.status === "suspended") return;
@@ -447,7 +441,7 @@ const errorLogFile = ref("");
const serverData = computed(() => server.general);
const isConnected = ref(false);
const isWSAuthIncorrect = ref(false);
const pyroConsole = usePyroConsole();
const modrinthServersConsole = useModrinthServersConsole();
const cpuData = ref<number[]>([]);
const ramData = ref<number[]>([]);
const isActioning = ref(false);
@@ -621,7 +615,7 @@ const connectWebSocket = () => {
return;
}
pyroConsole.clear();
modrinthServersConsole.clear();
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
isConnected.value = true;
isReconnecting.value = false;
@@ -629,7 +623,7 @@ const connectWebSocket = () => {
if (firstConnect.value) {
for (let i = 0; i < initialConsoleMessage.length; i++) {
pyroConsole.addLine(initialConsoleMessage[i]);
modrinthServersConsole.addLine(initialConsoleMessage[i]);
}
}
@@ -652,7 +646,9 @@ const connectWebSocket = () => {
socket.value.onclose = () => {
if (isMounted.value) {
pyroConsole.addLine("\nSomething went wrong with the connection, we're reconnecting...");
modrinthServersConsole.addLine(
"\nSomething went wrong with the connection, we're reconnecting...",
);
isConnected.value = false;
scheduleReconnect();
}
@@ -701,7 +697,7 @@ const startUptimeUpdates = () => {
const stopUptimeUpdates = () => {
if (uptimeIntervalId) {
clearInterval(uptimeIntervalId);
intervalId = null;
pollingIntervalId = null;
}
};
@@ -710,7 +706,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
case "log":
// eslint-disable-next-line no-case-declarations
const log = data.message.split("\n").filter((l) => l.trim());
pyroConsole.addLines(log);
modrinthServersConsole.addLines(log);
break;
case "stats":
updateStats(data);
@@ -812,6 +808,10 @@ const newLoaderVersion = ref<string | null>(null);
const newMCVersion = ref<string | null>(null);
const onReinstall = (potentialArgs: any) => {
if (serverData.value?.flows?.intro) {
server.general?.endIntro();
}
if (!serverData.value) return;
serverData.value.status = "installing";
@@ -967,11 +967,11 @@ const toAdverb = (word: string) => {
return word + "ing";
};
const sendPowerAction = async (action: "restart" | "start" | "stop" | "kill") => {
const sendPowerAction = async (action: PowerAction) => {
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
try {
isActioning.value = true;
await server.general?.power(actionName);
await server.general?.power(action);
} catch (error) {
console.error(`Error ${toAdverb(actionName)} server:`, error);
notifyError(
@@ -992,7 +992,7 @@ const notifyError = (title: string, text: string) => {
});
};
let intervalId: ReturnType<typeof setInterval> | null = null;
let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
const countdown = ref(15);
const formattedTime = computed(() => {
@@ -1036,23 +1036,142 @@ const backupInProgress = computed(() => {
});
const stopPolling = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
if (pollingIntervalId) {
clearTimeout(pollingIntervalId);
pollingIntervalId = null;
}
};
const startPolling = () => {
countdown.value = 15;
intervalId = setInterval(() => {
if (countdown.value <= 0) {
reloadNuxtApp();
} else {
countdown.value--;
stopPolling();
let retryCount = 0;
const maxRetries = 10;
const poll = async () => {
try {
await server.refresh(["general", "ws"]);
if (!server.moduleErrors?.general?.error) {
stopPolling();
connectWebSocket();
return;
}
retryCount++;
if (retryCount >= maxRetries) {
console.error("Max retries reached, stopping polling");
stopPolling();
return;
}
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
} catch (error) {
console.error("Polling failed:", error);
retryCount++;
if (retryCount < maxRetries) {
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
}
}
}, 1000);
};
poll();
};
const nodeUnavailableDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Node",
value: server.general?.datacenter ?? "Unknown! Please contact support!",
type: "inline" as const,
},
]);
const suspendedDescription = computed(() => {
if (serverData.value?.suspension_reason === "cancelled") {
return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.";
}
if (serverData.value?.suspension_reason) {
return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`;
}
return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error.";
});
const generalErrorDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Timestamp",
value: String(server.moduleErrors?.general?.timestamp),
type: "inline" as const,
},
{
label: "Error Name",
value: server.moduleErrors?.general?.error.name,
type: "inline" as const,
},
{
label: "Error Message",
value: server.moduleErrors?.general?.error.message,
type: "block" as const,
},
...(server.moduleErrors?.general?.error.originalError
? [
{
label: "Original Error",
value: String(server.moduleErrors.general.error.originalError),
type: "hidden" as const,
},
]
: []),
...(server.moduleErrors?.general?.error.stack
? [
{
label: "Stack Trace",
value: server.moduleErrors.general.error.stack,
type: "hidden" as const,
},
]
: []),
]);
const suspendedAction = computed(() => ({
label: "Go to billing settings",
onClick: () => router.push("/settings/billing"),
color: "brand" as const,
}));
const generalErrorAction = computed(() => ({
label: "Go back to all servers",
onClick: () => router.push("/servers/manage"),
color: "brand" as const,
}));
const nodeUnavailableAction = computed(() => ({
label: "Join Modrinth Discord",
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
color: "standard" as const,
}));
const connectionLostAction = computed(() => ({
label: "Reload",
onClick: () => reloadNuxtApp(),
color: "brand" as const,
disabled: formattedTime.value !== "00",
}));
const copyServerDebugInfo = () => {
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`;
navigator.clipboard.writeText(debugInfo);
@@ -1104,7 +1223,7 @@ const cleanup = () => {
};
async function dismissNotice(noticeId: number) {
await usePyroFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
await useServersFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
method: "POST",
}).catch((err) => {
app.$notify({
@@ -1123,14 +1242,18 @@ onMounted(() => {
isLoading.value = false;
return;
}
if (server.error) {
if (!server.error.message.includes("Forbidden")) {
if (server.moduleErrors.general?.error) {
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
startPolling();
}
} else {
connectWebSocket();
}
if (server.general?.flows?.intro && server.general?.project) {
server.general?.endIntro();
}
if (username.value && email.value && userId.value && createdAt.value) {
const currentUser = auth.value?.user as any;
const matches =

View File

@@ -1,6 +1,6 @@
<template>
<div
v-if="server.backups?.error"
v-if="server.moduleErrors.backups"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@@ -15,7 +15,9 @@
We couldn't load your server's backups. Here's what went wrong:
</p>
<p>
<span class="break-all font-mono">{{ JSON.stringify(server.backups.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.backups.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
<button class="mt-6 !w-full">Retry</button>
@@ -152,16 +154,17 @@ import { ButtonStyled, TagItem } from "@modrinth/ui";
import { useStorage } from "@vueuse/core";
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers";
import type { Backup } from "@modrinth/utils";
import BackupItem from "~/components/ui/servers/BackupItem.vue";
import BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
isServerRunning: boolean;
}>();

View File

@@ -5,12 +5,12 @@
</template>
<script setup lang="ts">
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@@ -11,7 +11,7 @@
/>
<div
v-if="server.content?.error"
v-if="server.moduleErrors.content"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@@ -24,7 +24,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't load your server's {{ type.toLowerCase() }}s. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.content.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.content.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['content'])">
<button class="mt-6 !w-full">Retry</button>
@@ -349,13 +351,14 @@ import {
} from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import type { Mod } from "@modrinth/utils";
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
import type { Server } from "~/composables/pyroServers";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const type = computed(() => {

View File

@@ -278,16 +278,11 @@ import {
} from "@modrinth/assets";
import { computed } from "vue";
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
import { formatBytes } from "@modrinth/utils";
import {
type DirectoryResponse,
type DirectoryItem,
type Server,
handleError,
} from "~/composables/pyroServers.ts";
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
import type { FilesystemOp, FSQueuedOp, DirectoryItem, DirectoryResponse } from "@modrinth/utils";
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
import type { FilesystemOp, FSQueuedOp } from "~/types/servers.ts";
import FilesUploadZipUrlModal from "~/components/ui/servers/FilesUploadZipUrlModal.vue";
import FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
@@ -316,7 +311,7 @@ interface RenameOperation extends BaseOperation {
type Operation = MoveOperation | RenameOperation;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
@@ -402,7 +397,7 @@ const fetchDirectoryContents = async (): Promise<DirectoryResponse> => {
};
} catch (error) {
console.error("Error fetching directory contents:", error);
if (error instanceof PyroFetchError && error.statusCode === 400) {
if (error instanceof ModrinthServersFetchError && error.statusCode === 400) {
return directoryData.value || { items: [], total: 0 };
}
throw error;
@@ -561,7 +556,7 @@ const handleRenameItem = async (newName: string) => {
});
} catch (error) {
console.error("Error renaming item:", error);
if (error instanceof PyroFetchError) {
if (error instanceof ModrinthServersFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",
@@ -719,7 +714,7 @@ const showDeleteModal = (item: any) => {
const handleCreateError = (error: any) => {
console.error("Error creating item:", error);
if (error instanceof PyroFetchError) {
if (error instanceof ModrinthServersFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",

View File

@@ -1,11 +1,7 @@
<template>
<div
v-if="isConnected && !isWsAuthIncorrect"
class="relative flex select-none flex-col gap-6"
data-pyro-server-manager-root
>
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
<div
v-if="inspectingError"
v-if="inspectingError && isConnected && !isWsAuthIncorrect"
data-pyro-servers-inspecting-error
class="flex justify-between rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
>
@@ -77,26 +73,34 @@
</ButtonStyled>
</div>
</div>
<div class="flex flex-col-reverse gap-6 md:flex-col">
<UiServersServerStats :data="stats" />
<UiServersServerStats
:data="isConnected && !isWsAuthIncorrect ? stats : undefined"
:loading="!isConnected || isWsAuthIncorrect"
/>
<div
class="relative flex h-[700px] w-full flex-col gap-3 overflow-hidden rounded-2xl border border-divider bg-bg-raised p-4 transition-all duration-300 ease-in-out md:p-8"
:class="{ 'border-0': !isConnected || isWsAuthIncorrect }"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
<UiServersPanelServerStatus :state="serverPowerState" />
<UiServersPanelServerStatus
v-if="isConnected && !isWsAuthIncorrect"
:state="serverPowerState"
/>
</div>
</div>
<!-- <div class="flex flex-row items-center gap-2 text-sm font-medium">
<InfoIcon class="hidden sm:block" />
Click and drag to select lines, then CMD+C to copy
</div> -->
<UiServersPanelTerminal :full-screen="fullScreen">
<UiServersPanelTerminal
:full-screen="fullScreen"
:loading="!isConnected || isWsAuthIncorrect"
>
<div class="relative w-full px-4 pt-4">
<ul
v-if="suggestions.length"
v-if="suggestions.length && isConnected && !isWsAuthIncorrect"
id="command-suggestions"
ref="suggestionsList"
class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
@@ -120,7 +124,7 @@
</ul>
<div class="relative flex items-center">
<span
v-if="bestSuggestion"
v-if="bestSuggestion && isConnected && !isWsAuthIncorrect"
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
>
<span class="ml-[23.5px] whitespace-pre">{{
@@ -142,7 +146,7 @@
<TerminalSquareIcon class="ml-3 h-5 w-5" />
</div>
<input
v-if="isServerRunning"
v-if="isServerRunning && isConnected && !isWsAuthIncorrect"
v-model="commandInput"
type="text"
placeholder="Send a command"
@@ -168,29 +172,25 @@
</UiServersPanelTerminal>
</div>
</div>
</div>
<UiServersOverviewLoading v-else-if="!isConnected && !isWsAuthIncorrect" />
<div v-else-if="isWsAuthIncorrect" class="flex flex-col">
<h2>Could not connect to the server.</h2>
<p>
An error occurred while attempting to connect to your server. Please try refreshing the page.
(WebSocket Authentication Failed)
</p>
</div>
<div v-else class="flex flex-col">
<h2>Could not connect to the server.</h2>
<p>
An error occurred while attempting to connect to your server. Please try refreshing the page.
(No further information)
</p>
<div
v-if="isWsAuthIncorrect"
class="absolute inset-0 flex flex-col items-center justify-center bg-bg"
>
<h2>Could not connect to the server.</h2>
<p>
An error occurred while attempting to connect to your server. Please try refreshing the
page. (WebSocket Authentication Failed)
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { TerminalSquareIcon, XIcon, IssuesIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { ServerState, Stats } from "~/types/servers";
import type { Server } from "~/composables/pyroServers";
import type { ServerState, Stats } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
type ServerProps = {
socket: WebSocket | null;
@@ -203,7 +203,7 @@ type ServerProps = {
exit_code?: number;
};
isServerRunning: boolean;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
};
const props = defineProps<ServerProps>();

View File

@@ -17,14 +17,14 @@ import {
UserIcon,
WrenchIcon,
} from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
const route = useRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();

View File

@@ -114,11 +114,10 @@
<script setup lang="ts">
import { EditIcon, TransferIcon } from "@modrinth/assets";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);
@@ -156,26 +155,18 @@ const saveGeneral = async () => {
if (serverSubdomain.value !== data.value?.net?.domain) {
try {
// type shit backend makes me do
const response = await props.server.network?.checkSubdomainAvailability(
const available = await props.server.network?.checkSubdomainAvailability(
serverSubdomain.value,
);
if (response === undefined) {
throw new Error("Failed to check subdomain availability");
}
if (typeof response === "object" && response !== null && "available" in response) {
const typedResponse = response as { available: boolean };
if (!typedResponse.available) {
addNotification({
group: "serverOptions",
type: "error",
title: "Subdomain not available",
text: "The subdomain you entered is already in use.",
});
return;
}
} else {
throw new Error("Invalid response format from availability check");
if (!available) {
addNotification({
group: "serverOptions",
type: "error",
title: "Subdomain not available",
text: "The subdomain you entered is already in use.",
});
return;
}
await props.server.network?.changeSubdomain(serverSubdomain.value);

View File

@@ -117,13 +117,13 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@@ -1,269 +1,22 @@
<template>
<LazyUiServersPlatformVersionSelectModal
ref="versionSelectModal"
<ServerInstallation
:server="props.server"
:current-loader="data?.loader as Loaders"
:backup-in-progress="backupInProgress"
@reinstall="emit('reinstall', $event)"
/>
<LazyUiServersPlatformMrpackModal
ref="mrpackModal"
:server="props.server"
@reinstall="emit('reinstall', $event)"
/>
<LazyUiServersPlatformChangeModpackVersionModal
ref="modpackVersionModal"
:server="props.server"
:project="data?.project"
:versions="Array.isArray(versions) ? versions : []"
:current-version="currentVersion"
:current-version-id="data?.upstream?.version_id"
:server-status="data?.status"
:backup-in-progress="props.backupInProgress"
@reinstall="emit('reinstall')"
/>
<div class="flex h-full w-full flex-col">
<div v-if="data && versions" class="flex w-full flex-col">
<div class="card flex flex-col gap-4">
<div class="flex select-none flex-col items-center justify-between gap-2 lg:flex-row">
<div class="flex flex-row items-center gap-2">
<h2 class="m-0 text-lg font-bold text-contrast">Modpack</h2>
<div
v-if="updateAvailable"
class="rounded-full bg-bg-orange px-2 py-1 text-xs font-medium text-orange"
>
<span>Update available</span>
</div>
</div>
<div v-if="data.upstream" class="flex gap-4">
<ButtonStyled>
<button
class="!w-full sm:!w-auto"
:disabled="isInstalling"
@click="mrpackModal.show()"
>
<UploadIcon class="size-4" /> Import .mrpack
</button>
</ButtonStyled>
<!-- dumb hack to make a button link not a link -->
<ButtonStyled>
<template v-if="isInstalling">
<button :disabled="isInstalling">
<TransferIcon class="size-4" />
Switch modpack
</button>
</template>
<nuxt-link v-else :to="`/modpacks?sid=${props.server.serverId}`">
<TransferIcon class="size-4" />
Switch modpack
</nuxt-link>
</ButtonStyled>
</div>
</div>
<div v-if="data.upstream" class="flex flex-col gap-2">
<div
v-if="versionsError || currentVersionError"
class="rounded-2xl border border-solid border-red p-4 text-contrast"
>
<p class="m-0 font-bold">Something went wrong while loading your modpack.</p>
<p class="m-0 mb-2 mt-1 text-sm">
{{ versionsError || currentVersionError }}
</p>
<ButtonStyled>
<button :disabled="isInstalling" @click="refreshData">Retry</button>
</ButtonStyled>
</div>
<NewProjectCard
v-if="!versionsError && !currentVersionError"
class="!cursor-default !bg-bg !filter-none"
:project="projectCardData"
:categories="data.project?.categories || []"
>
<template #actions>
<ButtonStyled color="brand">
<button :disabled="isInstalling" @click="modpackVersionModal.show()">
<SettingsIcon class="size-4" />
Change version
</button>
</ButtonStyled>
</template>
</NewProjectCard>
</div>
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
<ButtonStyled>
<nuxt-link
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
:class="{ disabled: backupInProgress }"
class="!w-full sm:!w-auto"
:to="`/modpacks?sid=${props.server.serverId}`"
>
<CompassIcon class="size-4" /> Find a modpack
</nuxt-link>
</ButtonStyled>
<span class="hidden sm:block">or</span>
<ButtonStyled>
<button
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
:disabled="!!backupInProgress"
class="!w-full sm:!w-auto"
@click="mrpackModal.show()"
>
<UploadIcon class="size-4" /> Upload .mrpack file
</button>
</ButtonStyled>
</div>
</div>
<div class="card flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
<p class="m-0">Your server's platform is the software that runs mods and plugins.</p>
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
<InfoIcon class="hidden sm:block" />
<span class="text-sm text-secondary">
The current platform was automatically selected based on your modpack.
</span>
</div>
</div>
<div
class="flex w-full flex-col gap-1 rounded-2xl"
:class="{
'pointer-events-none cursor-not-allowed select-none opacity-50':
props.server.general?.status === 'installing',
}"
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
>
<UiServersLoaderSelector
:data="data"
:is-installing="isInstalling"
@select-loader="selectLoader"
/>
</div>
</div>
</div>
<div v-else />
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import type { Loaders } from "~/types/servers";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
const { formatMessage } = useVIntl();
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();
const emit = defineEmits<{
reinstall: [any?];
}>();
const isInstalling = computed(() => props.server.general?.status === "installing");
const versionSelectModal = ref();
const mrpackModal = ref();
const modpackVersionModal = ref();
const data = computed(() => props.server.general);
const {
data: versions,
error: versionsError,
refresh: refreshVersions,
} = await useAsyncData(
`content-loader-versions-${data.value?.upstream?.project_id}`,
async () => {
if (!data.value?.upstream?.project_id) return [];
try {
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`);
return result || [];
} catch (e) {
console.error("couldnt fetch all versions:", e);
throw new Error("Failed to load modpack versions.");
}
},
{ default: () => [] },
);
const {
data: currentVersion,
error: currentVersionError,
refresh: refreshCurrentVersion,
} = await useAsyncData(
`content-loader-version-${data.value?.upstream?.version_id}`,
async () => {
if (!data.value?.upstream?.version_id) return null;
try {
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`);
return result || null;
} catch (e) {
console.error("couldnt fetch version:", e);
throw new Error("Failed to load modpack version.");
}
},
{ default: () => null },
);
const projectCardData = computed(() => ({
icon_url: data.value?.project?.icon_url,
title: data.value?.project?.title,
description: data.value?.project?.description,
downloads: data.value?.project?.downloads,
follows: data.value?.project?.followers,
// @ts-ignore
date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
}));
const selectLoader = (loader: string) => {
versionSelectModal.value?.show(loader as Loaders);
};
const refreshData = async () => {
await Promise.all([refreshVersions(), refreshCurrentVersion()]);
};
const updateAvailable = computed(() => {
// so sorry
// @ts-ignore
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
return false;
}
// @ts-ignore
const latestVersion = versions.value[0];
// @ts-ignore
return latestVersion.id !== currentVersion.value.id;
});
watch(
() => props.server.general?.status,
async (newStatus, oldStatus) => {
if (oldStatus === "installing" && newStatus === "available") {
await Promise.all([
refreshVersions(),
refreshCurrentVersion(),
props.server.refresh(["general"]),
]);
}
},
);
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
.button-base:active {
scale: none !important;
}
</style>

View File

@@ -60,7 +60,7 @@
<div class="relative h-full w-full overflow-y-auto">
<div
v-if="server.network?.error"
v-if="server.moduleErrors.network"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@@ -73,7 +73,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't load your server's network settings. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.network.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.network.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
<button class="mt-6 !w-full">Retry</button>
@@ -273,10 +275,10 @@ import {
} from "@modrinth/assets";
import { ButtonStyled, NewModal, ConfirmModal } from "@modrinth/ui";
import { ref, computed, nextTick } from "vue";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const isUpdating = ref(false);

View File

@@ -43,13 +43,13 @@
<script setup lang="ts">
import { useStorage } from "@vueuse/core";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const preferences = {

View File

@@ -1,6 +1,9 @@
<template>
<div class="relative h-full w-full select-none overflow-y-auto">
<div v-if="server.fs?.error" class="flex w-full flex-col items-center justify-center gap-4 p-4">
<div
v-if="server.moduleErrors.fs"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
@@ -11,7 +14,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't access your server's properties. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.fs.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.fs.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
<button class="mt-6 !w-full">Retry</button>
@@ -141,10 +146,10 @@
import { ref, watch, computed, inject } from "vue";
import { EyeIcon, SearchIcon, IssuesIcon } from "@modrinth/assets";
import Fuse from "fuse.js";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const tags = useTags();

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative h-full w-full">
<div
v-if="server.startup?.error"
v-if="server.moduleErrors.startup"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@@ -16,7 +16,9 @@
We couldn't load your server's startup settings. Here's what we know:
</p>
<p>
<span class="break-all font-mono">{{ JSON.stringify(server.startup.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.startup.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
<button class="mt-6 !w-full">Retry</button>
@@ -112,10 +114,10 @@
<script setup lang="ts">
import { UpdatedIcon, IssuesIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@@ -1,7 +1,7 @@
<template>
<div
data-pyro-server-list-root
class="experimental-styles-within relative mx-auto flex min-h-screen w-full max-w-[1280px] flex-col px-6"
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
>
<div
v-if="hasError || fetchError"
@@ -36,7 +36,7 @@
<li v-if="fetchError" class="text-red">
<p>Error details:</p>
<UiCopyCode
:text="(fetchError as PyroFetchError).message || 'Unknown error'"
:text="(fetchError as ModrinthServersFetchError).message || 'Unknown error'"
:copyable="false"
:selectable="false"
:language="'json'"
@@ -89,7 +89,10 @@
</div>
</div>
<ul v-if="filteredData.length > 0" class="m-0 flex flex-col gap-4 p-0">
<ul
v-if="filteredData.length > 0 || isPollingForNewServers"
class="m-0 flex flex-col gap-4 p-0"
>
<UiServersServerListing
v-for="server in filteredData"
:key="server.server_id"
@@ -102,6 +105,7 @@
:mc_version="server.mc_version"
:upstream="server.upstream"
:net="server.net"
:flows="server.flows"
/>
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
</ul>
@@ -117,9 +121,9 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import Fuse from "fuse.js";
import { HammerIcon, PlusIcon, SearchIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { Server, ModrinthServersFetchError } from "@modrinth/utils";
import { reloadNuxtApp } from "#app";
import type { PyroFetchError } from "~/composables/pyroFetch";
import type { Server } from "~/types/servers";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
definePageMeta({
middleware: "auth",
@@ -133,6 +137,7 @@ interface ServerResponse {
servers: Server[];
}
const router = useRouter();
const route = useRoute();
const hasError = ref(false);
const isPollingForNewServers = ref(false);
@@ -141,7 +146,9 @@ const {
data: serverResponse,
error: fetchError,
refresh,
} = await useAsyncData<ServerResponse>("ServerList", () => usePyroFetch<ServerResponse>("servers"));
} = await useAsyncData<ServerResponse>("ServerList", () =>
useServersFetch<ServerResponse>("servers"),
);
watch([fetchError, serverResponse], ([error, response]) => {
hasError.value = !!error || !response;
@@ -163,11 +170,19 @@ const fuse = computed(() => {
});
});
function introToTop(array: Server[]): Server[] {
return array.slice().sort((a, b) => {
return Number(b.flows?.intro) - Number(a.flows?.intro);
});
}
const filteredData = computed(() => {
if (!searchInput.value.trim()) {
return serverList.value;
return introToTop(serverList.value);
}
return fuse.value ? fuse.value.search(searchInput.value).map((result) => result.item) : [];
return fuse.value
? introToTop(fuse.value.search(searchInput.value).map((result) => result.item))
: [];
});
const previousServerList = ref<Server[]>([]);
@@ -179,6 +194,7 @@ const checkForNewServers = async () => {
if (JSON.stringify(previousServerList.value) !== JSON.stringify(serverList.value)) {
isPollingForNewServers.value = false;
clearInterval(intervalId);
router.replace({ query: {} });
} else if (refreshCount.value >= 5) {
isPollingForNewServers.value = false;
clearInterval(intervalId);

View File

@@ -330,8 +330,7 @@
<ButtonStyled
v-if="
getPyroCharge(subscription) &&
getPyroCharge(subscription).status !== 'cancelled' &&
getPyroCharge(subscription).status !== 'failed'
getPyroCharge(subscription).status !== 'cancelled'
"
>
<button @click="showCancellationSurvey(subscription)">
@@ -444,39 +443,13 @@
:return-url="`${config.public.siteUrl}/servers/manage`"
:server-name="`${auth?.user?.username}'s server`"
/>
<NewModal ref="addPaymentMethodModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ formatMessage(messages.paymentMethodTitle) }}
</span>
</template>
<div class="min-h-[16rem] md:w-[600px]">
<div
v-show="loadingPaymentMethodModal !== 2"
class="flex min-h-[16rem] items-center justify-center"
>
<AnimatedLogo class="w-[80px]" />
</div>
<div v-show="loadingPaymentMethodModal === 2" class="min-h-[16rem] p-1">
<div id="address-element"></div>
<div id="payment-element" class="mt-4"></div>
</div>
<div v-show="loadingPaymentMethodModal === 2" class="input-group mt-auto pt-4">
<ButtonStyled color="brand">
<button :disabled="loadingAddMethod" @click="submit">
<PlusIcon />
{{ formatMessage(messages.paymentMethodAdd) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="$refs.addPaymentMethodModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<AddPaymentMethodModal
ref="addPaymentMethodModal"
:publishable-key="config.public.stripePublishableKey"
:return-url="`${config.public.siteUrl}/settings/billing`"
:create-setup-intent="createSetupIntent"
:on-error="handleError"
/>
<div class="header__row">
<div class="header__title">
<h2 class="text-2xl">{{ formatMessage(messages.paymentMethodTitle) }}</h2>
@@ -590,9 +563,8 @@
<script setup>
import {
ConfirmModal,
NewModal,
AddPaymentMethodModal,
OverflowMenu,
AnimatedLogo,
PurchaseModal,
ButtonStyled,
CopyCode,
@@ -617,8 +589,9 @@ import {
UpdatedIcon,
HistoryIcon,
} from "@modrinth/assets";
import { calculateSavings, formatPrice, createStripeElements, getCurrency } from "@modrinth/utils";
import { calculateSavings, formatPrice, getCurrency } from "@modrinth/utils";
import { ref, computed } from "vue";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { products } from "~/generated/state.json";
definePageMeta({
@@ -754,19 +727,6 @@ const paymentMethodTypes = defineMessages({
},
});
let stripe = null;
let elements = null;
function loadStripe() {
try {
if (!stripe) {
stripe = Stripe(config.public.stripePublishableKey);
}
} catch (error) {
console.error("Error loading Stripe:", error);
}
}
const [
{ data: paymentMethods, refresh: refreshPaymentMethods },
{ data: charges, refresh: refreshCharges },
@@ -784,7 +744,7 @@ const [
useBaseFetch("billing/subscriptions", { internal: true }),
),
useAsyncData("billing/products", () => useBaseFetch("billing/products", { internal: true })),
useAsyncData("servers", () => usePyroFetch("servers")),
useAsyncData("servers", () => useServersFetch("servers")),
]);
const midasProduct = ref(products.find((x) => x.metadata?.type === "midas"));
@@ -842,69 +802,16 @@ const primaryPaymentMethodId = computed(() => {
});
const addPaymentMethodModal = ref();
const loadingPaymentMethodModal = ref(0);
async function addPaymentMethod() {
try {
loadingPaymentMethodModal.value = 0;
addPaymentMethodModal.value.show();
const result = await useBaseFetch("billing/payment_method", {
internal: true,
method: "POST",
});
loadStripe();
const {
elements: elementsVal,
addressElement,
paymentElement,
} = createStripeElements(stripe, paymentMethods.value, {
clientSecret: result.client_secret,
});
elements = elementsVal;
paymentElement.on("ready", () => {
loadingPaymentMethodModal.value += 1;
});
addressElement.on("ready", () => {
loadingPaymentMethodModal.value += 1;
});
} catch (err) {
data.$notify({
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: "error",
});
}
function addPaymentMethod() {
addPaymentMethodModal.value.show(paymentMethods.value);
}
const loadingAddMethod = ref(false);
async function submit() {
startLoading();
loadingAddMethod.value = true;
loadStripe();
const { error } = await stripe.confirmSetup({
elements,
confirmParams: {
return_url: `${config.public.siteUrl}/settings/billing`,
},
async function createSetupIntent() {
return await useBaseFetch("billing/payment_method", {
internal: true,
method: "POST",
});
if (error && error.type !== "validation_error") {
data.$notify({
group: "main",
title: "An error occurred",
text: error.message,
type: "error",
});
} else if (!error) {
await refresh();
addPaymentMethodModal.value.close();
}
loadingAddMethod.value = false;
stopLoading();
}
const removePaymentMethodIndex = ref();
@@ -1077,7 +984,7 @@ async function fetchCapacityStatuses(serverId, product) {
if (product) {
try {
return {
custom: await usePyroFetch(`servers/${serverId}/upgrade-stock`, {
custom: await useServersFetch(`servers/${serverId}/upgrade-stock`, {
method: "POST",
body: {
cpu: product.metadata.cpu,

View File

@@ -205,6 +205,7 @@
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import {
Checkbox,
CopyCode,
ConfirmModal,
commonSettingsMessages,
commonMessages,
@@ -219,7 +220,6 @@ import {
getScopeValue,
} from "~/composables/auth/scopes.ts";
import CopyCode from "~/components/ui/CopyCode.vue";
import Modal from "~/components/ui/Modal.vue";
const { formatMessage } = useVIntl();

View File

@@ -303,7 +303,7 @@
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
<div class="flex flex-wrap gap-2">
<nuxt-link
v-for="org in organizations"
v-for="org in sortedOrgs"
:key="org.id"
v-tooltip="org.name"
class="organization"
@@ -355,6 +355,7 @@ import {
GlobeIcon,
} from "@modrinth/assets";
import {
Avatar,
OverflowMenu,
ButtonStyled,
ContentPageHeader,
@@ -377,7 +378,6 @@ import BetaTesterBadge from "~/assets/images/badges/beta-tester.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
@@ -516,6 +516,8 @@ try {
});
}
const sortedOrgs = computed(() => organizations.value.sort((a, b) => a.name.localeCompare(b.name)));
if (!user.value) {
throw createError({
fatal: true,