forked from didirus/AstralRinth
Admin & staff page enhancements (#3333)
This commit is contained in:
@@ -227,7 +227,6 @@
|
|||||||
<template #modpacks> <PackageOpenIcon aria-hidden="true" /> Modpacks </template>
|
<template #modpacks> <PackageOpenIcon aria-hidden="true" /> Modpacks </template>
|
||||||
</TeleportOverflowMenu>
|
</TeleportOverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
type="transparent"
|
type="transparent"
|
||||||
:highlighted="
|
:highlighted="
|
||||||
@@ -252,14 +251,52 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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">
|
<ButtonStyled type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
v-if="auth.user"
|
v-if="auth.user"
|
||||||
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
direction="left"
|
direction="left"
|
||||||
:dropdown-id="createPopoutId"
|
:dropdown-id="`${basePopoutId}-create`"
|
||||||
aria-label="Create new..."
|
aria-label="Create new..."
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
@@ -291,7 +328,7 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
v-if="auth.user"
|
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"
|
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
|
||||||
:options="userMenuOptions"
|
:options="userMenuOptions"
|
||||||
>
|
>
|
||||||
@@ -312,7 +349,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template>
|
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template>
|
||||||
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </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>
|
<template #sign-out> <LogOutIcon aria-hidden="true" /> Sign out </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -399,7 +436,7 @@
|
|||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
to="/moderation"
|
to="/moderation"
|
||||||
>
|
>
|
||||||
<ModerationIcon aria-hidden="true" />
|
<ScaleIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.moderationLabel) }}
|
{{ formatMessage(commonMessages.moderationLabel) }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
|
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
|
||||||
@@ -460,7 +497,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<NotificationIcon aria-hidden="true" />
|
<BellIcon aria-hidden="true" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
@@ -479,7 +516,7 @@
|
|||||||
>
|
>
|
||||||
<template v-if="!auth.user">
|
<template v-if="!auth.user">
|
||||||
<HamburgerIcon v-if="!isMobileMenuOpen" aria-hidden="true" />
|
<HamburgerIcon v-if="!isMobileMenuOpen" aria-hidden="true" />
|
||||||
<CrossIcon v-else aria-hidden="true" />
|
<XIcon v-else aria-hidden="true" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -589,6 +626,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
|
ModrinthIcon,
|
||||||
ArrowBigUpDashIcon,
|
ArrowBigUpDashIcon,
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -626,17 +664,17 @@ import {
|
|||||||
TwitterIcon,
|
TwitterIcon,
|
||||||
MastodonIcon,
|
MastodonIcon,
|
||||||
GitHubIcon,
|
GitHubIcon,
|
||||||
XIcon as CrossIcon,
|
ScaleIcon,
|
||||||
ScaleIcon as ModerationIcon,
|
|
||||||
BellIcon as NotificationIcon,
|
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { Button, ButtonStyled, OverflowMenu, Avatar, commonMessages } from "@modrinth/ui";
|
import { Button, ButtonStyled, OverflowMenu, Avatar, commonMessages } from "@modrinth/ui";
|
||||||
|
|
||||||
|
import { isAdmin, isStaff } from "@modrinth/utils";
|
||||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||||
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
|
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
|
||||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||||
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
|
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
|
||||||
import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue";
|
import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue";
|
||||||
|
import Report from "~/pages/report.vue";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
@@ -652,8 +690,7 @@ const route = useNativeRoute();
|
|||||||
const router = useNativeRouter();
|
const router = useNativeRouter();
|
||||||
const link = config.public.siteUrl + route.path.replace(/\/+$/, "");
|
const link = config.public.siteUrl + route.path.replace(/\/+$/, "");
|
||||||
|
|
||||||
const createPopoutId = useId();
|
const basePopoutId = useId();
|
||||||
const userPopoutId = useId();
|
|
||||||
|
|
||||||
const verifyEmailBannerMessages = defineMessages({
|
const verifyEmailBannerMessages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
|
|||||||
@@ -58,50 +58,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
<div class="normal-page no-sidebar">
|
<div class="page experimental-styles-within">
|
||||||
<h1>{{ user.username }}'s subscriptions</h1>
|
<div
|
||||||
<div class="normal-page__content">
|
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 v-for="subscription in subscriptionCharges" :key="subscription.id" class="card">
|
||||||
<span class="font-extrabold text-contrast">
|
<div class="mb-4 grid grid-cols-[1fr_auto]">
|
||||||
<template v-if="subscription.product.metadata.type === 'midas'"> Modrinth Plus </template>
|
<div>
|
||||||
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
<span class="flex items-center gap-2 font-semibold text-contrast">
|
||||||
Modrinth Servers
|
<template v-if="subscription.product.metadata.type === 'midas'">
|
||||||
</template>
|
<ModrinthPlusIcon class="h-7 w-min" />
|
||||||
<template v-else> Unknown product </template>
|
</template>
|
||||||
<template v-if="subscription.interval">
|
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
||||||
{{ subscription.interval }}
|
<ModrinthServersIcon class="h-7 w-min" />
|
||||||
</template>
|
</template>
|
||||||
</span>
|
<template v-else> Unknown product </template>
|
||||||
<div class="mb-4 mt-2 flex items-center gap-1">
|
</span>
|
||||||
{{ subscription.status }} ⋅ {{ $dayjs(subscription.created).format("YYYY-MM-DD") }}
|
<div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary">
|
||||||
<template v-if="subscription.metadata?.id"> ⋅ {{ subscription.metadata.id }}</template>
|
{{ formatCategory(subscription.interval) }} ⋅ {{ subscription.status }} ⋅
|
||||||
</div>
|
{{ dayjs(subscription.created).format("MMMM D, YYYY [at] h:mma") }} ({{
|
||||||
<div
|
dayjs(subscription.created).fromNow()
|
||||||
v-for="charge in subscription.charges"
|
}})
|
||||||
:key="charge.id"
|
</div>
|
||||||
class="universal-card recessed flex items-center justify-between gap-4"
|
</div>
|
||||||
>
|
<div v-if="subscription.metadata?.id" class="flex flex-col items-end gap-2">
|
||||||
<div class="flex w-full items-center justify-between gap-4">
|
<ButtonStyled v-if="subscription.product.metadata.type === 'pyro'">
|
||||||
<div class="flex items-center gap-1">
|
<nuxt-link
|
||||||
<Badge
|
:to="`/servers/manage/${subscription.metadata.id}`"
|
||||||
:color="charge.status === 'succeeded' ? 'green' : 'red'"
|
target="_blank"
|
||||||
:type="charge.status"
|
class="w-fit"
|
||||||
/>
|
>
|
||||||
⋅
|
<ServerIcon /> Server panel <ExternalIcon class="h-4 w-4" />
|
||||||
{{ charge.type }}
|
</nuxt-link>
|
||||||
⋅
|
</ButtonStyled>
|
||||||
{{ $dayjs(charge.due).format("YYYY-MM-DD") }}
|
<CopyCode :text="subscription.metadata.id" />
|
||||||
⋅
|
</div>
|
||||||
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
</div>
|
||||||
<template v-if="subscription.interval"> ⋅ {{ subscription.interval }} </template>
|
<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>
|
</div>
|
||||||
<button
|
|
||||||
v-if="charge.status === 'succeeded' && charge.type !== 'refund'"
|
|
||||||
class="btn"
|
|
||||||
@click="showRefundModal(charge)"
|
|
||||||
>
|
|
||||||
Refund charge
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,11 +196,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Badge, ButtonStyled, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
|
import { Avatar, ButtonStyled, CopyCode, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
|
||||||
import { formatPrice } from "@modrinth/utils";
|
import { formatCategory, formatPrice } from "@modrinth/utils";
|
||||||
import { CheckIcon, XIcon } from "@modrinth/assets";
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
XIcon,
|
||||||
|
UserIcon,
|
||||||
|
ModrinthPlusIcon,
|
||||||
|
ServerIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
CurrencyIcon,
|
||||||
|
} from "@modrinth/assets";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
|
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
|
||||||
|
|
||||||
|
const flags = useFeatureFlags();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const vintl = useVIntl();
|
const vintl = useVIntl();
|
||||||
@@ -164,7 +262,10 @@ const subscriptionCharges = computed(() => {
|
|||||||
return subscriptions.value.map((subscription) => {
|
return subscriptions.value.map((subscription) => {
|
||||||
return {
|
return {
|
||||||
...subscription,
|
...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: products.find((product) =>
|
||||||
product.prices.some((price) => price.id === subscription.price_id),
|
product.prices.some((price) => price.id === subscription.price_id),
|
||||||
),
|
),
|
||||||
@@ -212,4 +313,30 @@ async function refundCharge() {
|
|||||||
}
|
}
|
||||||
refunding.value = false;
|
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>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 56rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getChangelog } from "@modrinth/utils";
|
import { getChangelog } from "@modrinth/utils";
|
||||||
import { ChangelogEntry } from "@modrinth/ui";
|
import { ChangelogEntry, Timeline } from "@modrinth/ui";
|
||||||
import { ChevronLeftIcon } from "@modrinth/assets";
|
import { ChevronLeftIcon } from "@modrinth/assets";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -39,41 +39,13 @@ if (!changelogEntry.value) {
|
|||||||
>
|
>
|
||||||
<ChevronLeftIcon /> View full changelog
|
<ChevronLeftIcon /> View full changelog
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="relative flex flex-col gap-4 pb-6">
|
<Timeline fade-out-end :fade-out-start="!isFirst" :class="{ '-mt-8': !isFirst }">
|
||||||
<div class="absolute flex h-full w-4 justify-center">
|
<ChangelogEntry
|
||||||
<div class="timeline-indicator" :class="{ first: isFirst }" />
|
:entry="changelogEntry"
|
||||||
</div>
|
:first="isFirst"
|
||||||
<ChangelogEntry :entry="changelogEntry" :first="isFirst" show-type class="relative z-[1]" />
|
show-type
|
||||||
</div>
|
:class="{ 'mt-8': !isFirst }"
|
||||||
|
/>
|
||||||
|
</Timeline>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import { type Product, getChangelog } from "@modrinth/utils";
|
import { type Product, getChangelog } from "@modrinth/utils";
|
||||||
import { ChangelogEntry } from "@modrinth/ui";
|
import { ChangelogEntry } from "@modrinth/ui";
|
||||||
|
import Timeline from "@modrinth/ui/src/components/base/Timeline.vue";
|
||||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -51,10 +52,7 @@ const changelogEntries = computed(() =>
|
|||||||
query="filter"
|
query="filter"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<div class="relative flex flex-col gap-4 pb-6">
|
<Timeline fade-out-end>
|
||||||
<div class="absolute flex h-full w-4 justify-center">
|
|
||||||
<div class="timeline-indicator" />
|
|
||||||
</div>
|
|
||||||
<ChangelogEntry
|
<ChangelogEntry
|
||||||
v-for="(entry, index) in changelogEntries"
|
v-for="(entry, index) in changelogEntries"
|
||||||
:key="entry.date"
|
:key="entry.date"
|
||||||
@@ -62,25 +60,6 @@ const changelogEntries = computed(() =>
|
|||||||
:first="index === 0"
|
:first="index === 0"
|
||||||
:show-type="filter === undefined"
|
:show-type="filter === undefined"
|
||||||
has-link
|
has-link
|
||||||
class="relative z-[1]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</Timeline>
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function copyText() {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
width: min-content;
|
width: fit-content;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
transition:
|
transition:
|
||||||
@@ -50,12 +50,6 @@ async function copyText() {
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
|
||||||
max-width: 10rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
|
|||||||
57
packages/ui/src/components/base/Timeline.vue
Normal file
57
packages/ui/src/components/base/Timeline.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
fadeOutStart?: boolean
|
||||||
|
fadeOutEnd?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
fadeOutStart: false,
|
||||||
|
fadeOutEnd: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="relative flex flex-col gap-4 pb-6 isolate">
|
||||||
|
<div class="absolute flex h-full w-4 justify-center">
|
||||||
|
<div
|
||||||
|
class="timeline-indicator"
|
||||||
|
:class="{ 'fade-out-start': fadeOutStart, 'fade-out-end': fadeOutEnd }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.timeline-indicator {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--timeline-line-color, 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;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
&.fade-out-start {
|
||||||
|
mask-image: linear-gradient(to top, black calc(100% - 15rem), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fade-out-end {
|
||||||
|
mask-image: linear-gradient(to bottom, black calc(100% - 15rem), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fade-out-start.fade-out-end {
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0%,
|
||||||
|
black 8rem,
|
||||||
|
black calc(100% - 8rem),
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,6 +35,7 @@ export { default as Slider } from './base/Slider.vue'
|
|||||||
export { default as StatItem } from './base/StatItem.vue'
|
export { default as StatItem } from './base/StatItem.vue'
|
||||||
export { default as TagItem } from './base/TagItem.vue'
|
export { default as TagItem } from './base/TagItem.vue'
|
||||||
export { default as TeleportDropdownMenu } from './base/TeleportDropdownMenu.vue'
|
export { default as TeleportDropdownMenu } from './base/TeleportDropdownMenu.vue'
|
||||||
|
export { default as Timeline } from './base/Timeline.vue'
|
||||||
export { default as Toggle } from './base/Toggle.vue'
|
export { default as Toggle } from './base/Toggle.vue'
|
||||||
|
|
||||||
// Branding
|
// Branding
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function addBodyPadding() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show(event?: MouseEvent) {
|
function show(event?: MouseEvent) {
|
||||||
props.onShow()
|
props.onShow?.()
|
||||||
open.value = true
|
open.value = true
|
||||||
|
|
||||||
addBodyPadding()
|
addBodyPadding()
|
||||||
|
|||||||
Reference in New Issue
Block a user