You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -2,38 +2,87 @@
|
|||||||
<div>
|
<div>
|
||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
<h2 class="text-2xl">Revenue</h2>
|
<h2 class="text-2xl">Revenue</h2>
|
||||||
<div v-if="userBalance.available >= minWithdraw">
|
<div class="grid-display">
|
||||||
<p>
|
<div class="grid-display__item">
|
||||||
You have
|
<div class="label">Available now</div>
|
||||||
<strong>{{ $formatMoney(userBalance.available) }}</strong>
|
<div class="value">
|
||||||
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your
|
{{ $formatMoney(userBalance.available) }}
|
||||||
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
|
</div>
|
||||||
</p>
|
</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>
|
</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">
|
<div class="input-group mt-4">
|
||||||
<nuxt-link
|
<span :class="{ 'disabled-cursor-wrapper': userBalance.available < minWithdraw }">
|
||||||
v-if="userBalance.available >= minWithdraw"
|
<nuxt-link
|
||||||
class="iconified-button brand-button"
|
:aria-disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
|
||||||
to="/dashboard/revenue/withdraw"
|
:class="{ 'disabled-link': userBalance.available < minWithdraw }"
|
||||||
>
|
:disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
|
||||||
<TransferIcon /> Withdraw
|
:tabindex="userBalance.available < minWithdraw ? -1 : undefined"
|
||||||
</nuxt-link>
|
class="iconified-button brand-button"
|
||||||
|
to="/dashboard/revenue/withdraw"
|
||||||
|
>
|
||||||
|
<TransferIcon /> Withdraw
|
||||||
|
</nuxt-link>
|
||||||
|
</span>
|
||||||
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
||||||
<HistoryIcon /> View transfer history
|
<HistoryIcon />
|
||||||
|
View transfer history
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
By uploading projects to Modrinth and withdrawing money from your account, you agree to the
|
<small>
|
||||||
<nuxt-link to="/legal/cmp" class="text-link">Rewards Program Terms</nuxt-link>. For more
|
By uploading projects to Modrinth and withdrawing money from your account, you agree to
|
||||||
information on how the rewards system works, see our information page
|
the
|
||||||
<nuxt-link to="/legal/cmp-info" class="text-link">here</nuxt-link>.
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
@@ -46,12 +95,13 @@
|
|||||||
{{ auth.user.payout_data.paypal_address }}
|
{{ auth.user.payout_data.paypal_address }}
|
||||||
</p>
|
</p>
|
||||||
<button class="btn mt-4" @click="removeAuthProvider('paypal')">
|
<button class="btn mt-4" @click="removeAuthProvider('paypal')">
|
||||||
<XIcon /> Disconnect account
|
<XIcon />
|
||||||
|
Disconnect account
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>Connect your PayPal account to enable withdrawing to your PayPal balance.</p>
|
<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 />
|
<PayPalIcon />
|
||||||
Sign in with PayPal
|
Sign in with PayPal
|
||||||
</a>
|
</a>
|
||||||
@@ -60,7 +110,8 @@
|
|||||||
<p>
|
<p>
|
||||||
Tremendous payments are sent to your Modrinth email. To change/set your Modrinth email,
|
Tremendous payments are sent to your Modrinth email. To change/set your Modrinth email,
|
||||||
visit
|
visit
|
||||||
<nuxt-link to="/settings/account" class="text-link">here</nuxt-link>.
|
<nuxt-link class="text-link" to="/settings/account">here</nuxt-link>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
<h3>Venmo</h3>
|
<h3>Venmo</h3>
|
||||||
<p>Enter your Venmo username below to enable withdrawing to your Venmo balance.</p>
|
<p>Enter your Venmo username below to enable withdrawing to your Venmo balance.</p>
|
||||||
@@ -68,18 +119,31 @@
|
|||||||
<input
|
<input
|
||||||
id="venmo"
|
id="venmo"
|
||||||
v-model="auth.user.payout_data.venmo_handle"
|
v-model="auth.user.payout_data.venmo_handle"
|
||||||
|
autocomplete="off"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
type="search"
|
|
||||||
name="search"
|
name="search"
|
||||||
placeholder="@example"
|
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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<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 auth = await useAuth();
|
||||||
const minWithdraw = ref(0.01);
|
const minWithdraw = ref(0.01);
|
||||||
@@ -88,6 +152,33 @@ const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
|
|||||||
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
|
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() {
|
async function updateVenmo() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
@@ -118,4 +209,57 @@ strong {
|
|||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
font-weight: 500;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -82,42 +82,41 @@
|
|||||||
<p>
|
<p>
|
||||||
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all
|
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
|
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
|
from our ad providers, which is 60 days after the last day of each month.
|
||||||
some example dates of how NET 60 payments are made:
|
|
||||||
</p>
|
</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>
|
<table>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Timeline</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Payment available date</th>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<td>Revenue earned on</td>
|
||||||
<tbody>
|
<td>
|
||||||
<tr>
|
<input id="revenue-date-picker" v-model="rawSelectedDate" type="date" />
|
||||||
<td>January 1st</td>
|
<noscript
|
||||||
<td>March 31st</td>
|
>(JavaScript must be enabled for the date picker to function, example date: 2024-07-15)
|
||||||
</tr>
|
</noscript>
|
||||||
<tr>
|
</td>
|
||||||
<td>January 15th</td>
|
</tr>
|
||||||
<td>March 31st</td>
|
<tr>
|
||||||
</tr>
|
<td>End of the month</td>
|
||||||
<tr>
|
<td>{{ formatDate(endOfMonthDate) }}</td>
|
||||||
<td>March 3rd</td>
|
</tr>
|
||||||
<td>May 30th</td>
|
<tr>
|
||||||
</tr>
|
<td>NET 60 policy applied</td>
|
||||||
<tr>
|
<td>+ 60 days</td>
|
||||||
<td>June 30th</td>
|
</tr>
|
||||||
<td>August 29th</td>
|
<tr class="final-result">
|
||||||
</tr>
|
<td>Available for withdrawal</td>
|
||||||
<tr>
|
<td>{{ formatDate(withdrawalDate) }}</td>
|
||||||
<td>July 14th</td>
|
</tr>
|
||||||
<td>September 29th</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>October 12th</td>
|
|
||||||
<td>December 30th</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
<h3>How do I know Modrinth is being transparent about revenue?</h3>
|
<h3>How do I know Modrinth is being transparent about revenue?</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -127,12 +126,40 @@
|
|||||||
revenue distribution system</a
|
revenue distribution system</a
|
||||||
>. We also have an
|
>. We also have an
|
||||||
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
|
<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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { formatDate, formatMoney } from "@modrinth/utils";
|
||||||
|
|
||||||
const description =
|
const description =
|
||||||
"Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.";
|
"Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.";
|
||||||
|
|
||||||
@@ -142,4 +169,18 @@ useSeoMeta({
|
|||||||
ogTitle: "Rewards Program Information",
|
ogTitle: "Rewards Program Information",
|
||||||
ogDescription: description,
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
||||||
|
import advanced from "dayjs/plugin/advancedFormat";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
dayjs.extend(quarterOfYear);
|
dayjs.extend(quarterOfYear);
|
||||||
|
dayjs.extend(advanced);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ export const formatNumber = (number, abbreviate = true) => {
|
|||||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
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) {
|
export function formatMoney(number, abbreviate = false) {
|
||||||
const x = Number(number)
|
const x = Number(number)
|
||||||
if (x >= 1000000 && abbreviate) {
|
if (x >= 1000000 && abbreviate) {
|
||||||
|
|||||||
Reference in New Issue
Block a user