feat(frontend): Improve revenue information (#3250)

* Improve revenue information

* Improve NET 60 period info + show next period if current period is over.

* invert period check

* %

* Finalize changes

* Cleanup

* Remove .idea

* Discard changes to .idea/discord.xml

* Discard changes to .idea/code.iml

* Discard changes to .idea/.gitignore

* Discard changes to .idea/libraries/KotlinJavaRuntime.xml

* Discard changes to .idea/vcs.xml

* Discard changes to .idea/modules.xml

* Discard changes to .idea/.gitignore

* fix lint issues

* table fix, lint fix and media sizing fix

* fix responsiveness

* Remove comment

* utc comment

* fix lint
This commit is contained in:
Calum H.
2025-02-21 01:52:10 +00:00
committed by GitHub
parent 067f471766
commit c77f3395b2
4 changed files with 269 additions and 69 deletions

View File

@@ -2,38 +2,87 @@
<div>
<section class="universal-card">
<h2 class="text-2xl">Revenue</h2>
<div v-if="userBalance.available >= minWithdraw">
<p>
You have
<strong>{{ $formatMoney(userBalance.available) }}</strong>
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p>
<div class="grid-display">
<div class="grid-display__item">
<div class="label">Available now</div>
<div class="value">
{{ $formatMoney(userBalance.available) }}
</div>
</div>
<div class="grid-display__item">
<div class="label">
Total pending
<nuxt-link
v-tooltip="`Click to read about how Modrinth handles your revenue.`"
class="align-middle text-link"
to="/legal/cmp-info#pending"
>
<UnknownIcon />
</nuxt-link>
</div>
<div class="value">
{{ $formatMoney(userBalance.pending) }}
</div>
</div>
<div class="grid-display__item available-soon">
<h3 class="label">
Available soon
<nuxt-link
v-tooltip="`Click to read about how Modrinth handles your revenue.`"
class="align-middle text-link"
to="/legal/cmp-info#pending"
>
<UnknownIcon />
</nuxt-link>
</h3>
<ul class="available-soon-list">
<li v-for="date in availableSoonDateKeys" :key="date" class="available-soon-item">
<span class="amount">
{{ $formatMoney(availableSoonDates[date]) }}
<small
v-if="availableSoonDateKeys.indexOf(date) === availableSoonDateKeys.length - 1"
></small
>
</span>
<span class="date">
{{ formatDate(dayjs(date)) }}
</span>
</li>
</ul>
</div>
</div>
<p v-else>
You have made
<strong>{{ $formatMoney(userBalance.available) }}</strong
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
<strong>{{ $formatMoney(userBalance.pending) }}</strong> of your balance is
<nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p>
<div class="input-group mt-4">
<nuxt-link
v-if="userBalance.available >= minWithdraw"
class="iconified-button brand-button"
to="/dashboard/revenue/withdraw"
>
<TransferIcon /> Withdraw
</nuxt-link>
<span :class="{ 'disabled-cursor-wrapper': userBalance.available < minWithdraw }">
<nuxt-link
:aria-disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
:class="{ 'disabled-link': userBalance.available < minWithdraw }"
:disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
:tabindex="userBalance.available < minWithdraw ? -1 : undefined"
class="iconified-button brand-button"
to="/dashboard/revenue/withdraw"
>
<TransferIcon /> Withdraw
</nuxt-link>
</span>
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
<HistoryIcon /> View transfer history
<HistoryIcon />
View transfer history
</NuxtLink>
</div>
<p>
By uploading projects to Modrinth and withdrawing money from your account, you agree to the
<nuxt-link to="/legal/cmp" class="text-link">Rewards Program Terms</nuxt-link>. For more
information on how the rewards system works, see our information page
<nuxt-link to="/legal/cmp-info" class="text-link">here</nuxt-link>.
<small>
By uploading projects to Modrinth and withdrawing money from your account, you agree to
the
<nuxt-link class="text-link" to="/legal/cmp">Rewards Program Terms</nuxt-link>. For more
information on how the rewards system works, see our information page
<nuxt-link class="text-link" to="/legal/cmp-info">here</nuxt-link>.
</small>
</p>
<p>
<small>
Ongoing revenue period, subject to change. The finalized amount will be available to
view on the last day of the current month.
</small>
</p>
</section>
<section class="universal-card">
@@ -46,12 +95,13 @@
{{ auth.user.payout_data.paypal_address }}
</p>
<button class="btn mt-4" @click="removeAuthProvider('paypal')">
<XIcon /> Disconnect account
<XIcon />
Disconnect account
</button>
</template>
<template v-else>
<p>Connect your PayPal account to enable withdrawing to your PayPal balance.</p>
<a class="btn mt-4" :href="`${getAuthUrl('paypal')}&token=${auth.token}`">
<a :href="`${getAuthUrl('paypal')}&token=${auth.token}`" class="btn mt-4">
<PayPalIcon />
Sign in with PayPal
</a>
@@ -60,7 +110,8 @@
<p>
Tremendous payments are sent to your Modrinth email. To change/set your Modrinth email,
visit
<nuxt-link to="/settings/account" class="text-link">here</nuxt-link>.
<nuxt-link class="text-link" to="/settings/account">here</nuxt-link>
.
</p>
<h3>Venmo</h3>
<p>Enter your Venmo username below to enable withdrawing to your Venmo balance.</p>
@@ -68,18 +119,31 @@
<input
id="venmo"
v-model="auth.user.payout_data.venmo_handle"
autocomplete="off"
class="mt-4"
type="search"
name="search"
placeholder="@example"
autocomplete="off"
type="search"
/>
<button class="btn btn-secondary" @click="updateVenmo"><SaveIcon /> Save information</button>
<button class="btn btn-secondary" @click="updateVenmo">
<SaveIcon />
Save information
</button>
</section>
</div>
</template>
<script setup>
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from "@modrinth/assets";
import {
HistoryIcon,
PayPalIcon,
SaveIcon,
TransferIcon,
UnknownIcon,
XIcon,
} from "@modrinth/assets";
import { formatDate } from "@modrinth/utils";
import dayjs from "dayjs";
import { computed } from "vue";
const auth = await useAuth();
const minWithdraw = ref(0.01);
@@ -88,6 +152,33 @@ const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
);
const deadlineEnding = computed(() => {
let deadline = dayjs().subtract(2, "month").endOf("month").add(60, "days");
if (deadline.isBefore(dayjs().startOf("day"))) {
deadline = dayjs().subtract(1, "month").endOf("month").add(60, "days");
}
return deadline;
});
const availableSoonDates = computed(() => {
// Get the next 3 dates from userBalance.dates that are from now to the deadline + 4 months to make sure we get all the pending ones.
const dates = Object.keys(userBalance.value.dates)
.filter((date) => {
const dateObj = dayjs(date);
return (
dateObj.isAfter(dayjs()) && dateObj.isBefore(dayjs(deadlineEnding.value).add(4, "month"))
);
})
.sort((a, b) => dayjs(a).diff(dayjs(b)));
return dates.reduce((acc, date) => {
acc[date] = userBalance.value.dates[date];
return acc;
}, {});
});
const availableSoonDateKeys = computed(() => Object.keys(availableSoonDates.value));
async function updateVenmo() {
startLoading();
try {
@@ -118,4 +209,57 @@ strong {
color: var(--color-text-dark);
font-weight: 500;
}
.disabled-cursor-wrapper {
cursor: not-allowed;
}
.disabled-link {
pointer-events: none;
}
.grid-display {
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
.available-soon {
padding-top: 0;
.label {
margin: 0;
}
&-list {
list-style-type: none;
padding: 0;
margin: 0;
}
&-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.2rem 0 0;
border-bottom: 1px solid var(--color-divider);
.amount {
font-weight: 600;
small {
vertical-align: top;
margin: 0;
padding: 0;
}
}
.date {
color: var(--color-text-secondary);
font-size: 0.9em;
}
&:last-child {
border-bottom: none;
}
}
}
</style>

View File

@@ -82,42 +82,41 @@
<p>
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all
revenue is immediately available to withdraw. We pay creators as soon as we receive the money
from our ad providers, which is 60 days after the last day of each month. This table outlines
some example dates of how NET 60 payments are made:
from our ad providers, which is 60 days after the last day of each month.
</p>
<p>
To understand when revenue becomes available, you can use this calculator to estimate when
revenue earned on a specific date will be available for withdrawal. Please be advised that all
dates within this calculator are represented at 00:00 UTC.
</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Payment available date</th>
</tr>
</thead>
<tbody>
<tr>
<td>January 1st</td>
<td>March 31st</td>
</tr>
<tr>
<td>January 15th</td>
<td>March 31st</td>
</tr>
<tr>
<td>March 3rd</td>
<td>May 30th</td>
</tr>
<tr>
<td>June 30th</td>
<td>August 29th</td>
</tr>
<tr>
<td>July 14th</td>
<td>September 29th</td>
</tr>
<tr>
<td>October 12th</td>
<td>December 30th</td>
</tr>
</tbody>
<tr>
<th>Timeline</th>
<th>Date</th>
</tr>
<tr>
<td>Revenue earned on</td>
<td>
<input id="revenue-date-picker" v-model="rawSelectedDate" type="date" />
<noscript
>(JavaScript must be enabled for the date picker to function, example date: 2024-07-15)
</noscript>
</td>
</tr>
<tr>
<td>End of the month</td>
<td>{{ formatDate(endOfMonthDate) }}</td>
</tr>
<tr>
<td>NET 60 policy applied</td>
<td>+ 60 days</td>
</tr>
<tr class="final-result">
<td>Available for withdrawal</td>
<td>{{ formatDate(withdrawalDate) }}</td>
</tr>
</table>
<h3>How do I know Modrinth is being transparent about revenue?</h3>
<p>
@@ -127,12 +126,40 @@
revenue distribution system</a
>. We also have an
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
to query exact daily revenue for the site.
to query exact daily revenue for the site - so far, Modrinth has generated
<strong>{{ formatMoney(platformRevenue) }}</strong> in revenue.
</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Revenue</th>
<th>Creator Revenue (75%)</th>
<th>Modrinth's Cut (25%)</th>
</tr>
</thead>
<tbody>
<tr v-for="item in platformRevenueData" :key="item.time">
<td>{{ formatDate(dayjs.unix(item.time)) }}</td>
<td>{{ formatMoney(item.revenue) }}</td>
<td>{{ formatMoney(item.creator_revenue) }}</td>
<td>{{ formatMoney(item.revenue - item.creator_revenue) }}</td>
</tr>
</tbody>
</table>
<small
>Modrinth's total revenue in the previous 5 days, for the entire dataset, use the
aforementioned
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a>.</small
>
</div>
</template>
<script setup>
<script lang="ts" setup>
import dayjs from "dayjs";
import { computed, ref } from "vue";
import { formatDate, formatMoney } from "@modrinth/utils";
const description =
"Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.";
@@ -142,4 +169,18 @@ useSeoMeta({
ogTitle: "Rewards Program Information",
ogDescription: description,
});
const rawSelectedDate = ref(dayjs().format("YYYY-MM-DD"));
const selectedDate = computed(() => dayjs(rawSelectedDate.value));
const endOfMonthDate = computed(() => selectedDate.value.endOf("month"));
const withdrawalDate = computed(() => endOfMonthDate.value.add(60, "days"));
const { data: transparencyInformation } = await useAsyncData("payout/platform_revenue", () =>
useBaseFetch("payout/platform_revenue", {
apiVersion: 3,
}),
);
const platformRevenue = transparencyInformation.value.all_time;
const platformRevenueData = transparencyInformation.value.data.slice(0, 5);
</script>

View File

@@ -1,7 +1,11 @@
import dayjs from "dayjs";
import quarterOfYear from "dayjs/plugin/quarterOfYear";
import advanced from "dayjs/plugin/advancedFormat";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(quarterOfYear);
dayjs.extend(advanced);
dayjs.extend(relativeTime);
export default defineNuxtPlugin(() => {
return {

View File

@@ -87,6 +87,17 @@ export const formatNumber = (number, abbreviate = true) => {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export function formatDate(
date: dayjs.Dayjs,
options: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric',
},
): string {
return date.toDate().toLocaleDateString(undefined, options)
}
export function formatMoney(number, abbreviate = false) {
const x = Number(number)
if (x >= 1000000 && abbreviate) {