You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit 'd51a1c47c70d44bfcc1af6fe58f244170513470c' into feature-clean
This commit is contained in:
@@ -140,6 +140,8 @@
|
||||
right: 1rem;
|
||||
z-index: 100;
|
||||
max-width: calc(100% - 2rem);
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow-y: auto;
|
||||
|
||||
> div {
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
|
||||
|
||||
@@ -19,13 +19,21 @@
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div v-else-if="report.item_type === 'user'" class="item-info">
|
||||
<nuxt-link :to="`/user/${report.user.username}`" class="iconified-stacked-link">
|
||||
<nuxt-link
|
||||
v-if="report.user"
|
||||
:to="`/user/${report.user.username}`"
|
||||
class="iconified-stacked-link"
|
||||
>
|
||||
<Avatar :src="report.user.avatar_url" circle size="xs" no-shadow :raised="raised" />
|
||||
<div class="stacked">
|
||||
<span class="title">{{ report.user.username }}</span>
|
||||
<span>User</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<div v-else class="item-info">
|
||||
<div class="backed-svg" :class="{ raised: raised }"><UnknownIcon /></div>
|
||||
<span>Reported user not found: <CopyCode :text="report.item_id" /> </span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="report.item_type === 'version'" class="item-info">
|
||||
<nuxt-link
|
||||
@@ -50,7 +58,7 @@
|
||||
</div>
|
||||
<div v-else class="item-info">
|
||||
<div class="backed-svg" :class="{ raised: raised }"><UnknownIcon /></div>
|
||||
<span>Unknown report type</span>
|
||||
<span>Unknown report type: {{ report.item_type }}</span>
|
||||
</div>
|
||||
<div class="report-type">
|
||||
<Badge v-if="report.closed" type="closed" />
|
||||
|
||||
@@ -5,6 +5,27 @@
|
||||
<div class="pointer-events-none absolute inset-0 z-[-1]">
|
||||
<div id="absolute-background-teleport" class="relative"></div>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-0 z-50">
|
||||
<div
|
||||
class="over-the-top-random-animation"
|
||||
:style="{ '--_r-count': rCount }"
|
||||
:class="{ threshold: rCount > 20, 'rings-expand': rCount >= 40 }"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight text-9xl font-extrabold text-contrast"
|
||||
>
|
||||
?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
|
||||
<div
|
||||
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
|
||||
@@ -206,7 +227,6 @@
|
||||
<template #modpacks> <PackageOpenIcon aria-hidden="true" /> Modpacks </template>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
:highlighted="
|
||||
@@ -231,14 +251,52 @@
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<ButtonStyled type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="auth.user && isStaff(auth.user)"
|
||||
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
||||
position="bottom"
|
||||
direction="left"
|
||||
:dropdown-id="`${basePopoutId}-staff`"
|
||||
aria-label="Create new..."
|
||||
:options="[
|
||||
{
|
||||
id: 'review-projects',
|
||||
color: 'orange',
|
||||
link: '/moderation/review',
|
||||
},
|
||||
{
|
||||
id: 'review-reports',
|
||||
color: 'orange',
|
||||
link: '/moderation/reports',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: isAdmin(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'user-lookup',
|
||||
color: 'primary',
|
||||
link: '/admin/user_email',
|
||||
shown: isAdmin(auth.user),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<ModrinthIcon aria-hidden="true" />
|
||||
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
|
||||
<template #review-projects> <ScaleIcon aria-hidden="true" /> Review projects </template>
|
||||
<template #review-reports> <ReportIcon aria-hidden="true" /> Reports </template>
|
||||
<template #user-lookup> <UserIcon aria-hidden="true" /> Lookup by email </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="auth.user"
|
||||
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
||||
position="bottom"
|
||||
direction="left"
|
||||
:dropdown-id="createPopoutId"
|
||||
:dropdown-id="`${basePopoutId}-create`"
|
||||
aria-label="Create new..."
|
||||
:options="[
|
||||
{
|
||||
@@ -270,7 +328,7 @@
|
||||
</ButtonStyled>
|
||||
<OverflowMenu
|
||||
v-if="auth.user"
|
||||
:dropdown-id="userPopoutId"
|
||||
:dropdown-id="`${basePopoutId}-user`"
|
||||
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
||||
:options="userMenuOptions"
|
||||
>
|
||||
@@ -291,7 +349,7 @@
|
||||
</template>
|
||||
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template>
|
||||
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template>
|
||||
<template #moderation> <ModerationIcon aria-hidden="true" /> Moderation </template>
|
||||
<template #moderation> <ScaleIcon aria-hidden="true" /> Moderation </template>
|
||||
<template #sign-out> <LogOutIcon aria-hidden="true" /> Sign out </template>
|
||||
</OverflowMenu>
|
||||
<template v-else>
|
||||
@@ -378,7 +436,7 @@
|
||||
class="iconified-button"
|
||||
to="/moderation"
|
||||
>
|
||||
<ModerationIcon aria-hidden="true" />
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.moderationLabel) }}
|
||||
</NuxtLink>
|
||||
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
|
||||
@@ -439,7 +497,7 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<NotificationIcon aria-hidden="true" />
|
||||
<BellIcon aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/dashboard"
|
||||
@@ -458,7 +516,7 @@
|
||||
>
|
||||
<template v-if="!auth.user">
|
||||
<HamburgerIcon v-if="!isMobileMenuOpen" aria-hidden="true" />
|
||||
<CrossIcon v-else aria-hidden="true" />
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Avatar
|
||||
@@ -568,6 +626,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
ModrinthIcon,
|
||||
ArrowBigUpDashIcon,
|
||||
BookmarkIcon,
|
||||
ServerIcon,
|
||||
@@ -605,12 +664,11 @@ import {
|
||||
TwitterIcon,
|
||||
MastodonIcon,
|
||||
GitHubIcon,
|
||||
XIcon as CrossIcon,
|
||||
ScaleIcon as ModerationIcon,
|
||||
BellIcon as NotificationIcon,
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Button, ButtonStyled, OverflowMenu, Avatar, commonMessages } from "@modrinth/ui";
|
||||
|
||||
import { isAdmin, isStaff } from "@modrinth/utils";
|
||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
@@ -628,10 +686,10 @@ const flags = useFeatureFlags();
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const route = useNativeRoute();
|
||||
const router = useNativeRouter();
|
||||
const link = config.public.siteUrl + route.path.replace(/\/+$/, "");
|
||||
|
||||
const createPopoutId = useId();
|
||||
const userPopoutId = useId();
|
||||
const basePopoutId = useId();
|
||||
|
||||
const verifyEmailBannerMessages = defineMessages({
|
||||
title: {
|
||||
@@ -890,12 +948,57 @@ const isDiscoveringSubpage = computed(
|
||||
() => route.name && route.name.startsWith("type-id") && !route.query.sid,
|
||||
);
|
||||
|
||||
const rCount = ref(0);
|
||||
|
||||
const randomProjects = ref([]);
|
||||
const disableRandomProjects = ref(false);
|
||||
|
||||
const disableRandomProjectsForRoute = computed(
|
||||
() =>
|
||||
route.name.startsWith("servers") ||
|
||||
route.name.includes("settings") ||
|
||||
route.name.includes("admin"),
|
||||
);
|
||||
|
||||
async function onKeyDown(event) {
|
||||
if (disableRandomProjects.value || disableRandomProjectsForRoute.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "r") {
|
||||
rCount.value++;
|
||||
|
||||
if (randomProjects.value.length < 3) {
|
||||
randomProjects.value = await useBaseFetch("projects_random?count=50").catch((err) => {
|
||||
console.error(err);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rCount.value >= 40) {
|
||||
rCount.value = 0;
|
||||
const randomProject = randomProjects.value[0];
|
||||
await router.push(`/project/${randomProject.slug}`);
|
||||
randomProjects.value.splice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(event) {
|
||||
if (event.key === "r") {
|
||||
rCount.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (window && import.meta.client) {
|
||||
window.history.scrollRestoration = "auto";
|
||||
}
|
||||
|
||||
runAnalytics();
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -1482,5 +1585,115 @@ const footerLinks = [
|
||||
background: var(--brand-gradient-strong-bg);
|
||||
border-color: var(--brand-gradient-border);
|
||||
}
|
||||
|
||||
.over-the-top-random-animation {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
scale: 0.5;
|
||||
transition: all 0.5s ease-out;
|
||||
opacity: 0;
|
||||
animation:
|
||||
tilt-shaking calc(0.2s / (max((var(--_r-count) - 20), 1) / 20)) linear infinite,
|
||||
translate-x-shaking calc(0.3s / (max((var(--_r-count) - 20), 1) / 20)) linear infinite,
|
||||
translate-y-shaking calc(0.25s / (max((var(--_r-count) - 20), 1) / 20)) linear infinite;
|
||||
|
||||
&.threshold {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.rings-expand {
|
||||
scale: 0.8;
|
||||
opacity: 0;
|
||||
|
||||
.animation-ring-1 {
|
||||
width: 25rem;
|
||||
height: 25rem;
|
||||
}
|
||||
.animation-ring-2 {
|
||||
width: 50rem;
|
||||
height: 50rem;
|
||||
}
|
||||
.animation-ring-3 {
|
||||
width: 100rem;
|
||||
height: 100rem;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
scale: calc(1 + max((var(--_r-count) - 20), 0) * 0.1);
|
||||
transition: all 0.2s ease-out;
|
||||
width: 20rem;
|
||||
height: 20rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tilt-shaking {
|
||||
0% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
25% {
|
||||
rotate: calc(1deg * (var(--_r-count) - 20));
|
||||
}
|
||||
50% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
75% {
|
||||
rotate: calc(-1deg * (var(--_r-count) - 20));
|
||||
}
|
||||
100% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes translate-x-shaking {
|
||||
0% {
|
||||
translate: 0;
|
||||
}
|
||||
25% {
|
||||
translate: calc(2px * (var(--_r-count) - 20));
|
||||
}
|
||||
50% {
|
||||
translate: 0;
|
||||
}
|
||||
75% {
|
||||
translate: calc(-2px * (var(--_r-count) - 20));
|
||||
}
|
||||
100% {
|
||||
translate: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes translate-y-shaking {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(calc(2px * (var(--_r-count) - 20)));
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(calc(-2px * (var(--_r-count) - 20)));
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
@@ -98,6 +98,14 @@
|
||||
action: () => (auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in')),
|
||||
shown: !currentMember,
|
||||
},
|
||||
{ divider: true, shown: currentMember || flags.developerMode },
|
||||
{
|
||||
id: 'copy-id',
|
||||
action: () => {
|
||||
copyToClipboard(version.id);
|
||||
},
|
||||
shown: currentMember || flags.developerMode,
|
||||
},
|
||||
{ divider: true, shown: currentMember },
|
||||
{
|
||||
id: 'edit',
|
||||
@@ -148,6 +156,10 @@
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy ID
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -174,6 +186,7 @@ import {
|
||||
ReportIcon,
|
||||
UploadIcon,
|
||||
InfoIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from "@modrinth/assets";
|
||||
import DropArea from "~/components/ui/DropArea.vue";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
|
||||
@@ -58,50 +58,137 @@
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<div class="normal-page no-sidebar">
|
||||
<h1>{{ user.username }}'s subscriptions</h1>
|
||||
<div class="normal-page__content">
|
||||
<div class="page experimental-styles-within">
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar :src="user.avatar_url" :alt="user.username" size="32px" circle />
|
||||
<h1 class="m-0 text-2xl font-extrabold">{{ user.username }}'s subscriptions</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<nuxt-link :to="`/user/${user.id}`">
|
||||
<UserIcon aria-hidden="true" />
|
||||
User profile
|
||||
<ExternalIcon class="h-4 w-4" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-for="subscription in subscriptionCharges" :key="subscription.id" class="card">
|
||||
<span class="font-extrabold text-contrast">
|
||||
<template v-if="subscription.product.metadata.type === 'midas'"> Modrinth Plus </template>
|
||||
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
||||
Modrinth Servers
|
||||
</template>
|
||||
<template v-else> Unknown product </template>
|
||||
<template v-if="subscription.interval">
|
||||
{{ subscription.interval }}
|
||||
</template>
|
||||
</span>
|
||||
<div class="mb-4 mt-2 flex items-center gap-1">
|
||||
{{ subscription.status }} ⋅ {{ $dayjs(subscription.created).format("YYYY-MM-DD") }}
|
||||
<template v-if="subscription.metadata?.id"> ⋅ {{ subscription.metadata.id }}</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="charge in subscription.charges"
|
||||
:key="charge.id"
|
||||
class="universal-card recessed flex items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<Badge
|
||||
:color="charge.status === 'succeeded' ? 'green' : 'red'"
|
||||
:type="charge.status"
|
||||
/>
|
||||
⋅
|
||||
{{ charge.type }}
|
||||
⋅
|
||||
{{ $dayjs(charge.due).format("YYYY-MM-DD") }}
|
||||
⋅
|
||||
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
||||
<template v-if="subscription.interval"> ⋅ {{ subscription.interval }} </template>
|
||||
<div class="mb-4 grid grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<span class="flex items-center gap-2 font-semibold text-contrast">
|
||||
<template v-if="subscription.product.metadata.type === 'midas'">
|
||||
<ModrinthPlusIcon class="h-7 w-min" />
|
||||
</template>
|
||||
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
||||
<ModrinthServersIcon class="h-7 w-min" />
|
||||
</template>
|
||||
<template v-else> Unknown product </template>
|
||||
</span>
|
||||
<div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary">
|
||||
{{ formatCategory(subscription.interval) }} ⋅ {{ subscription.status }} ⋅
|
||||
{{ dayjs(subscription.created).format("MMMM D, YYYY [at] h:mma") }} ({{
|
||||
dayjs(subscription.created).fromNow()
|
||||
}})
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="subscription.metadata?.id" class="flex flex-col items-end gap-2">
|
||||
<ButtonStyled v-if="subscription.product.metadata.type === 'pyro'">
|
||||
<nuxt-link
|
||||
:to="`/servers/manage/${subscription.metadata.id}`"
|
||||
target="_blank"
|
||||
class="w-fit"
|
||||
>
|
||||
<ServerIcon /> Server panel <ExternalIcon class="h-4 w-4" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<CopyCode :text="subscription.metadata.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(charge, index) in subscription.charges"
|
||||
:key="charge.id"
|
||||
class="relative overflow-clip rounded-xl bg-bg px-4 py-3"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 top-0 w-1"
|
||||
:class="charge.type === 'refund' ? 'bg-purple' : chargeStatuses[charge.status].color"
|
||||
/>
|
||||
<div class="grid w-full grid-cols-[1fr_auto] items-center gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span>
|
||||
<span class="font-bold text-contrast">
|
||||
<template v-if="charge.status === 'succeeded'"> Succeeded </template>
|
||||
<template v-else-if="charge.status === 'failed'"> Failed </template>
|
||||
<template v-else-if="charge.status === 'cancelled'"> Cancelled </template>
|
||||
<template v-else-if="charge.status === 'processing'"> Processing </template>
|
||||
<template v-else-if="charge.status === 'open'"> Upcoming </template>
|
||||
<template v-else> {{ charge.status }} </template>
|
||||
</span>
|
||||
⋅
|
||||
<span>
|
||||
<template v-if="charge.type === 'refund'"> Refund </template>
|
||||
<template v-else-if="charge.type === 'subscription'">
|
||||
<template v-if="charge.status === 'cancelled'"> Subscription </template>
|
||||
<template v-else-if="index === subscription.charges.length - 1">
|
||||
Started subscription
|
||||
</template>
|
||||
<template v-else> Subscription renewal </template>
|
||||
</template>
|
||||
<template v-else-if="charge.type === 'one-time'"> One-time charge </template>
|
||||
<template v-else-if="charge.type === 'proration'"> Proration charge </template>
|
||||
<template v-else> {{ charge.status }} </template>
|
||||
</span>
|
||||
<template v-if="charge.status !== 'cancelled'">
|
||||
⋅
|
||||
{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}
|
||||
</template>
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
||||
<span class="text-secondary">({{ dayjs(charge.due).fromNow() }}) </span>
|
||||
</span>
|
||||
<div
|
||||
v-if="flags.developerMode"
|
||||
class="flex w-full items-center gap-1 text-xs text-secondary"
|
||||
>
|
||||
{{ charge.status }}
|
||||
⋅
|
||||
{{ charge.type }}
|
||||
⋅
|
||||
{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}
|
||||
⋅
|
||||
{{ dayjs(charge.due).format("YYYY-MM-DD h:mma") }}
|
||||
<template v-if="charge.subscription_interval">
|
||||
⋅ {{ charge.subscription_interval }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
charges.some((x) => x.type === 'refund' && x.parent_charge_id === charge.id)
|
||||
"
|
||||
>
|
||||
<div class="button-like disabled"><CheckIcon /> Charge refunded</div>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="charge.status === 'succeeded' && charge.type !== 'refund'"
|
||||
color="red"
|
||||
color-fill="text"
|
||||
>
|
||||
<button @click="showRefundModal(charge)">
|
||||
<CurrencyIcon />
|
||||
Refund options
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="charge.status === 'succeeded' && charge.type !== 'refund'"
|
||||
class="btn"
|
||||
@click="showRefundModal(charge)"
|
||||
>
|
||||
Refund charge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,11 +196,22 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Badge, ButtonStyled, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
|
||||
import { formatPrice } from "@modrinth/utils";
|
||||
import { CheckIcon, XIcon } from "@modrinth/assets";
|
||||
import { Avatar, ButtonStyled, CopyCode, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
|
||||
import { formatCategory, formatPrice } from "@modrinth/utils";
|
||||
import {
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
UserIcon,
|
||||
ModrinthPlusIcon,
|
||||
ServerIcon,
|
||||
ExternalIcon,
|
||||
CurrencyIcon,
|
||||
} from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
import { products } from "~/generated/state.json";
|
||||
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const route = useRoute();
|
||||
const data = useNuxtApp();
|
||||
const vintl = useVIntl();
|
||||
@@ -164,7 +262,10 @@ const subscriptionCharges = computed(() => {
|
||||
return subscriptions.value.map((subscription) => {
|
||||
return {
|
||||
...subscription,
|
||||
charges: charges.value.filter((charge) => charge.subscription_id === subscription.id),
|
||||
charges: charges.value
|
||||
.filter((charge) => charge.subscription_id === subscription.id)
|
||||
.slice()
|
||||
.sort((a, b) => dayjs(b.due).diff(dayjs(a.due))),
|
||||
product: products.find((product) =>
|
||||
product.prices.some((price) => price.id === subscription.price_id),
|
||||
),
|
||||
@@ -212,4 +313,30 @@ async function refundCharge() {
|
||||
}
|
||||
refunding.value = false;
|
||||
}
|
||||
|
||||
const chargeStatuses = {
|
||||
open: {
|
||||
color: "bg-blue",
|
||||
},
|
||||
processing: {
|
||||
color: "bg-orange",
|
||||
},
|
||||
succeeded: {
|
||||
color: "bg-green",
|
||||
},
|
||||
failed: {
|
||||
color: "bg-red",
|
||||
},
|
||||
cancelled: {
|
||||
color: "bg-red",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 1rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 56rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,17 +164,45 @@ const projectTypes = computed(() => {
|
||||
return [...set];
|
||||
});
|
||||
|
||||
function segmentData(data, segmentSize = 900) {
|
||||
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 organizationIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
||||
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
||||
|
||||
const url = `teams?ids=${encodeURIComponent(JSON.stringify(teamIds))}`;
|
||||
const orgUrl = `organizations?ids=${encodeURIComponent(JSON.stringify(organizationIds))}`;
|
||||
const { data: result } = await useAsyncData(url, () => useBaseFetch(url));
|
||||
const { data: orgs } = await useAsyncData(orgUrl, () => useBaseFetch(orgUrl, { apiVersion: 3 }));
|
||||
const [{ data: teams }, { data: orgs }] = await Promise.all([
|
||||
useAsyncData(`teams?ids=${asEncodedJsonArray(teamIds)}`, () =>
|
||||
fetchSegmented(teamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`),
|
||||
),
|
||||
useAsyncData(`organizations?ids=${asEncodedJsonArray(orgIds)}`, () =>
|
||||
fetchSegmented(orgIds, (ids) => `organizations?ids=${asEncodedJsonArray(ids)}`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
if (result.value) {
|
||||
members.value = result.value;
|
||||
if (teams.value) {
|
||||
members.value = teams.value;
|
||||
|
||||
projects.value = projects.value.map((project) => {
|
||||
project.owner = members.value
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { getChangelog } from "@modrinth/utils";
|
||||
import { ChangelogEntry } from "@modrinth/ui";
|
||||
import { ChangelogEntry, Timeline } from "@modrinth/ui";
|
||||
import { ChevronLeftIcon } from "@modrinth/assets";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -39,41 +39,13 @@ if (!changelogEntry.value) {
|
||||
>
|
||||
<ChevronLeftIcon /> View full changelog
|
||||
</nuxt-link>
|
||||
<div class="relative flex flex-col gap-4 pb-6">
|
||||
<div class="absolute flex h-full w-4 justify-center">
|
||||
<div class="timeline-indicator" :class="{ first: isFirst }" />
|
||||
</div>
|
||||
<ChangelogEntry :entry="changelogEntry" :first="isFirst" show-type class="relative z-[1]" />
|
||||
</div>
|
||||
<Timeline fade-out-end :fade-out-start="!isFirst" :class="{ '-mt-8': !isFirst }">
|
||||
<ChangelogEntry
|
||||
:entry="changelogEntry"
|
||||
:first="isFirst"
|
||||
show-type
|
||||
:class="{ 'mt-8': !isFirst }"
|
||||
/>
|
||||
</Timeline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-indicator {
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--color-raised-bg) 66%,
|
||||
rgba(255, 255, 255, 0) 0%
|
||||
);
|
||||
background-size: 100% 30px;
|
||||
background-repeat: repeat-y;
|
||||
|
||||
height: calc(100% + 2rem);
|
||||
width: 4px;
|
||||
margin-top: -2rem;
|
||||
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 8rem,
|
||||
black calc(100% - 8rem),
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
&.first {
|
||||
margin-top: 1rem;
|
||||
|
||||
mask-image: linear-gradient(black calc(100% - 15rem), transparent 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { type Product, getChangelog } from "@modrinth/utils";
|
||||
import { ChangelogEntry } from "@modrinth/ui";
|
||||
import Timeline from "@modrinth/ui/src/components/base/Timeline.vue";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -51,10 +52,7 @@ const changelogEntries = computed(() =>
|
||||
query="filter"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="relative flex flex-col gap-4 pb-6">
|
||||
<div class="absolute flex h-full w-4 justify-center">
|
||||
<div class="timeline-indicator" />
|
||||
</div>
|
||||
<Timeline fade-out-end>
|
||||
<ChangelogEntry
|
||||
v-for="(entry, index) in changelogEntries"
|
||||
:key="entry.date"
|
||||
@@ -62,25 +60,6 @@ const changelogEntries = computed(() =>
|
||||
:first="index === 0"
|
||||
:show-type="filter === undefined"
|
||||
has-link
|
||||
class="relative z-[1]"
|
||||
/>
|
||||
</div>
|
||||
</Timeline>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-indicator {
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--color-raised-bg) 66%,
|
||||
rgba(255, 255, 255, 0) 0%
|
||||
);
|
||||
background-size: 100% 30px;
|
||||
background-repeat: repeat-y;
|
||||
margin-top: 1rem;
|
||||
|
||||
height: calc(100% - 1rem);
|
||||
width: 4px;
|
||||
|
||||
mask-image: linear-gradient(to bottom, black calc(100% - 15rem), transparent 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user