refactor: migrate to common eslint+prettier configs (#4168)

* refactor: migrate to common eslint+prettier configs

* fix: prettier frontend

* feat: config changes

* fix: lint issues

* fix: lint

* fix: type imports

* fix: cyclical import issue

* fix: lockfile

* fix: missing dep

* fix: switch to tabs

* fix: continue switch to tabs

* fix: rustfmt parity

* fix: moderation lint issue

* fix: lint issues

* fix: ui intl

* fix: lint issues

* Revert "fix: rustfmt parity"

This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711.

* feat: revert last rs
This commit is contained in:
Cal H.
2025-08-14 21:48:38 +01:00
committed by GitHub
parent 82697278dc
commit 2aabcf36ee
702 changed files with 101360 additions and 102020 deletions

View File

@@ -1,443 +1,444 @@
<template>
<NewModal ref="refundModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Refund charge</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="visibility" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Refund type
<span class="text-brand-red">*</span>
</span>
<span> The type of refund to issue. </span>
</label>
<DropdownSelect
id="refund-type"
v-model="refundType"
:options="refundTypes"
name="Refund type"
/>
</div>
<div v-if="refundType === 'partial'" class="flex flex-col gap-2">
<label for="amount" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Amount
<span class="text-brand-red">*</span>
</span>
<span>
Enter the amount in cents of USD. For example for $2, enter 200. (net
{{ selectedCharge.net }})
</span>
</label>
<input id="amount" v-model="refundAmount" type="number" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label for="unprovision" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Unprovision
<span class="text-brand-red">*</span>
</span>
<span> Whether or not the subscription should be unprovisioned on refund. </span>
</label>
<Toggle id="unprovision" v-model="unprovision" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="refunding" @click="refundCharge">
<CheckIcon aria-hidden="true" />
Refund charge
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="refundModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal ref="modifyModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Modify charge</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="cancel" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Cancel server
<span class="text-brand-red">*</span>
</span>
<span>
Whether or not the subscription should be cancelled. Submitting this as "true" will
cancel the subscription, while submitting it as "false" will force another charge
attempt to be made.
</span>
</label>
<Toggle id="cancel" v-model="cancel" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="modifying" @click="modifyCharge">
<CheckIcon aria-hidden="true" />
Modify charge
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="modifyModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<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">
<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") }} ({{
formatRelativeTime(subscription.created)
}})
</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">
<span
v-if="charge.status === 'cancelled' && $dayjs(charge.due).isBefore($dayjs())"
class="font-bold"
>
Ended:
</span>
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
<span v-else class="font-bold">Due:</span>
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span>
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
<span v-else class="font-bold">Charged:</span>
{{ dayjs(charge.last_attempt).format("MMMM D, YYYY [at] h:mma") }}
<span class="text-secondary"
>({{ formatRelativeTime(charge.last_attempt) }})
</span>
</span>
<div 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>
<ButtonStyled v-else-if="charge.status === 'failed'" color="red" color-fill="text">
<button @click="showModifyModal(subscription)">
<CurrencyIcon />
Modify charge
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<NewModal ref="refundModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Refund charge</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="visibility" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Refund type
<span class="text-brand-red">*</span>
</span>
<span> The type of refund to issue. </span>
</label>
<DropdownSelect
id="refund-type"
v-model="refundType"
:options="refundTypes"
name="Refund type"
/>
</div>
<div v-if="refundType === 'partial'" class="flex flex-col gap-2">
<label for="amount" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Amount
<span class="text-brand-red">*</span>
</span>
<span>
Enter the amount in cents of USD. For example for $2, enter 200. (net
{{ selectedCharge.net }})
</span>
</label>
<input id="amount" v-model="refundAmount" type="number" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label for="unprovision" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Unprovision
<span class="text-brand-red">*</span>
</span>
<span> Whether or not the subscription should be unprovisioned on refund. </span>
</label>
<Toggle id="unprovision" v-model="unprovision" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="refunding" @click="refundCharge">
<CheckIcon aria-hidden="true" />
Refund charge
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="refundModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal ref="modifyModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Modify charge</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="cancel" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Cancel server
<span class="text-brand-red">*</span>
</span>
<span>
Whether or not the subscription should be cancelled. Submitting this as "true" will
cancel the subscription, while submitting it as "false" will force another charge
attempt to be made.
</span>
</label>
<Toggle id="cancel" v-model="cancel" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="modifying" @click="modifyCharge">
<CheckIcon aria-hidden="true" />
Modify charge
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="modifyModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<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">
<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') }} ({{
formatRelativeTime(subscription.created)
}})
</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">
<span
v-if="charge.status === 'cancelled' && $dayjs(charge.due).isBefore($dayjs())"
class="font-bold"
>
Ended:
</span>
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
<span v-else class="font-bold">Due:</span>
{{ dayjs(charge.due).format('MMMM D, YYYY [at] h:mma') }}
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span>
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
<span v-else class="font-bold">Charged:</span>
{{ dayjs(charge.last_attempt).format('MMMM D, YYYY [at] h:mma') }}
<span class="text-secondary"
>({{ formatRelativeTime(charge.last_attempt) }})
</span>
</span>
<div 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>
<ButtonStyled v-else-if="charge.status === 'failed'" color="red" color-fill="text">
<button @click="showModifyModal(subscription)">
<CurrencyIcon />
Modify charge
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
CheckIcon,
CurrencyIcon,
ExternalIcon,
ModrinthPlusIcon,
ServerIcon,
UserIcon,
XIcon,
} from "@modrinth/assets";
CheckIcon,
CurrencyIcon,
ExternalIcon,
ModrinthPlusIcon,
ServerIcon,
UserIcon,
XIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
CopyCode,
DropdownSelect,
injectNotificationManager,
NewModal,
Toggle,
useRelativeTime,
} from "@modrinth/ui";
import { formatCategory, formatPrice } from "@modrinth/utils";
import dayjs from "dayjs";
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
import { products } from "~/generated/state.json";
Avatar,
ButtonStyled,
CopyCode,
DropdownSelect,
injectNotificationManager,
NewModal,
Toggle,
useRelativeTime,
} from '@modrinth/ui'
import { formatCategory, formatPrice } from '@modrinth/utils'
import dayjs from 'dayjs'
const { addNotification } = injectNotificationManager();
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
import { products } from '~/generated/state.json'
const route = useRoute();
const vintl = useVIntl();
const { addNotification } = injectNotificationManager()
const { formatMessage } = vintl;
const formatRelativeTime = useRelativeTime();
const route = useRoute()
const vintl = useVIntl()
const { formatMessage } = vintl
const formatRelativeTime = useRelativeTime()
const messages = defineMessages({
userNotFoundError: {
id: "admin.billing.error.not-found",
defaultMessage: "User not found",
},
});
userNotFoundError: {
id: 'admin.billing.error.not-found',
defaultMessage: 'User not found',
},
})
const { data: user } = await useAsyncData(`user/${route.params.id}`, () =>
useBaseFetch(`user/${route.params.id}`),
);
useBaseFetch(`user/${route.params.id}`),
)
if (!user.value) {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
});
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
}
let subscriptions, charges, refreshCharges;
let subscriptions, charges, refreshCharges
try {
[{ data: subscriptions }, { data: charges, refresh: refreshCharges }] = await Promise.all([
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
internal: true,
}),
),
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
internal: true,
}),
),
]);
;[{ data: subscriptions }, { data: charges, refresh: refreshCharges }] = await Promise.all([
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
internal: true,
}),
),
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
internal: true,
}),
),
])
} catch {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
});
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
}
const subscriptionCharges = computed(() => {
return subscriptions.value.map((subscription) => {
return {
...subscription,
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),
),
};
});
});
return subscriptions.value.map((subscription) => {
return {
...subscription,
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),
),
}
})
})
const refunding = ref(false);
const refundModal = ref();
const selectedCharge = ref(null);
const refundType = ref("full");
const refundTypes = ref(["full", "partial", "none"]);
const refundAmount = ref(0);
const unprovision = ref(true);
const refunding = ref(false)
const refundModal = ref()
const selectedCharge = ref(null)
const refundType = ref('full')
const refundTypes = ref(['full', 'partial', 'none'])
const refundAmount = ref(0)
const unprovision = ref(true)
const modifying = ref(false);
const modifyModal = ref();
const cancel = ref(false);
const modifying = ref(false)
const modifyModal = ref()
const cancel = ref(false)
function showRefundModal(charge) {
selectedCharge.value = charge;
refundType.value = "full";
refundAmount.value = 0;
unprovision.value = true;
refundModal.value.show();
selectedCharge.value = charge
refundType.value = 'full'
refundAmount.value = 0
unprovision.value = true
refundModal.value.show()
}
function showModifyModal(charge) {
selectedCharge.value = charge;
cancel.value = false;
modifyModal.value.show();
selectedCharge.value = charge
cancel.value = false
modifyModal.value.show()
}
async function refundCharge() {
refunding.value = true;
try {
await useBaseFetch(`billing/charge/${selectedCharge.value.id}/refund`, {
method: "POST",
body: JSON.stringify({
type: refundType.value,
amount: refundAmount.value,
unprovision: unprovision.value,
}),
internal: true,
});
await refreshCharges();
refundModal.value.hide();
} catch (err) {
addNotification({
title: "Error refunding",
text: err.data?.description ?? err,
type: "error",
});
}
refunding.value = false;
refunding.value = true
try {
await useBaseFetch(`billing/charge/${selectedCharge.value.id}/refund`, {
method: 'POST',
body: JSON.stringify({
type: refundType.value,
amount: refundAmount.value,
unprovision: unprovision.value,
}),
internal: true,
})
await refreshCharges()
refundModal.value.hide()
} catch (err) {
addNotification({
title: 'Error refunding',
text: err.data?.description ?? err,
type: 'error',
})
}
refunding.value = false
}
async function modifyCharge() {
modifying.value = true;
try {
await useBaseFetch(`billing/subscription/${selectedCharge.value.id}`, {
method: "PATCH",
body: JSON.stringify({
cancelled: cancel.value,
}),
internal: true,
});
addNotification({
title: "Resubscription request submitted",
text: "If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.",
type: "success",
});
await refreshCharges();
} catch (err) {
addNotification({
title: "Error reattempting charge",
text: err.data?.description ?? err,
type: "error",
});
}
modifying.value = false;
modifying.value = true
try {
await useBaseFetch(`billing/subscription/${selectedCharge.value.id}`, {
method: 'PATCH',
body: JSON.stringify({
cancelled: cancel.value,
}),
internal: true,
})
addNotification({
title: 'Resubscription request submitted',
text: 'If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.',
type: 'success',
})
await refreshCharges()
} catch (err) {
addNotification({
title: 'Error reattempting charge',
text: err.data?.description ?? err,
type: 'error',
})
}
modifying.value = false
}
const chargeStatuses = {
open: {
color: "bg-blue",
},
processing: {
color: "bg-orange",
},
succeeded: {
color: "bg-green",
},
failed: {
color: "bg-red",
},
cancelled: {
color: "bg-red",
},
};
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;
padding: 1rem;
margin-left: auto;
margin-right: auto;
max-width: 56rem;
}
</style>

View File

@@ -1,502 +1,506 @@
<template>
<NewModal ref="createNoticeModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">{{
editingNotice ? `Editing notice #${editingNotice?.id}` : "Creating a notice"
}}</span>
</template>
<div class="flex w-[700px] flex-col gap-3">
<div class="flex items-center justify-between gap-2">
<label for="level-selector" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Level </span>
<span>Determines how the notice should be styled.</span>
</label>
<TeleportDropdownMenu
id="level-selector"
v-model="newNoticeLevel"
class="max-w-[10rem]"
:options="levelOptions"
:display-name="(x) => formatMessage(x.name)"
name="Level"
/>
</div>
<div v-if="!newNoticeSurvey" class="flex flex-col gap-2">
<label for="notice-title" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Title </span>
</label>
<input
id="notice-title"
v-model="newNoticeTitle"
placeholder="E.g. Maintenance"
type="text"
autocomplete="off"
/>
</div>
<div class="flex flex-col gap-2">
<label for="notice-message" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
{{ newNoticeSurvey ? "Survey ID" : "Message" }}
<span class="text-brand-red">*</span>
</span>
</label>
<input
v-if="newNoticeSurvey"
id="notice-message"
v-model="newNoticeMessage"
placeholder="E.g. rXGtq2"
type="text"
autocomplete="off"
/>
<div v-else class="textarea-wrapper h-32">
<textarea id="notice-message" v-model="newNoticeMessage" />
</div>
</div>
<div v-if="!newNoticeSurvey" class="flex items-center justify-between gap-2">
<label for="dismissable-toggle" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Dismissable </span>
<span>Allow users to dismiss the notice from their panel.</span>
</label>
<Toggle id="dismissable-toggle" v-model="newNoticeDismissable" />
</div>
<div class="flex items-center justify-between gap-2">
<label for="scheduled-date" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Announcement date </span>
<span>Leave blank for notice to be available immediately.</span>
</label>
<input
id="scheduled-date"
v-model="newNoticeScheduledDate"
type="datetime-local"
autocomplete="off"
/>
</div>
<div class="flex items-center justify-between gap-2">
<label for="expiration-date" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Expiration date </span>
<span>The notice will automatically be deleted after this date.</span>
</label>
<input
id="expiration-date"
v-model="newNoticeExpiresDate"
type="datetime-local"
autocomplete="off"
/>
</div>
<NewModal ref="createNoticeModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">{{
editingNotice ? `Editing notice #${editingNotice?.id}` : 'Creating a notice'
}}</span>
</template>
<div class="flex w-[700px] flex-col gap-3">
<div class="flex items-center justify-between gap-2">
<label for="level-selector" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Level </span>
<span>Determines how the notice should be styled.</span>
</label>
<TeleportDropdownMenu
id="level-selector"
v-model="newNoticeLevel"
class="max-w-[10rem]"
:options="levelOptions"
:display-name="(x) => formatMessage(x.name)"
name="Level"
/>
</div>
<div v-if="!newNoticeSurvey" class="flex flex-col gap-2">
<label for="notice-title" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Title </span>
</label>
<input
id="notice-title"
v-model="newNoticeTitle"
placeholder="E.g. Maintenance"
type="text"
autocomplete="off"
/>
</div>
<div class="flex flex-col gap-2">
<label for="notice-message" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
{{ newNoticeSurvey ? 'Survey ID' : 'Message' }}
<span class="text-brand-red">*</span>
</span>
</label>
<input
v-if="newNoticeSurvey"
id="notice-message"
v-model="newNoticeMessage"
placeholder="E.g. rXGtq2"
type="text"
autocomplete="off"
/>
<div v-else class="textarea-wrapper h-32">
<textarea id="notice-message" v-model="newNoticeMessage" />
</div>
</div>
<div v-if="!newNoticeSurvey" class="flex items-center justify-between gap-2">
<label for="dismissable-toggle" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Dismissable </span>
<span>Allow users to dismiss the notice from their panel.</span>
</label>
<Toggle id="dismissable-toggle" v-model="newNoticeDismissable" />
</div>
<div class="flex items-center justify-between gap-2">
<label for="scheduled-date" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Announcement date </span>
<span>Leave blank for notice to be available immediately.</span>
</label>
<input
id="scheduled-date"
v-model="newNoticeScheduledDate"
type="datetime-local"
autocomplete="off"
/>
</div>
<div class="flex items-center justify-between gap-2">
<label for="expiration-date" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Expiration date </span>
<span>The notice will automatically be deleted after this date.</span>
</label>
<input
id="expiration-date"
v-model="newNoticeExpiresDate"
type="datetime-local"
autocomplete="off"
/>
</div>
<div v-if="!newNoticeSurvey" class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast"> Preview </span>
<ServerNotice
:level="newNoticeLevel.id"
:message="
!trimmedMessage || trimmedMessage.length < 1
? 'Type a message to begin previewing it.'
: trimmedMessage
"
:dismissable="newNoticeDismissable"
:title="trimmedTitle"
preview
/>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button v-if="editingNotice" :disabled="!!noticeSubmitError" @click="() => saveChanges()">
<SaveIcon aria-hidden="true" />
{{ formatMessage(commonMessages.saveChangesButton) }}
</button>
<button v-else :disabled="!!noticeSubmitError" @click="() => createNotice()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.createNotice) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="createNoticeModal?.hide">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<AssignNoticeModal ref="assignNoticeModal" @close="refreshNotices" />
<div class="page experimental-styles-within">
<div
class="mb-6 flex items-end justify-between border-0 border-b border-solid border-divider pb-4"
>
<h1 class="m-0 text-2xl">Servers notices</h1>
<ButtonStyled color="brand">
<button @click="openNewNoticeModal">
<PlusIcon />
{{ formatMessage(messages.createNotice) }}
</button>
</ButtonStyled>
</div>
<div>
<div v-if="!notices || notices.length === 0">{{ formatMessage(messages.noNotices) }}</div>
<div
v-else
class="grid grid-cols-[auto_auto_auto] gap-4 md:grid-cols-[min-content_auto_auto_auto_auto_min-content]"
>
<div class="col-span-full grid grid-cols-subgrid gap-4 px-4 font-bold text-contrast">
<div>{{ formatMessage(messages.id) }}</div>
<div>{{ formatMessage(messages.begins) }}</div>
<div>{{ formatMessage(messages.expires) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.level) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.dismissable) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.actions) }}</div>
</div>
<div
v-for="notice in notices"
:key="`notice-${notice.id}`"
class="col-span-full grid grid-cols-subgrid gap-4 rounded-2xl bg-bg-raised p-4"
>
<div class="col-span-full grid grid-cols-subgrid items-center gap-4">
<div>
<CopyCode :text="`${notice.id}`" />
</div>
<div class="text-sm">
<span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{
formatRelativeTime(notice.announce_at)
}})
</span>
<template v-else> Never begins </template>
</div>
<div class="text-sm">
<span
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ formatRelativeTime(notice.expires) }}
</span>
<template v-else> Never expires </template>
</div>
<div
:style="
NOTICE_LEVELS[notice.level]
? {
'--_color': NOTICE_LEVELS[notice.level].colors.text,
'--_bg-color': NOTICE_LEVELS[notice.level].colors.bg,
}
: undefined
"
>
<TagItem>
{{
NOTICE_LEVELS[notice.level]
? formatMessage(NOTICE_LEVELS[notice.level].name)
: notice.level
}}
</TagItem>
</div>
<div
:style="{
'--_color': notice.dismissable ? 'var(--color-green)' : 'var(--color-red)',
'--_bg-color': notice.dismissable ? 'var(--color-green-bg)' : 'var(--color-red-bg)',
}"
>
<TagItem>
{{
formatMessage(notice.dismissable ? messages.dismissable : messages.undismissable)
}}
</TagItem>
</div>
<div class="col-span-2 flex gap-2 md:col-span-1">
<ButtonStyled>
<button @click="() => startEditing(notice)">
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="() => deleteNotice(notice)">
<TrashIcon /> {{ formatMessage(commonMessages.deleteLabel) }}
</button>
</ButtonStyled>
</div>
</div>
<div class="col-span-full grid">
<ServerNotice
:level="notice.level"
:message="notice.message"
:dismissable="notice.dismissable"
:title="notice.title"
preview
/>
<div class="mt-4 flex items-center gap-2">
<span v-if="!notice.assigned || notice.assigned.length === 0"
>Not assigned to any servers</span
>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'server')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes
</span>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'node')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers
</span>
<span v-else>
Assigned to
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers and
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes
</span>
<button
class="m-0 flex items-center gap-1 border-none bg-transparent p-0 text-blue hover:underline hover:brightness-125 active:scale-95 active:brightness-150"
@click="() => startEditing(notice, true)"
>
<SettingsIcon />
Edit assignments
</button>
<template v-if="notice.dismissed_by.length > 0">
<span> Dismissed by {{ notice.dismissed_by.length }} servers </span>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="!newNoticeSurvey" class="flex flex-col gap-2">
<span class="text-lg font-semibold text-contrast"> Preview </span>
<ServerNotice
:level="newNoticeLevel.id"
:message="
!trimmedMessage || trimmedMessage.length < 1
? 'Type a message to begin previewing it.'
: trimmedMessage
"
:dismissable="newNoticeDismissable"
:title="trimmedTitle"
preview
/>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button v-if="editingNotice" :disabled="!!noticeSubmitError" @click="() => saveChanges()">
<SaveIcon aria-hidden="true" />
{{ formatMessage(commonMessages.saveChangesButton) }}
</button>
<button v-else :disabled="!!noticeSubmitError" @click="() => createNotice()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.createNotice) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="createNoticeModal?.hide">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<AssignNoticeModal ref="assignNoticeModal" @close="refreshNotices" />
<div class="page experimental-styles-within">
<div
class="mb-6 flex items-end justify-between border-0 border-b border-solid border-divider pb-4"
>
<h1 class="m-0 text-2xl">Servers notices</h1>
<ButtonStyled color="brand">
<button @click="openNewNoticeModal">
<PlusIcon />
{{ formatMessage(messages.createNotice) }}
</button>
</ButtonStyled>
</div>
<div>
<div v-if="!notices || notices.length === 0">
{{ formatMessage(messages.noNotices) }}
</div>
<div
v-else
class="grid grid-cols-[auto_auto_auto] gap-4 md:grid-cols-[min-content_auto_auto_auto_auto_min-content]"
>
<div class="col-span-full grid grid-cols-subgrid gap-4 px-4 font-bold text-contrast">
<div>{{ formatMessage(messages.id) }}</div>
<div>{{ formatMessage(messages.begins) }}</div>
<div>{{ formatMessage(messages.expires) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.level) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.dismissable) }}</div>
<div class="hidden md:block">{{ formatMessage(messages.actions) }}</div>
</div>
<div
v-for="notice in notices"
:key="`notice-${notice.id}`"
class="col-span-full grid grid-cols-subgrid gap-4 rounded-2xl bg-bg-raised p-4"
>
<div class="col-span-full grid grid-cols-subgrid items-center gap-4">
<div>
<CopyCode :text="`${notice.id}`" />
</div>
<div class="text-sm">
<span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format('MMM D, YYYY [at] h:mm A') }}
({{ formatRelativeTime(notice.announce_at) }})
</span>
<template v-else> Never begins </template>
</div>
<div class="text-sm">
<span
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ formatRelativeTime(notice.expires) }}
</span>
<template v-else> Never expires </template>
</div>
<div
:style="
NOTICE_LEVELS[notice.level]
? {
'--_color': NOTICE_LEVELS[notice.level].colors.text,
'--_bg-color': NOTICE_LEVELS[notice.level].colors.bg,
}
: undefined
"
>
<TagItem>
{{
NOTICE_LEVELS[notice.level]
? formatMessage(NOTICE_LEVELS[notice.level].name)
: notice.level
}}
</TagItem>
</div>
<div
:style="{
'--_color': notice.dismissable ? 'var(--color-green)' : 'var(--color-red)',
'--_bg-color': notice.dismissable ? 'var(--color-green-bg)' : 'var(--color-red-bg)',
}"
>
<TagItem>
{{
formatMessage(notice.dismissable ? messages.dismissable : messages.undismissable)
}}
</TagItem>
</div>
<div class="col-span-2 flex gap-2 md:col-span-1">
<ButtonStyled>
<button @click="() => startEditing(notice)">
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="() => deleteNotice(notice)">
<TrashIcon /> {{ formatMessage(commonMessages.deleteLabel) }}
</button>
</ButtonStyled>
</div>
</div>
<div class="col-span-full grid">
<ServerNotice
:level="notice.level"
:message="notice.message"
:dismissable="notice.dismissable"
:title="notice.title"
preview
/>
<div class="mt-4 flex items-center gap-2">
<span v-if="!notice.assigned || notice.assigned.length === 0"
>Not assigned to any servers</span
>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'server')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'node')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === 'server').length }}
servers
</span>
<span v-else>
Assigned to
{{ notice.assigned.filter((n) => n.kind === 'server').length }}
servers and
{{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span>
<button
class="m-0 flex items-center gap-1 border-none bg-transparent p-0 text-blue hover:underline hover:brightness-125 active:scale-95 active:brightness-150"
@click="() => startEditing(notice, true)"
>
<SettingsIcon />
Edit assignments
</button>
<template v-if="notice.dismissed_by.length > 0">
<span> Dismissed by {{ notice.dismissed_by.length }} servers </span>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { EditIcon, PlusIcon, SaveIcon, SettingsIcon, TrashIcon, XIcon } from "@modrinth/assets";
import { EditIcon, PlusIcon, SaveIcon, SettingsIcon, TrashIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
CopyCode,
injectNotificationManager,
NewModal,
ServerNotice,
TagItem,
TeleportDropdownMenu,
Toggle,
useRelativeTime,
} from "@modrinth/ui";
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useVIntl } from "@vintl/vintl";
import dayjs from "dayjs";
import { computed } from "vue";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
ButtonStyled,
commonMessages,
CopyCode,
injectNotificationManager,
NewModal,
ServerNotice,
TagItem,
TeleportDropdownMenu,
Toggle,
useRelativeTime,
} from '@modrinth/ui'
import { NOTICE_LEVELS } from '@modrinth/ui/src/utils/notices.ts'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed } from 'vue'
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime();
import AssignNoticeModal from '~/components/ui/servers/notice/AssignNoticeModal.vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const notices = ref<ServerNoticeType[]>([]);
const createNoticeModal = ref<InstanceType<typeof NewModal>>();
const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
await refreshNotices();
const notices = ref<ServerNoticeType[]>([])
const createNoticeModal = ref<InstanceType<typeof NewModal>>()
const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>()
await refreshNotices()
async function refreshNotices() {
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));
if (dateDiff === 0) {
return b.id - a.id;
}
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))
if (dateDiff === 0) {
return b.id - a.id
}
return dateDiff;
});
});
return dateDiff
})
})
}
const levelOptions = Object.keys(NOTICE_LEVELS).map((x) => ({
id: x,
...NOTICE_LEVELS[x],
}));
id: x,
...NOTICE_LEVELS[x],
}))
const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm";
const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm'
const newNoticeLevel = ref(levelOptions[0]);
const newNoticeDismissable = ref(false);
const newNoticeMessage = ref("");
const newNoticeScheduledDate = ref<string>();
const newNoticeTitle = ref<string>();
const newNoticeExpiresDate = ref<string>();
const newNoticeLevel = ref(levelOptions[0])
const newNoticeDismissable = ref(false)
const newNoticeMessage = ref('')
const newNoticeScheduledDate = ref<string>()
const newNoticeTitle = ref<string>()
const newNoticeExpiresDate = ref<string>()
function openNewNoticeModal() {
newNoticeLevel.value = levelOptions[0];
newNoticeDismissable.value = false;
newNoticeMessage.value = "";
newNoticeScheduledDate.value = undefined;
newNoticeExpiresDate.value = undefined;
editingNotice.value = undefined;
createNoticeModal.value?.show();
newNoticeLevel.value = levelOptions[0]
newNoticeDismissable.value = false
newNoticeMessage.value = ''
newNoticeScheduledDate.value = undefined
newNoticeExpiresDate.value = undefined
editingNotice.value = undefined
createNoticeModal.value?.show()
}
const editingNotice = ref<undefined | ServerNoticeType>();
const editingNotice = ref<undefined | ServerNoticeType>()
function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
newNoticeLevel.value = levelOptions.find((x) => x.id === notice.level) ?? levelOptions[0];
newNoticeDismissable.value = notice.dismissable;
newNoticeMessage.value = notice.message;
newNoticeTitle.value = notice.title;
newNoticeScheduledDate.value = dayjs(notice.announce_at).format(DATE_TIME_FORMAT);
newNoticeExpiresDate.value = notice.expires
? dayjs(notice.expires).format(DATE_TIME_FORMAT)
: undefined;
editingNotice.value = notice;
if (assignments) {
assignNoticeModal.value?.show?.(notice);
} else {
createNoticeModal.value?.show();
}
newNoticeLevel.value = levelOptions.find((x) => x.id === notice.level) ?? levelOptions[0]
newNoticeDismissable.value = notice.dismissable
newNoticeMessage.value = notice.message
newNoticeTitle.value = notice.title
newNoticeScheduledDate.value = dayjs(notice.announce_at).format(DATE_TIME_FORMAT)
newNoticeExpiresDate.value = notice.expires
? dayjs(notice.expires).format(DATE_TIME_FORMAT)
: undefined
editingNotice.value = notice
if (assignments) {
assignNoticeModal.value?.show?.(notice)
} else {
createNoticeModal.value?.show()
}
}
async function deleteNotice(notice: ServerNoticeType) {
await useServersFetch(`notices/${notice.id}`, {
method: "DELETE",
})
.then(() => {
addNotification({
title: `Successfully deleted notice #${notice.id}`,
type: "success",
});
})
.catch((err) => {
addNotification({
title: "Error deleting notice",
text: err,
type: "error",
});
});
await refreshNotices();
await useServersFetch(`notices/${notice.id}`, {
method: 'DELETE',
})
.then(() => {
addNotification({
title: `Successfully deleted notice #${notice.id}`,
type: 'success',
})
})
.catch((err) => {
addNotification({
title: 'Error deleting notice',
text: err,
type: 'error',
})
})
await refreshNotices()
}
const trimmedMessage = computed(() => newNoticeMessage.value?.trim());
const trimmedTitle = computed(() => newNoticeTitle.value?.trim());
const newNoticeSurvey = computed(() => newNoticeLevel.value.id === "survey");
const trimmedMessage = computed(() => newNoticeMessage.value?.trim())
const trimmedTitle = computed(() => newNoticeTitle.value?.trim())
const newNoticeSurvey = computed(() => newNoticeLevel.value.id === 'survey')
const noticeSubmitError = computed(() => {
let error: undefined | string;
if (!trimmedMessage.value || trimmedMessage.value.length === 0) {
error = "Notice message is required";
}
if (!newNoticeLevel.value) {
error = "Notice level is required";
}
return error;
});
let error: undefined | string
if (!trimmedMessage.value || trimmedMessage.value.length === 0) {
error = 'Notice message is required'
}
if (!newNoticeLevel.value) {
error = 'Notice level is required'
}
return error
})
function validateSubmission(message: string) {
if (noticeSubmitError.value) {
addNotification({
title: message,
text: noticeSubmitError.value,
type: "error",
});
return false;
}
return true;
if (noticeSubmitError.value) {
addNotification({
title: message,
text: noticeSubmitError.value,
type: 'error',
})
return false
}
return true
}
async function saveChanges() {
if (!validateSubmission("Error saving notice")) {
return;
}
if (!validateSubmission('Error saving notice')) {
return
}
await useServersFetch(`notices/${editingNotice.value?.id}`, {
method: "PATCH",
body: {
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
dismissable: newNoticeSurvey.value ? true : newNoticeDismissable.value,
announce_at: newNoticeScheduledDate.value
? dayjs(newNoticeScheduledDate.value).toISOString()
: dayjs().toISOString(),
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: "Error saving changes to notice",
text: err,
type: "error",
});
});
await refreshNotices();
createNoticeModal.value?.hide();
await useServersFetch(`notices/${editingNotice.value?.id}`, {
method: 'PATCH',
body: {
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
dismissable: newNoticeSurvey.value ? true : newNoticeDismissable.value,
announce_at: newNoticeScheduledDate.value
? dayjs(newNoticeScheduledDate.value).toISOString()
: dayjs().toISOString(),
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: 'Error saving changes to notice',
text: err,
type: 'error',
})
})
await refreshNotices()
createNoticeModal.value?.hide()
}
async function createNotice() {
if (!validateSubmission("Error creating notice")) {
return;
}
if (!validateSubmission('Error creating notice')) {
return
}
await useServersFetch("notices", {
method: "POST",
body: {
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
dismissable: newNoticeSurvey.value ? true : newNoticeDismissable.value,
announce_at: newNoticeScheduledDate.value
? dayjs(newNoticeScheduledDate.value).toISOString()
: dayjs().toISOString(),
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: "Error creating notice",
text: err,
type: "error",
});
});
await refreshNotices();
createNoticeModal.value?.hide();
await useServersFetch('notices', {
method: 'POST',
body: {
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
dismissable: newNoticeSurvey.value ? true : newNoticeDismissable.value,
announce_at: newNoticeScheduledDate.value
? dayjs(newNoticeScheduledDate.value).toISOString()
: dayjs().toISOString(),
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: 'Error creating notice',
text: err,
type: 'error',
})
})
await refreshNotices()
createNoticeModal.value?.hide()
}
const messages = defineMessages({
createNotice: {
id: "servers.notices.create-notice",
defaultMessage: "Create notice",
},
noNotices: {
id: "servers.notices.no-notices",
defaultMessage: "No notices",
},
dismissable: {
id: "servers.notice.dismissable",
defaultMessage: "Dismissable",
},
undismissable: {
id: "servers.notice.undismissable",
defaultMessage: "Undismissable",
},
id: {
id: "servers.notice.id",
defaultMessage: "ID",
},
begins: {
id: "servers.notice.begins",
defaultMessage: "Begins",
},
expires: {
id: "servers.notice.expires",
defaultMessage: "Expires",
},
actions: {
id: "servers.notice.actions",
defaultMessage: "Actions",
},
level: {
id: "servers.notice.level",
defaultMessage: "Level",
},
});
createNotice: {
id: 'servers.notices.create-notice',
defaultMessage: 'Create notice',
},
noNotices: {
id: 'servers.notices.no-notices',
defaultMessage: 'No notices',
},
dismissable: {
id: 'servers.notice.dismissable',
defaultMessage: 'Dismissable',
},
undismissable: {
id: 'servers.notice.undismissable',
defaultMessage: 'Undismissable',
},
id: {
id: 'servers.notice.id',
defaultMessage: 'ID',
},
begins: {
id: 'servers.notice.begins',
defaultMessage: 'Begins',
},
expires: {
id: 'servers.notice.expires',
defaultMessage: 'Expires',
},
actions: {
id: 'servers.notice.actions',
defaultMessage: 'Actions',
},
level: {
id: 'servers.notice.level',
defaultMessage: 'Level',
},
})
</script>
<style lang="scss" scoped>
.page {
padding: 1rem;
margin-left: auto;
margin-right: auto;
max-width: 78.5rem;
padding: 1rem;
margin-left: auto;
margin-right: auto;
max-width: 78.5rem;
}
</style>

View File

@@ -1,62 +1,62 @@
<template>
<div class="normal-page no-sidebar">
<h1>User account request</h1>
<div class="normal-page__content">
<div class="card flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="name">
<span class="text-lg font-semibold text-contrast">
User email
<span class="text-brand-red">*</span>
</span>
</label>
<input
id="name"
v-model="userEmail"
type="email"
maxlength="64"
:placeholder="`Enter user email...`"
autocomplete="off"
/>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button @click="getUserFromEmail">
<MailIcon aria-hidden="true" />
Get user account
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
<div class="normal-page no-sidebar">
<h1>User account request</h1>
<div class="normal-page__content">
<div class="card flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="name">
<span class="text-lg font-semibold text-contrast">
User email
<span class="text-brand-red">*</span>
</span>
</label>
<input
id="name"
v-model="userEmail"
type="email"
maxlength="64"
:placeholder="`Enter user email...`"
autocomplete="off"
/>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button @click="getUserFromEmail">
<MailIcon aria-hidden="true" />
Get user account
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MailIcon } from "@modrinth/assets";
import { ButtonStyled, injectNotificationManager } from "@modrinth/ui";
import { MailIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
const { addNotification } = injectNotificationManager();
const { addNotification } = injectNotificationManager()
const userEmail = ref("");
const userEmail = ref('')
async function getUserFromEmail() {
startLoading();
startLoading()
try {
const result = await useBaseFetch(`user_email?email=${encodeURIComponent(userEmail.value)}`, {
method: "GET",
apiVersion: 3,
});
try {
const result = await useBaseFetch(`user_email?email=${encodeURIComponent(userEmail.value)}`, {
method: 'GET',
apiVersion: 3,
})
await navigateTo(`/user/${result.username}`);
} catch (err) {
console.error(err);
addNotification({
title: "An error occurred",
text: err.data.description,
type: "error",
});
}
stopLoading();
await navigateTo(`/user/${result.username}`)
} catch (err) {
console.error(err)
addNotification({
title: 'An error occurred',
text: err.data.description,
type: 'error',
})
}
stopLoading()
}
</script>