You've already forked AstralRinth
forked from didirus/AstralRinth
Add quick server button, dynamic price preview for custom server modal (#3815)
* Add quick server creation button, and dynamic pricing to custom server selection * Remove test in compatibility card * Lint + remove duplicate file * Adjust z-index of popup * $6 -> $5 * Dismiss prompt if the button is clicked * Make "Create a server" disabled for now * Use existing loaders type
This commit is contained in:
@@ -31,6 +31,9 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
|||||||
projectBackground: false,
|
projectBackground: false,
|
||||||
searchBackground: false,
|
searchBackground: false,
|
||||||
advancedDebugInfo: false,
|
advancedDebugInfo: false,
|
||||||
|
showProjectPageDownloadModalServersPromo: true,
|
||||||
|
showProjectPageCreateServersTooltip: true,
|
||||||
|
showProjectPageQuickServerButton: false,
|
||||||
// advancedRendering: true,
|
// advancedRendering: true,
|
||||||
// externalLinksNewTab: true,
|
// externalLinksNewTab: true,
|
||||||
// notUsingBlockers: false,
|
// notUsingBlockers: false,
|
||||||
|
|||||||
@@ -452,6 +452,16 @@
|
|||||||
{{ formatCategory(currentPlatform) }}.
|
{{ formatCategory(currentPlatform) }}.
|
||||||
</p>
|
</p>
|
||||||
</AutomaticAccordion>
|
</AutomaticAccordion>
|
||||||
|
<ServersPromo
|
||||||
|
v-if="flags.showProjectPageDownloadModalServersPromo"
|
||||||
|
:link="`/servers#plan`"
|
||||||
|
@close="
|
||||||
|
() => {
|
||||||
|
flags.showProjectPageDownloadModalServersPromo = false;
|
||||||
|
saveFeatureFlags();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
@@ -495,6 +505,64 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
|
||||||
|
theme="dismissable-prompt"
|
||||||
|
:triggers="[]"
|
||||||
|
:shown="flags.showProjectPageCreateServersTooltip"
|
||||||
|
:auto-hide="false"
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<ButtonStyled size="large" circular>
|
||||||
|
<nuxt-link
|
||||||
|
v-tooltip="'Create a server'"
|
||||||
|
:to="`/servers?project=${project.id}#plan`"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
flags.showProjectPageCreateServersTooltip = false;
|
||||||
|
saveFeatureFlags();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ServerPlusIcon aria-hidden="true" />
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
<template #popper>
|
||||||
|
<div class="experimental-styles-within flex max-w-60 flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<h3 class="m-0 flex items-center gap-2 text-base font-bold text-contrast">
|
||||||
|
Create a server
|
||||||
|
<TagItem
|
||||||
|
:style="{
|
||||||
|
'--_color': 'var(--color-brand)',
|
||||||
|
'--_bg-color': 'var(--color-brand-highlight)',
|
||||||
|
}"
|
||||||
|
>New</TagItem
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
<ButtonStyled size="small" circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="`Don't show again`"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
flags.showProjectPageCreateServersTooltip = false;
|
||||||
|
saveFeatureFlags();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
||||||
|
Modrinth Servers is the easiest way to play with your friends without hassle!
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
||||||
|
Starting at $5<span class="text-xs"> / month</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tooltip>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
size="large"
|
size="large"
|
||||||
@@ -850,12 +918,14 @@ import {
|
|||||||
ReportIcon,
|
ReportIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
ServerPlusIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TagsIcon,
|
TagsIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
ModrinthIcon,
|
ModrinthIcon,
|
||||||
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -872,6 +942,8 @@ import {
|
|||||||
ProjectSidebarLinks,
|
ProjectSidebarLinks,
|
||||||
ProjectStatusBadge,
|
ProjectStatusBadge,
|
||||||
ScrollablePanel,
|
ScrollablePanel,
|
||||||
|
TagItem,
|
||||||
|
ServersPromo,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||||
@@ -885,6 +957,7 @@ import {
|
|||||||
} from "@modrinth/utils";
|
} from "@modrinth/utils";
|
||||||
import { navigateTo } from "#app";
|
import { navigateTo } from "#app";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { Tooltip } from "floating-vue";
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||||
@@ -898,6 +971,7 @@ import NavTabs from "~/components/ui/NavTabs.vue";
|
|||||||
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
||||||
import { userCollectProject } from "~/composables/user.js";
|
import { userCollectProject } from "~/composables/user.js";
|
||||||
import { reportProject } from "~/utils/report-helpers.ts";
|
import { reportProject } from "~/utils/report-helpers.ts";
|
||||||
|
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
||||||
|
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
@@ -1311,6 +1385,10 @@ const description = computed(
|
|||||||
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
|
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canCreateServerFrom = computed(() => {
|
||||||
|
return project.value.project_type === "modpack" && project.value.server_side !== "unsupported";
|
||||||
|
});
|
||||||
|
|
||||||
if (!route.name.startsWith("type-id-settings")) {
|
if (!route.name.startsWith("type-id-settings")) {
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: () => title.value,
|
title: () => title.value,
|
||||||
@@ -1679,4 +1757,33 @@ const navLinks = computed(() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.servers-popup {
|
||||||
|
box-shadow:
|
||||||
|
0 0 12px 1px rgba(0, 175, 92, 0.6),
|
||||||
|
var(--shadow-floating);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid var(--color-button-bg);
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: 17px;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-bottom: 5px solid var(--color-raised-bg);
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -500,6 +500,7 @@
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
id="plan"
|
id="plan"
|
||||||
|
pyro-hash="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"
|
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="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
||||||
@@ -603,7 +604,9 @@
|
|||||||
<RightArrowIcon class="shrink-0" />
|
<RightArrowIcon class="shrink-0" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<p class="m-0 text-sm">Starting at $3/GB RAM</p>
|
<p v-if="lowestPrice" class="m-0 text-sm">
|
||||||
|
Starting at {{ formatPrice(locale, lowestPrice, selectedCurrency, true) }} / month
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -622,20 +625,34 @@ import {
|
|||||||
VersionIcon,
|
VersionIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { monthsInInterval } from "@modrinth/ui/src/utils/billing.ts";
|
||||||
|
import { formatPrice } from "@modrinth/utils";
|
||||||
|
import { useVIntl } from "@vintl/vintl";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||||
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
||||||
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
||||||
|
|
||||||
|
const { locale } = useVIntl();
|
||||||
|
|
||||||
const billingPeriods = ref(["monthly", "quarterly"]);
|
const billingPeriods = ref(["monthly", "quarterly"]);
|
||||||
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
|
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
|
||||||
|
|
||||||
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
const pyroProducts = products
|
||||||
|
.filter((p) => p.metadata.type === "pyro")
|
||||||
|
.sort((a, b) => a.metadata.ram - b.metadata.ram);
|
||||||
const pyroPlanProducts = pyroProducts.filter(
|
const pyroPlanProducts = pyroProducts.filter(
|
||||||
(p) => p.metadata.ram === 4096 || p.metadata.ram === 6144 || p.metadata.ram === 8192,
|
(p) => p.metadata.ram === 4096 || p.metadata.ram === 6144 || p.metadata.ram === 8192,
|
||||||
);
|
);
|
||||||
pyroPlanProducts.sort((a, b) => a.metadata.ram - b.metadata.ram);
|
|
||||||
|
const lowestPrice = computed(() => {
|
||||||
|
const amount = pyroProducts[0]?.prices?.find(
|
||||||
|
(price) => price.currency_code === selectedCurrency.value,
|
||||||
|
)?.prices?.intervals?.[billingPeriod.value];
|
||||||
|
return amount ? amount / monthsInInterval[billingPeriod.value] : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
const title = "Modrinth Servers";
|
const title = "Modrinth Servers";
|
||||||
const description =
|
const description =
|
||||||
@@ -799,6 +816,8 @@ async function fetchPaymentData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedProjectId = ref();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const isAtCapacity = computed(
|
const isAtCapacity = computed(
|
||||||
() => isSmallAtCapacity.value && isMediumAtCapacity.value && isLargeAtCapacity.value,
|
() => isSmallAtCapacity.value && isMediumAtCapacity.value && isLargeAtCapacity.value,
|
||||||
@@ -817,7 +836,12 @@ const scrollToFaq = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(scrollToFaq);
|
onMounted(() => {
|
||||||
|
scrollToFaq();
|
||||||
|
if (route.query?.project) {
|
||||||
|
selectedProjectId.value = route.query?.project;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => route.hash, scrollToFaq);
|
watch(() => route.hash, scrollToFaq);
|
||||||
|
|
||||||
@@ -876,9 +900,9 @@ const selectProduct = async (product) => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (product === "custom") {
|
if (product === "custom") {
|
||||||
purchaseModal.value?.show(billingPeriod.value);
|
purchaseModal.value?.show(billingPeriod.value, undefined, selectedProjectId.value);
|
||||||
} else {
|
} else {
|
||||||
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value);
|
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value, selectedProjectId.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
instantMove: true,
|
instantMove: true,
|
||||||
distance: 8,
|
distance: 8,
|
||||||
},
|
},
|
||||||
|
"dismissable-prompt": {
|
||||||
|
$extend: "dropdown",
|
||||||
|
placement: "bottom-start",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1
packages/assets/icons/badge-check.svg
Normal file
1
packages/assets/icons/badge-check.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
|
||||||
|
After Width: | Height: | Size: 441 B |
1
packages/assets/icons/server-plus.svg
Normal file
1
packages/assets/icons/server-plus.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><path d="M22 12H2M11.1 4H7.2c-.8 0-1.5.4-1.8 1.1L2 12v6c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2v-6l-1.5-3M6 16h0M10 16h0M14.4 4h6M17.4 1v6"/></svg>
|
||||||
|
After Width: | Height: | Size: 297 B |
@@ -44,6 +44,7 @@ import _AlignLeftIcon from './icons/align-left.svg?component'
|
|||||||
import _ArchiveIcon from './icons/archive.svg?component'
|
import _ArchiveIcon from './icons/archive.svg?component'
|
||||||
import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component'
|
import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component'
|
||||||
import _AsteriskIcon from './icons/asterisk.svg?component'
|
import _AsteriskIcon from './icons/asterisk.svg?component'
|
||||||
|
import _BadgeCheckIcon from './icons/badge-check.svg?component'
|
||||||
import _BanIcon from './icons/ban.svg?component'
|
import _BanIcon from './icons/ban.svg?component'
|
||||||
import _BellIcon from './icons/bell.svg?component'
|
import _BellIcon from './icons/bell.svg?component'
|
||||||
import _BellRingIcon from './icons/bell-ring.svg?component'
|
import _BellRingIcon from './icons/bell-ring.svg?component'
|
||||||
@@ -163,6 +164,7 @@ import _ScanEyeIcon from './icons/scan-eye.svg?component'
|
|||||||
import _SearchIcon from './icons/search.svg?component'
|
import _SearchIcon from './icons/search.svg?component'
|
||||||
import _SendIcon from './icons/send.svg?component'
|
import _SendIcon from './icons/send.svg?component'
|
||||||
import _ServerIcon from './icons/server.svg?component'
|
import _ServerIcon from './icons/server.svg?component'
|
||||||
|
import _ServerPlusIcon from './icons/server-plus.svg?component'
|
||||||
import _SettingsIcon from './icons/settings.svg?component'
|
import _SettingsIcon from './icons/settings.svg?component'
|
||||||
import _ShareIcon from './icons/share.svg?component'
|
import _ShareIcon from './icons/share.svg?component'
|
||||||
import _ShieldIcon from './icons/shield.svg?component'
|
import _ShieldIcon from './icons/shield.svg?component'
|
||||||
@@ -264,6 +266,7 @@ export const AlignLeftIcon = _AlignLeftIcon
|
|||||||
export const ArchiveIcon = _ArchiveIcon
|
export const ArchiveIcon = _ArchiveIcon
|
||||||
export const ArrowBigUpDashIcon = _ArrowBigUpDashIcon
|
export const ArrowBigUpDashIcon = _ArrowBigUpDashIcon
|
||||||
export const AsteriskIcon = _AsteriskIcon
|
export const AsteriskIcon = _AsteriskIcon
|
||||||
|
export const BadgeCheckIcon = _BadgeCheckIcon
|
||||||
export const BanIcon = _BanIcon
|
export const BanIcon = _BanIcon
|
||||||
export const BellIcon = _BellIcon
|
export const BellIcon = _BellIcon
|
||||||
export const BellRingIcon = _BellRingIcon
|
export const BellRingIcon = _BellRingIcon
|
||||||
@@ -383,6 +386,7 @@ export const ScanEyeIcon = _ScanEyeIcon
|
|||||||
export const SearchIcon = _SearchIcon
|
export const SearchIcon = _SearchIcon
|
||||||
export const SendIcon = _SendIcon
|
export const SendIcon = _SendIcon
|
||||||
export const ServerIcon = _ServerIcon
|
export const ServerIcon = _ServerIcon
|
||||||
|
export const ServerPlusIcon = _ServerPlusIcon
|
||||||
export const SettingsIcon = _SettingsIcon
|
export const SettingsIcon = _SettingsIcon
|
||||||
export const ShareIcon = _ShareIcon
|
export const ShareIcon = _ShareIcon
|
||||||
export const ShieldIcon = _ShieldIcon
|
export const ShieldIcon = _ShieldIcon
|
||||||
|
|||||||
@@ -822,10 +822,69 @@ a,
|
|||||||
|
|
||||||
// TOOLTIPS
|
// TOOLTIPS
|
||||||
|
|
||||||
|
.v-popper--theme-dropdown,
|
||||||
|
.v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
|
||||||
|
.v-popper__inner {
|
||||||
|
border: 1px solid var(--color-button-bg) !important;
|
||||||
|
padding: var(--gap-sm) !important;
|
||||||
|
width: fit-content !important;
|
||||||
|
border-radius: var(--radius-md) !important;
|
||||||
|
background-color: var(--color-raised-bg) !important;
|
||||||
|
box-shadow: var(--shadow-floating) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-outer {
|
||||||
|
border-color: var(--color-button-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-inner {
|
||||||
|
border-color: var(--color-raised-bg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement='bottom-end'] .v-popper__wrapper {
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement='top-end'] .v-popper__wrapper {
|
||||||
|
transform-origin: bottom right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement='bottom-start'] .v-popper__wrapper {
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement='top-start'] .v-popper__wrapper {
|
||||||
|
transform-origin: bottom left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--show-from .v-popper__wrapper {
|
||||||
|
transform: scale(0.85);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--show-to .v-popper__wrapper {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
transition:
|
||||||
|
transform 0.125s ease-in-out,
|
||||||
|
opacity 0.125s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--hide-from .v-popper__wrapper {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
transition: transform 0.0625s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--hide-to .v-popper__wrapper {
|
||||||
|
//transform: scale(.9);
|
||||||
|
}
|
||||||
|
|
||||||
.v-popper--theme-tooltip {
|
.v-popper--theme-tooltip {
|
||||||
.v-popper__inner {
|
.v-popper__inner {
|
||||||
background: var(--color-tooltip-bg) !important;
|
background: var(--color-tooltip-bg) !important;
|
||||||
color: var(--color-tooltip-text) !important;
|
color: initial !important;
|
||||||
padding: 0.5rem 0.5rem !important;
|
padding: 0.5rem 0.5rem !important;
|
||||||
border-radius: var(--radius-sm) !important;
|
border-radius: var(--radius-sm) !important;
|
||||||
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
|
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
|
||||||
@@ -840,6 +899,30 @@ a,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-popper--theme-dismissable-prompt {
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.v-popper__inner {
|
||||||
|
background: var(--color-raised-bg) !important;
|
||||||
|
border: 1px solid var(--color-button-border);
|
||||||
|
color: var(--color-tooltip-text) !important;
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
border-radius: 0.75rem !important;
|
||||||
|
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-outer {
|
||||||
|
border-color: var(--color-button-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-inner {
|
||||||
|
border-color: var(--color-raised-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARKDOWN
|
// MARKDOWN
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
@@ -1205,65 +1288,6 @@ select {
|
|||||||
border-top-right-radius: var(--radius-md) !important;
|
border-top-right-radius: var(--radius-md) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-popper--theme-dropdown,
|
|
||||||
.v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
|
|
||||||
.v-popper__inner {
|
|
||||||
border: 1px solid var(--color-button-bg) !important;
|
|
||||||
padding: var(--gap-sm) !important;
|
|
||||||
width: fit-content !important;
|
|
||||||
border-radius: var(--radius-md) !important;
|
|
||||||
background-color: var(--color-raised-bg) !important;
|
|
||||||
box-shadow: var(--shadow-floating) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__arrow-outer {
|
|
||||||
border-color: var(--color-button-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__arrow-inner {
|
|
||||||
border-color: var(--color-raised-bg) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper[data-popper-placement='bottom-end'] .v-popper__wrapper {
|
|
||||||
transform-origin: top right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper[data-popper-placement='top-end'] .v-popper__wrapper {
|
|
||||||
transform-origin: bottom right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper[data-popper-placement='bottom-start'] .v-popper__wrapper {
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper[data-popper-placement='top-start'] .v-popper__wrapper {
|
|
||||||
transform-origin: bottom left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper.v-popper__popper--show-from .v-popper__wrapper {
|
|
||||||
transform: scale(0.85);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper.v-popper__popper--show-to .v-popper__wrapper {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
transition:
|
|
||||||
transform 0.125s ease-in-out,
|
|
||||||
opacity 0.125s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper.v-popper__popper--hide-from .v-popper__wrapper {
|
|
||||||
transform: none;
|
|
||||||
opacity: 1;
|
|
||||||
transition: transform 0.0625s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper.v-popper__popper--hide-to .v-popper__wrapper {
|
|
||||||
//transform: scale(.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-radio {
|
.preview-radio {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const selectedPlan = ref<ServerPlan>()
|
|||||||
const selectedInterval = ref<ServerBillingInterval>('quarterly')
|
const selectedInterval = ref<ServerBillingInterval>('quarterly')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selectedRegion = ref<string>()
|
const selectedRegion = ref<string>()
|
||||||
|
const projectId = ref<string>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initializeStripe,
|
initializeStripe,
|
||||||
@@ -85,6 +86,7 @@ const {
|
|||||||
selectedPlan,
|
selectedPlan,
|
||||||
selectedInterval,
|
selectedInterval,
|
||||||
selectedRegion,
|
selectedRegion,
|
||||||
|
projectId,
|
||||||
props.initiatePayment,
|
props.initiatePayment,
|
||||||
props.onError,
|
props.onError,
|
||||||
)
|
)
|
||||||
@@ -201,7 +203,7 @@ watch(selectedPlan, () => {
|
|||||||
console.log(selectedPlan.value)
|
console.log(selectedPlan.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
function begin(interval: ServerBillingInterval, plan?: ServerPlan, project?: string) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
selectedPlan.value = plan
|
selectedPlan.value = plan
|
||||||
selectedInterval.value = interval
|
selectedInterval.value = interval
|
||||||
@@ -209,6 +211,7 @@ function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
|||||||
selectedPaymentMethod.value = undefined
|
selectedPaymentMethod.value = undefined
|
||||||
currentStep.value = steps[0]
|
currentStep.value = steps[0]
|
||||||
skipPaymentMethods.value = true
|
skipPaymentMethods.value = true
|
||||||
|
projectId.value = project
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +256,8 @@ defineExpose({
|
|||||||
:pings="pings"
|
:pings="pings"
|
||||||
:custom="customServer"
|
:custom="customServer"
|
||||||
:available-products="availableProducts"
|
:available-products="availableProducts"
|
||||||
|
:currency="currency"
|
||||||
|
:interval="selectedInterval"
|
||||||
:fetch-stock="fetchStock"
|
:fetch-stock="fetchStock"
|
||||||
/>
|
/>
|
||||||
<PaymentMethodSelector
|
<PaymentMethodSelector
|
||||||
|
|||||||
@@ -4,19 +4,28 @@ import { defineMessages, useVIntl } from '@vintl/vintl'
|
|||||||
import { IntlFormatted } from '@vintl/vintl/components'
|
import { IntlFormatted } from '@vintl/vintl/components'
|
||||||
import { onMounted, ref, computed, watch } from 'vue'
|
import { onMounted, ref, computed, watch } from 'vue'
|
||||||
import type { RegionPing } from './ModrinthServersPurchaseModal.vue'
|
import type { RegionPing } from './ModrinthServersPurchaseModal.vue'
|
||||||
import type { ServerPlan, ServerRegion, ServerStockRequest } from '../../utils/billing'
|
import {
|
||||||
|
monthsInInterval,
|
||||||
|
type ServerBillingInterval,
|
||||||
|
type ServerPlan,
|
||||||
|
type ServerRegion,
|
||||||
|
type ServerStockRequest,
|
||||||
|
} from '../../utils/billing'
|
||||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||||
import Slider from '../base/Slider.vue'
|
import Slider from '../base/Slider.vue'
|
||||||
import { SpinnerIcon, XIcon, InfoIcon } from '@modrinth/assets'
|
import { SpinnerIcon, XIcon, InfoIcon } from '@modrinth/assets'
|
||||||
import ServersSpecs from './ServersSpecs.vue'
|
import ServersSpecs from './ServersSpecs.vue'
|
||||||
|
import { formatPrice } from '../../../../utils'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage, locale } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
regions: ServerRegion[]
|
regions: ServerRegion[]
|
||||||
pings: RegionPing[]
|
pings: RegionPing[]
|
||||||
fetchStock: (region: ServerRegion, request: ServerStockRequest) => Promise<number>
|
fetchStock: (region: ServerRegion, request: ServerStockRequest) => Promise<number>
|
||||||
custom: boolean
|
custom: boolean
|
||||||
|
currency: string
|
||||||
|
interval: ServerBillingInterval
|
||||||
availableProducts: ServerPlan[]
|
availableProducts: ServerPlan[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -25,6 +34,12 @@ const checkingCustomStock = ref(false)
|
|||||||
const selectedPlan = defineModel<ServerPlan>('plan')
|
const selectedPlan = defineModel<ServerPlan>('plan')
|
||||||
const selectedRegion = defineModel<string>('region')
|
const selectedRegion = defineModel<string>('region')
|
||||||
|
|
||||||
|
const selectedPrice = computed(() => {
|
||||||
|
const amount = selectedPlan.value?.prices?.find((price) => price.currency_code === props.currency)
|
||||||
|
?.prices?.intervals?.[props.interval]
|
||||||
|
return amount ? amount / monthsInInterval[props.interval] : undefined
|
||||||
|
})
|
||||||
|
|
||||||
const regionOrder: string[] = ['us-vin', 'eu-lim']
|
const regionOrder: string[] = ['us-vin', 'eu-lim']
|
||||||
|
|
||||||
const sortedRegions = computed(() => {
|
const sortedRegions = computed(() => {
|
||||||
@@ -216,7 +231,12 @@ onMounted(() => {
|
|||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
|
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
|
||||||
<div class="bg-bg rounded-xl p-4 mt-4 text-secondary">
|
<p v-if="selectedPrice" class="mt-2 mb-0">
|
||||||
|
<span class="text-contrast text-lg font-bold"
|
||||||
|
>{{ formatPrice(locale, selectedPrice, currency, true) }} / month</span
|
||||||
|
><span v-if="interval !== 'monthly'">, billed {{ interval }}</span>
|
||||||
|
</p>
|
||||||
|
<div class="bg-bg rounded-xl p-4 mt-2 text-secondary">
|
||||||
<div v-if="checkingCustomStock" class="flex gap-2 items-center">
|
<div v-if="checkingCustomStock" class="flex gap-2 items-center">
|
||||||
<SpinnerIcon class="size-5 shrink-0 animate-spin" /> Checking availability...
|
<SpinnerIcon class="size-5 shrink-0 animate-spin" /> Checking availability...
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { ServerBillingInterval, ServerPlan, ServerRegion } from '../../utils/billing'
|
import {
|
||||||
|
monthsInInterval,
|
||||||
|
type ServerBillingInterval,
|
||||||
|
type ServerPlan,
|
||||||
|
type ServerRegion,
|
||||||
|
} from '../../utils/billing'
|
||||||
import TagItem from '../base/TagItem.vue'
|
import TagItem from '../base/TagItem.vue'
|
||||||
import ServersSpecs from './ServersSpecs.vue'
|
import ServersSpecs from './ServersSpecs.vue'
|
||||||
import { formatPrice, getPingLevel } from '@modrinth/utils'
|
import { formatPrice, getPingLevel } from '@modrinth/utils'
|
||||||
@@ -77,12 +82,6 @@ const period = computed(() => {
|
|||||||
return '???'
|
return '???'
|
||||||
})
|
})
|
||||||
|
|
||||||
const monthsInInterval: Record<ServerBillingInterval, number> = {
|
|
||||||
monthly: 1,
|
|
||||||
quarterly: 3,
|
|
||||||
yearly: 12,
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInterval(newInterval: ServerBillingInterval) {
|
function setInterval(newInterval: ServerBillingInterval) {
|
||||||
interval.value = newInterval
|
interval.value = newInterval
|
||||||
emit('reloadPaymentIntent')
|
emit('reloadPaymentIntent')
|
||||||
|
|||||||
@@ -109,5 +109,6 @@ export { default as VersionSummary } from './version/VersionSummary.vue'
|
|||||||
export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
||||||
|
|
||||||
// Servers
|
// Servers
|
||||||
|
export { default as ServersPromo } from './servers/ServersPromo.vue'
|
||||||
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'
|
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'
|
||||||
export { default as ServersSpecs } from './billing/ServersSpecs.vue'
|
export { default as ServersSpecs } from './billing/ServersSpecs.vue'
|
||||||
|
|||||||
60
packages/ui/src/components/servers/ServersPromo.vue
Normal file
60
packages/ui/src/components/servers/ServersPromo.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RightArrowIcon, ModrinthIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||||
|
import AutoLink from '../base/AutoLink.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
link: string
|
||||||
|
closable?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
closable: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="brand-gradient-bg card-shadow bg-bg relative p-4 border-[1px] border-solid border-brand rounded-2xl grid grid-cols-[1fr_auto] overflow-hidden"
|
||||||
|
>
|
||||||
|
<ModrinthIcon
|
||||||
|
class="absolute -top-12 -right-12 size-48 text-brand-highlight opacity-25"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--color-brand)"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-lg leading-tight font-extrabold text-contrast"
|
||||||
|
>Want to play with <br />
|
||||||
|
<span class="text-brand">your friends?</span></span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium">Create a server with Modrinth in just a few clicks.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end justify-end z-10">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<AutoLink :to="link"> View plans <RightArrowIcon /> </AutoLink>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-2 right-2 z-10">
|
||||||
|
<ButtonStyled v-if="closable" size="small" circular>
|
||||||
|
<button v-tooltip="`Don't show again`" @click="emit('close')">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.brand-gradient-bg {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to top right,
|
||||||
|
var(--color-brand-highlight) -80%,
|
||||||
|
var(--color-bg)
|
||||||
|
);
|
||||||
|
--color-button-bg: var(--brand-gradient-button);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -31,6 +31,7 @@ export const useStripe = (
|
|||||||
product: Ref<ServerPlan | undefined>,
|
product: Ref<ServerPlan | undefined>,
|
||||||
interval: Ref<ServerBillingInterval>,
|
interval: Ref<ServerBillingInterval>,
|
||||||
region: Ref<string | undefined>,
|
region: Ref<string | undefined>,
|
||||||
|
project: Ref<string | undefined>,
|
||||||
initiatePayment: (
|
initiatePayment: (
|
||||||
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
||||||
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse>,
|
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse>,
|
||||||
@@ -222,16 +223,22 @@ export const useStripe = (
|
|||||||
|
|
||||||
let result: BasePaymentIntentResponse
|
let result: BasePaymentIntentResponse
|
||||||
|
|
||||||
|
const metadata: CreatePaymentIntentRequest['metadata'] = {
|
||||||
|
type: 'pyro',
|
||||||
|
server_region: region.value,
|
||||||
|
source: project.value
|
||||||
|
? {
|
||||||
|
project_id: project.value,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}
|
||||||
|
|
||||||
if (paymentIntentId.value) {
|
if (paymentIntentId.value) {
|
||||||
result = await updateIntent({
|
result = await updateIntent({
|
||||||
...requestType,
|
...requestType,
|
||||||
charge,
|
charge,
|
||||||
existing_payment_intent: paymentIntentId.value,
|
existing_payment_intent: paymentIntentId.value,
|
||||||
metadata: {
|
metadata,
|
||||||
type: 'pyro',
|
|
||||||
server_region: region.value,
|
|
||||||
source: {},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
console.log(`Updated payment intent: ${interval.value} for ${result.total}`)
|
console.log(`Updated payment intent: ${interval.value} for ${result.total}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -242,11 +249,7 @@ export const useStripe = (
|
|||||||
} = await createIntent({
|
} = await createIntent({
|
||||||
...requestType,
|
...requestType,
|
||||||
charge,
|
charge,
|
||||||
metadata: {
|
metadata: metadata,
|
||||||
type: 'pyro',
|
|
||||||
server_region: region.value,
|
|
||||||
source: {},
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
console.log(`Created payment intent: ${interval.value} for ${result.total}`)
|
console.log(`Created payment intent: ${interval.value} for ${result.total}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import type Stripe from 'stripe'
|
import type Stripe from 'stripe'
|
||||||
|
import type { Loaders } from '@modrinth/utils'
|
||||||
|
|
||||||
export type ServerBillingInterval = 'monthly' | 'yearly' | 'quarterly'
|
export type ServerBillingInterval = 'monthly' | 'yearly' | 'quarterly'
|
||||||
|
|
||||||
|
export const monthsInInterval: Record<ServerBillingInterval, number> = {
|
||||||
|
monthly: 1,
|
||||||
|
quarterly: 3,
|
||||||
|
yearly: 12,
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerPlan {
|
export interface ServerPlan {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -72,11 +79,18 @@ export type CreatePaymentIntentRequest = PaymentRequestType & {
|
|||||||
type: 'pyro'
|
type: 'pyro'
|
||||||
server_name?: string
|
server_name?: string
|
||||||
server_region?: string
|
server_region?: string
|
||||||
source: {
|
source:
|
||||||
loader?: string
|
| {
|
||||||
game_version?: string
|
loader: Loaders
|
||||||
loader_version?: string
|
game_version?: string
|
||||||
}
|
loader_version?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
project_id: string
|
||||||
|
version_id?: string
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
| {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user