You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit 'dbde3c4669af10dd577590ed6980e5bd4552d13c' into feature-clean
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user