You've already forked AstralRinth
* feat: abstract api-client DI into ui package * feat: cross platform page system * feat: tanstack as cross platform useAsyncData * feat: archon servers routes + labrinth billing routes * fix: dont use partial * feat: migrate server list page to tanstack + api-client + re-enabled broken features! * feat: migrate servers manage page to api-client before page system * feat: migrate manage page to page system * fix: type issues * fix: upgrade wrapper bugs * refactor: move state types into api-client * feat: disable financial stuff on app frontend * feat: finalize cross platform page system for now * fix: lint * fix: build issues * feat: remove papaparse * fix: lint * fix: interface error --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
149 lines
4.3 KiB
Vue
149 lines
4.3 KiB
Vue
<script setup lang="ts">
|
|
import type { Labrinth } from '@modrinth/api-client'
|
|
import { InfoIcon } from '@modrinth/assets'
|
|
import { formatPrice } from '@modrinth/utils'
|
|
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
|
import { Menu } from 'floating-vue'
|
|
import { computed, inject, type Ref } from 'vue'
|
|
|
|
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
|
|
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
|
|
import ServersSpecs from './ServersSpecs.vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
plan: Labrinth.Billing.Internal.Product
|
|
title: MessageDescriptor
|
|
description: MessageDescriptor
|
|
buttonColor?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
|
mostPopular?: boolean
|
|
selected?: boolean
|
|
}>(),
|
|
{
|
|
buttonColor: 'standard',
|
|
mostPopular: false,
|
|
selected: false,
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'select', plan: Labrinth.Billing.Internal.Product): void
|
|
}>()
|
|
|
|
const { formatMessage, locale } = useVIntl()
|
|
|
|
// TODO: Use DI framework when merged.
|
|
const selectedInterval = inject<Ref<ServerBillingInterval>>('selectedInterval')
|
|
const currency = inject<string>('currency')
|
|
|
|
const perMonth = computed(() => {
|
|
if (!props.plan || !currency || !selectedInterval?.value) return undefined
|
|
const total = getPriceForInterval(props.plan, currency, selectedInterval.value)
|
|
if (!total) return undefined
|
|
return total / monthsInInterval[selectedInterval.value]
|
|
})
|
|
|
|
const planSpecs = computed(() => {
|
|
const metadata = props.plan.metadata
|
|
if (metadata.type === 'pyro' || metadata.type === 'medal') {
|
|
return {
|
|
ram: metadata.ram,
|
|
storage: metadata.storage,
|
|
cpu: metadata.cpu,
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
const mostPopularStyle = computed(() => {
|
|
if (!props.mostPopular) return undefined
|
|
const style: Record<string, string> = {
|
|
backgroundImage:
|
|
'radial-gradient(86.12% 101.64% at 95.97% 94.07%, rgba(27, 217, 106, 0.23) 0%, rgba(14, 115, 56, 0.2) 100%)',
|
|
boxShadow: '0px 12px 38.1px rgba(27, 217, 106, 0.13)',
|
|
}
|
|
|
|
if (!props.selected) {
|
|
style.borderColor = 'rgba(12, 107, 52, 0.55)'
|
|
}
|
|
|
|
return style
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="rounded-2xl p-4 font-semibold transition-all duration-300 experimental-styles-within h-full border-2 border-solid cursor-pointer select-none"
|
|
:class="{
|
|
'bg-brand-highlight border-brand': selected,
|
|
'bg-button-bg border-transparent': !selected,
|
|
'!bg-bg': mostPopular,
|
|
}"
|
|
:style="mostPopularStyle"
|
|
role="button"
|
|
tabindex="0"
|
|
:aria-pressed="selected"
|
|
@click="emit('select', plan)"
|
|
@keydown.enter.prevent="emit('select', plan)"
|
|
@keydown.space.prevent="emit('select', plan)"
|
|
>
|
|
<div class="flex h-full flex-col justify-between gap-2">
|
|
<div class="flex flex-col">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-2xl font-semibold text-contrast">
|
|
{{ formatMessage(title) }}
|
|
</span>
|
|
<div
|
|
v-if="mostPopular"
|
|
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
|
|
>
|
|
Most Popular
|
|
</div>
|
|
</div>
|
|
<span class="m-0 text-lg font-bold text-contrast">
|
|
{{ formatPrice(locale, perMonth, currency, true) }}
|
|
<span class="text-sm font-semibold text-secondary">
|
|
/ month{{ selectedInterval !== 'monthly' ? `, billed ${selectedInterval}` : '' }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
<span class="text-sm">{{ formatMessage(description) }}</span>
|
|
|
|
<div class="w-fit">
|
|
<Menu
|
|
placement="bottom-start"
|
|
:triggers="['hover', 'focus']"
|
|
:auto-hide="true"
|
|
:delay="{ show: 100, hide: 120 }"
|
|
:distance="6"
|
|
>
|
|
<template #default="{ shown }">
|
|
<div
|
|
class="flex w-fit items-center gap-2 cursor-help text-sm font-medium cursor-default select-none outline-none"
|
|
:class="shown ? 'text-primary' : 'text-secondary'"
|
|
role="button"
|
|
tabindex="0"
|
|
aria-haspopup="true"
|
|
:aria-expanded="shown"
|
|
>
|
|
<InfoIcon />
|
|
View plan details
|
|
</div>
|
|
</template>
|
|
|
|
<template #popper>
|
|
<div v-if="planSpecs" class="w-fit rounded-md border border-contrast/10 p-3 shadow-lg">
|
|
<ServersSpecs
|
|
:ram="planSpecs.ram"
|
|
:storage="planSpecs.storage"
|
|
:cpus="planSpecs.cpu"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Menu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|