Add TailwindCSS (#1252)

* Setup TailwindCSS

* Fully setup configuration

* Refactor some tailwind variables
This commit is contained in:
Evan Song
2024-07-06 20:57:32 -07:00
committed by GitHub
parent 0f2ddb452c
commit abec2e48d4
176 changed files with 7905 additions and 7433 deletions

View File

@@ -5,20 +5,20 @@
</template>
<script setup>
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
import ChartDisplay from "~/components/ui/charts/ChartDisplay.vue";
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: 'Analytics - Modrinth',
})
title: "Analytics - Modrinth",
});
const auth = await useAuth()
const id = auth.value?.user?.id
const auth = await useAuth();
const id = auth.value?.user?.id;
const { data: projects } = await useAsyncData(`user/${id}/projects`, () =>
useBaseFetch(`user/${id}/projects`)
)
useBaseFetch(`user/${id}/projects`),
);
</script>

View File

@@ -88,71 +88,71 @@
</div>
</template>
<script setup>
import { BoxIcon, SearchIcon, XIcon, PlusIcon, LinkIcon, LockIcon } from '@modrinth/assets'
import { Avatar, Button } from '@modrinth/ui'
import WorldIcon from '~/assets/images/utils/world.svg?component'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import { BoxIcon, SearchIcon, XIcon, PlusIcon, LinkIcon, LockIcon } from "@modrinth/assets";
import { Avatar, Button } from "@modrinth/ui";
import WorldIcon from "~/assets/images/utils/world.svg?component";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
const { formatMessage } = useVIntl()
const formatCompactNumber = useCompactNumber()
const { formatMessage } = useVIntl();
const formatCompactNumber = useCompactNumber();
const messages = defineMessages({
createNewButton: {
id: 'dashboard.collections.button.create-new',
defaultMessage: 'Create new',
id: "dashboard.collections.button.create-new",
defaultMessage: "Create new",
},
collectionsLongTitle: {
id: 'dashboard.collections.long-title',
defaultMessage: 'Your collections',
id: "dashboard.collections.long-title",
defaultMessage: "Your collections",
},
followingCollectionDescription: {
id: 'collection.description.following',
id: "collection.description.following",
defaultMessage: "Auto-generated collection of all the projects you're following.",
},
projectsCountLabel: {
id: 'dashboard.collections.label.projects-count',
defaultMessage: '{count, plural, one {{count} project} other {{count} projects}}',
id: "dashboard.collections.label.projects-count",
defaultMessage: "{count, plural, one {{count} project} other {{count} projects}}",
},
searchInputLabel: {
id: 'dashboard.collections.label.search-input',
defaultMessage: 'Search your collections',
id: "dashboard.collections.label.search-input",
defaultMessage: "Search your collections",
},
})
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: () => `${formatMessage(messages.collectionsLongTitle)} - Modrinth`,
})
});
const user = await useUser()
const auth = await useAuth()
const user = await useUser();
const auth = await useAuth();
if (process.client) {
await initUserFollows()
await initUserFollows();
}
const filterQuery = ref('')
const filterQuery = ref("");
const { data: collections } = await useAsyncData(`user/${auth.value.user.id}/collections`, () =>
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 })
)
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
);
const orderedCollections = computed(() => {
if (!collections.value) return []
if (!collections.value) return [];
return collections.value
.sort((a, b) => {
const aUpdated = new Date(a.updated)
const bUpdated = new Date(b.updated)
return bUpdated - aUpdated
const aUpdated = new Date(a.updated);
const bUpdated = new Date(b.updated);
return bUpdated - aUpdated;
})
.filter((collection) => {
if (!filterQuery.value) return true
return collection.name.toLowerCase().includes(filterQuery.value.toLowerCase())
})
})
if (!filterQuery.value) return true;
return collection.name.toLowerCase().includes(filterQuery.value.toLowerCase());
});
});
</script>
<style lang="scss">
.collections-grid {

View File

@@ -41,7 +41,7 @@
class="goto-link view-more-notifs"
to="/dashboard/notifications"
>
View {{ extraNotifs }} more notification{{ extraNotifs === 1 ? '' : 's' }}
View {{ extraNotifs }} more notification{{ extraNotifs === 1 ? "" : "s" }}
<ChevronRightIcon />
</nuxt-link>
</template>
@@ -66,7 +66,7 @@
<span
>from
{{ downloadsProjectCount }}
project{{ downloadsProjectCount === 1 ? '' : 's' }}</span
project{{ downloadsProjectCount === 1 ? "" : "s" }}</span
>
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
<!-- >View breakdown-->
@@ -83,7 +83,7 @@
<span>
<span
>from {{ followersProjectCount }} project{{
followersProjectCount === 1 ? '' : 's'
followersProjectCount === 1 ? "" : "s"
}}</span
></span
>
@@ -108,58 +108,58 @@
</div>
</template>
<script setup>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
import HistoryIcon from '~/assets/images/utils/history.svg?component'
import Avatar from '~/components/ui/Avatar.vue'
import NotificationItem from '~/components/ui/NotificationItem.vue'
import { fetchExtraNotificationData, groupNotifications } from '~/helpers/notifications.js'
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
import HistoryIcon from "~/assets/images/utils/history.svg?component";
import Avatar from "~/components/ui/Avatar.vue";
import NotificationItem from "~/components/ui/NotificationItem.vue";
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.js";
useHead({
title: 'Dashboard - Modrinth',
})
title: "Dashboard - Modrinth",
});
const auth = await useAuth()
const auth = await useAuth();
const [{ data: projects }] = await Promise.all([
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
useBaseFetch(`user/${auth.value.user.id}/projects`)
useBaseFetch(`user/${auth.value.user.id}/projects`),
),
])
]);
const downloadsProjectCount = computed(
() => projects.value.filter((project) => project.downloads > 0).length
)
() => projects.value.filter((project) => project.downloads > 0).length,
);
const followersProjectCount = computed(
() => projects.value.filter((project) => project.followers > 0).length
)
() => projects.value.filter((project) => project.followers > 0).length,
);
const { data, refresh } = await useAsyncData(async () => {
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`);
const filteredNotifications = notifications.filter((notif) => !notif.read)
const slice = filteredNotifications.slice(0, 30) // send first 30 notifs to be grouped before trimming to 3
const filteredNotifications = notifications.filter((notif) => !notif.read);
const slice = filteredNotifications.slice(0, 30); // send first 30 notifs to be grouped before trimming to 3
return fetchExtraNotificationData(slice).then((notifications) => {
notifications = groupNotifications(notifications).slice(0, 3)
return { notifications, extraNotifs: filteredNotifications.length - slice.length }
})
})
notifications = groupNotifications(notifications).slice(0, 3);
return { notifications, extraNotifs: filteredNotifications.length - slice.length };
});
});
const notifications = computed(() => {
if (data.value === null) {
return []
return [];
}
return data.value.notifications
})
return data.value.notifications;
});
const extraNotifs = computed(() => (data.value ? data.value.extraNotifs : 0))
const extraNotifs = computed(() => (data.value ? data.value.extraNotifs : 0));
</script>
<style lang="scss">
.dashboard-overview {
display: grid;
grid-template:
'header header'
'notifications analytics' / 1fr auto;
"header header"
"notifications analytics" / 1fr auto;
gap: var(--spacing-card-md);
> .universal-card {

View File

@@ -50,110 +50,110 @@
</div>
</template>
<script setup>
import { Button } from '@modrinth/ui'
import { HistoryIcon } from '@modrinth/assets'
import { Button } from "@modrinth/ui";
import { HistoryIcon } from "@modrinth/assets";
import {
fetchExtraNotificationData,
groupNotifications,
markAsRead,
} from '~/helpers/notifications.js'
import NotificationItem from '~/components/ui/NotificationItem.vue'
import Chips from '~/components/ui/Chips.vue'
import CheckCheckIcon from '~/assets/images/utils/check-check.svg?component'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import Pagination from '~/components/ui/Pagination.vue'
} from "~/helpers/notifications.js";
import NotificationItem from "~/components/ui/NotificationItem.vue";
import Chips from "~/components/ui/Chips.vue";
import CheckCheckIcon from "~/assets/images/utils/check-check.svg?component";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import Pagination from "~/components/ui/Pagination.vue";
useHead({
title: 'Notifications - Modrinth',
})
title: "Notifications - Modrinth",
});
const auth = await useAuth()
const auth = await useAuth();
const route = useNativeRoute()
const router = useNativeRouter()
const route = useNativeRoute();
const router = useNativeRouter();
const history = computed(() => {
return route.name === 'dashboard-notifications-history'
})
return route.name === "dashboard-notifications-history";
});
const selectedType = ref('all')
const page = ref(1)
const selectedType = ref("all");
const page = ref(1);
const perPage = ref(50)
const perPage = ref(50);
const { data, pending, error, refresh } = await useAsyncData(
async () => {
const pageNum = page.value - 1
const pageNum = page.value - 1;
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
const showRead = history.value
const hasRead = notifications.some((notif) => notif.read)
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`);
const showRead = history.value;
const hasRead = notifications.some((notif) => notif.read);
const types = [
...new Set(
notifications
.filter((notification) => {
return showRead || !notification.read
return showRead || !notification.read;
})
.map((notification) => notification.type)
.map((notification) => notification.type),
),
]
];
const filteredNotifications = notifications.filter(
(notification) =>
(selectedType.value === 'all' || notification.type === selectedType.value) &&
(showRead || !notification.read)
)
const pages = Math.ceil(filteredNotifications.length / perPage.value)
(selectedType.value === "all" || notification.type === selectedType.value) &&
(showRead || !notification.read),
);
const pages = Math.ceil(filteredNotifications.length / perPage.value);
return fetchExtraNotificationData(
filteredNotifications.slice(pageNum * perPage.value, perPage.value + pageNum * perPage.value)
filteredNotifications.slice(pageNum * perPage.value, perPage.value + pageNum * perPage.value),
).then((notifications) => {
return {
notifications,
types: types.length > 1 ? ['all', ...types] : types,
types: types.length > 1 ? ["all", ...types] : types,
pages,
hasRead,
}
})
};
});
},
{ watch: [page, history, selectedType] }
)
{ watch: [page, history, selectedType] },
);
const notifications = computed(() => {
if (data.value === null) {
return []
return [];
}
return groupNotifications(data.value.notifications, history.value)
})
const notifTypes = computed(() => data.value.types)
const pages = computed(() => data.value.pages)
const hasRead = computed(() => data.value.hasRead)
return groupNotifications(data.value.notifications, history.value);
});
const notifTypes = computed(() => data.value.types);
const pages = computed(() => data.value.pages);
const hasRead = computed(() => data.value.hasRead);
function updateRoute() {
if (history.value) {
router.push('/dashboard/notifications')
router.push("/dashboard/notifications");
} else {
router.push('/dashboard/notifications/history')
router.push("/dashboard/notifications/history");
}
selectedType.value = 'all'
page.value = 1
selectedType.value = "all";
page.value = 1;
}
async function readAll() {
const ids = notifications.value.flatMap((notification) => [
notification.id,
...(notification.grouped_notifs ? notification.grouped_notifs.map((notif) => notif.id) : []),
])
]);
const updateNotifs = await markAsRead(ids)
allNotifs.value = updateNotifs(allNotifs.value)
const updateNotifs = await markAsRead(ids);
allNotifs.value = updateNotifs(allNotifs.value);
}
function changePage(newPage) {
page.value = newPage
page.value = newPage;
if (process.client) {
window.scrollTo({ top: 0, behavior: 'smooth' })
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
</script>

View File

@@ -49,36 +49,36 @@
</template>
<script setup>
import { PlusIcon, UsersIcon } from '@modrinth/assets'
import { Avatar } from '@modrinth/ui'
import { useAuth } from '~/composables/auth.js'
import OrganizationCreateModal from '~/components/ui/OrganizationCreateModal.vue'
import { PlusIcon, UsersIcon } from "@modrinth/assets";
import { Avatar } from "@modrinth/ui";
import { useAuth } from "~/composables/auth.js";
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
const createOrgModal = ref(null)
const createOrgModal = ref(null);
const auth = await useAuth()
const uid = computed(() => auth.value.user?.id || null)
const auth = await useAuth();
const uid = computed(() => auth.value.user?.id || null);
const { data: orgs, error } = useAsyncData('organizations', () => {
if (!uid.value) return Promise.resolve(null)
const { data: orgs, error } = useAsyncData("organizations", () => {
if (!uid.value) return Promise.resolve(null);
return useBaseFetch('user/' + uid.value + '/organizations', {
return useBaseFetch("user/" + uid.value + "/organizations", {
apiVersion: 3,
})
})
});
});
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted)
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
if (error.value) {
createError({
statusCode: 500,
message: 'Failed to fetch organizations',
})
message: "Failed to fetch organizations",
});
}
const openCreateOrgModal = () => {
createOrgModal.value?.show()
}
createOrgModal.value?.show();
};
</script>
<style scoped lang="scss">

View File

@@ -119,14 +119,14 @@
<p>
Changes will be applied to
<strong>{{ selectedProjects.length }}</strong> project{{
selectedProjects.length > 1 ? 's' : ''
selectedProjects.length > 1 ? "s" : ""
}}.
</p>
<ul>
<li
v-for="project in selectedProjects.slice(
0,
editLinks.showAffected ? selectedProjects.length : 3
editLinks.showAffected ? selectedProjects.length : 3,
)"
:key="project.id"
>
@@ -300,24 +300,24 @@
</template>
<script>
import { Multiselect } from 'vue-multiselect'
import { Multiselect } from "vue-multiselect";
import Badge from '~/components/ui/Badge.vue'
import Checkbox from '~/components/ui/Checkbox.vue'
import Modal from '~/components/ui/Modal.vue'
import Avatar from '~/components/ui/Avatar.vue'
import ModalCreation from '~/components/ui/ModalCreation.vue'
import CopyCode from '~/components/ui/CopyCode.vue'
import Badge from "~/components/ui/Badge.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import Modal from "~/components/ui/Modal.vue";
import Avatar from "~/components/ui/Avatar.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import SettingsIcon from '~/assets/images/utils/settings.svg?component'
import TrashIcon from '~/assets/images/utils/trash.svg?component'
import IssuesIcon from '~/assets/images/utils/issues.svg?component'
import PlusIcon from '~/assets/images/utils/plus.svg?component'
import CrossIcon from '~/assets/images/utils/x.svg?component'
import EditIcon from '~/assets/images/utils/edit.svg?component'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import AscendingIcon from '~/assets/images/utils/sort-asc.svg?component'
import DescendingIcon from '~/assets/images/utils/sort-desc.svg?component'
import SettingsIcon from "~/assets/images/utils/settings.svg?component";
import TrashIcon from "~/assets/images/utils/trash.svg?component";
import IssuesIcon from "~/assets/images/utils/issues.svg?component";
import PlusIcon from "~/assets/images/utils/plus.svg?component";
import CrossIcon from "~/assets/images/utils/x.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import AscendingIcon from "~/assets/images/utils/sort-asc.svg?component";
import DescendingIcon from "~/assets/images/utils/sort-desc.svg?component";
export default defineNuxtComponent({
components: {
@@ -339,97 +339,97 @@ export default defineNuxtComponent({
DescendingIcon,
},
async setup() {
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const user = await useUser()
await initUserProjects()
return { formatMessage, user: ref(user) }
const user = await useUser();
await initUserProjects();
return { formatMessage, user: ref(user) };
},
data() {
return {
projects: this.updateSort(this.user.projects, 'Name'),
projects: this.updateSort(this.user.projects, "Name"),
versions: [],
selectedProjects: [],
sortBy: 'Name',
sortBy: "Name",
descending: false,
editLinks: {
showAffected: false,
source: {
val: '',
val: "",
clear: false,
},
discord: {
val: '',
val: "",
clear: false,
},
wiki: {
val: '',
val: "",
clear: false,
},
issues: {
val: '',
val: "",
clear: false,
},
},
}
};
},
head: {
title: 'Projects - Modrinth',
title: "Projects - Modrinth",
},
created() {
this.UPLOAD_VERSION = 1 << 0
this.DELETE_VERSION = 1 << 1
this.EDIT_DETAILS = 1 << 2
this.EDIT_BODY = 1 << 3
this.MANAGE_INVITES = 1 << 4
this.REMOVE_MEMBER = 1 << 5
this.EDIT_MEMBER = 1 << 6
this.DELETE_PROJECT = 1 << 7
this.UPLOAD_VERSION = 1 << 0;
this.DELETE_VERSION = 1 << 1;
this.EDIT_DETAILS = 1 << 2;
this.EDIT_BODY = 1 << 3;
this.MANAGE_INVITES = 1 << 4;
this.REMOVE_MEMBER = 1 << 5;
this.EDIT_MEMBER = 1 << 6;
this.DELETE_PROJECT = 1 << 7;
},
methods: {
updateDescending() {
this.descending = !this.descending
this.projects = this.updateSort(this.projects, this.sortBy, this.descending)
this.descending = !this.descending;
this.projects = this.updateSort(this.projects, this.sortBy, this.descending);
},
updateSort(projects, sort, descending) {
let sortedArray = projects
let sortedArray = projects;
switch (sort) {
case 'Name':
case "Name":
sortedArray = projects.slice().sort((a, b) => {
return a.title.localeCompare(b.title)
})
break
case 'Status':
return a.title.localeCompare(b.title);
});
break;
case "Status":
sortedArray = projects.slice().sort((a, b) => {
if (a.status < b.status) {
return -1
return -1;
}
if (a.status > b.status) {
return 1
return 1;
}
return 0
})
break
case 'Type':
return 0;
});
break;
case "Type":
sortedArray = projects.slice().sort((a, b) => {
if (a.project_type < b.project_type) {
return -1
return -1;
}
if (a.project_type > b.project_type) {
return 1
return 1;
}
return 0
})
break
return 0;
});
break;
default:
break
break;
}
if (descending) {
sortedArray = sortedArray.reverse()
sortedArray = sortedArray.reverse();
}
return sortedArray
return sortedArray;
},
async bulkEditLinks() {
try {
@@ -438,60 +438,60 @@ export default defineNuxtComponent({
source_url: this.editLinks.source.clear ? null : this.editLinks.source.val.trim(),
wiki_url: this.editLinks.wiki.clear ? null : this.editLinks.wiki.val.trim(),
discord_url: this.editLinks.discord.clear ? null : this.editLinks.discord.val.trim(),
}
};
if (!baseData.issues_url?.length ?? 1 > 0) {
delete baseData.issues_url
delete baseData.issues_url;
}
if (!baseData.source_url?.length ?? 1 > 0) {
delete baseData.source_url
delete baseData.source_url;
}
if (!baseData.wiki_url?.length ?? 1 > 0) {
delete baseData.wiki_url
delete baseData.wiki_url;
}
if (!baseData.discord_url?.length ?? 1 > 0) {
delete baseData.discord_url
delete baseData.discord_url;
}
await useBaseFetch(
`projects?ids=${JSON.stringify(this.selectedProjects.map((x) => x.id))}`,
{
method: 'PATCH',
method: "PATCH",
body: baseData,
}
)
},
);
this.$refs.editLinksModal.hide()
this.$refs.editLinksModal.hide();
this.$notify({
group: 'main',
title: 'Success',
group: "main",
title: "Success",
text: "Bulk edited selected project's links.",
type: 'success',
})
this.selectedProjects = []
type: "success",
});
this.selectedProjects = [];
this.editLinks.issues.val = ''
this.editLinks.source.val = ''
this.editLinks.wiki.val = ''
this.editLinks.discord.val = ''
this.editLinks.issues.clear = false
this.editLinks.source.clear = false
this.editLinks.wiki.clear = false
this.editLinks.discord.clear = false
this.editLinks.issues.val = "";
this.editLinks.source.val = "";
this.editLinks.wiki.val = "";
this.editLinks.discord.val = "";
this.editLinks.issues.clear = false;
this.editLinks.source.clear = false;
this.editLinks.wiki.clear = false;
this.editLinks.discord.clear = false;
} catch (e) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: e,
type: 'error',
})
type: "error",
});
}
},
},
})
});
</script>
<style lang="scss" scoped>
.grid-table {
@@ -543,7 +543,7 @@ export default defineNuxtComponent({
.grid-table__row {
display: grid;
grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings';
grid-template: "checkbox icon name type settings" "checkbox icon id status settings";
grid-template-columns:
min-content min-content minmax(min-content, 2fr)
minmax(min-content, 1fr) min-content;
@@ -580,7 +580,7 @@ export default defineNuxtComponent({
}
.grid-table__header {
grid-template: 'checkbox settings';
grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr);
:nth-child(2),
@@ -596,7 +596,7 @@ export default defineNuxtComponent({
@media screen and (max-width: 560px) {
.grid-table__row {
display: grid;
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
grid-template: "checkbox icon name settings" "checkbox icon id settings" "checkbox icon type settings" "checkbox icon status settings";
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
:nth-child(5) {
@@ -605,7 +605,7 @@ export default defineNuxtComponent({
}
.grid-table__header {
grid-template: 'checkbox settings';
grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr);
}
}
@@ -644,7 +644,7 @@ export default defineNuxtComponent({
width: fit-content;
}
.label-button[data-active='true'] {
.label-button[data-active="true"] {
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}

View File

@@ -6,12 +6,12 @@
/>
</template>
<script setup>
import ReportView from '~/components/ui/report/ReportView.vue'
import ReportView from "~/components/ui/report/ReportView.vue";
const route = useNativeRoute()
const auth = await useAuth()
const route = useNativeRoute();
const auth = await useAuth();
useHead({
title: `Report ${route.params.id} - Modrinth`,
})
});
</script>

View File

@@ -7,10 +7,10 @@
</div>
</template>
<script setup>
import ReportsList from '~/components/ui/report/ReportsList.vue'
import ReportsList from "~/components/ui/report/ReportsList.vue";
const auth = await useAuth()
const auth = await useAuth();
useHead({
title: 'Active reports - Modrinth',
})
title: "Active reports - Modrinth",
});
</script>

View File

@@ -75,34 +75,34 @@
</div>
</template>
<script setup>
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from '@modrinth/assets'
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from "@modrinth/assets";
const auth = await useAuth()
const minWithdraw = ref(0.01)
const auth = await useAuth();
const minWithdraw = ref(0.01);
async function updateVenmo() {
startLoading()
startLoading();
try {
const data = {
venmo_handle: auth.value.user.payout_data.venmo_handle ?? null,
}
};
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'PATCH',
method: "PATCH",
body: data,
apiVersion: 3,
})
await useAuth(auth.value.token)
});
await useAuth(auth.value.token);
} catch (err) {
const data = useNuxtApp()
const data = useNuxtApp();
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -25,8 +25,8 @@
</div>
<p>
{{
selectedYear !== 'all'
? selectedMethod !== 'all'
selectedYear !== "all"
? selectedMethod !== "all"
? formatMessage(messages.transfersTotalYearMethod, {
amount: $formatMoney(totalAmount),
year: selectedYear,
@@ -36,12 +36,12 @@
amount: $formatMoney(totalAmount),
year: selectedYear,
})
: selectedMethod !== 'all'
? formatMessage(messages.transfersTotalMethod, {
amount: $formatMoney(totalAmount),
method: selectedMethod,
})
: formatMessage(messages.transfersTotal, { amount: $formatMoney(totalAmount) })
: selectedMethod !== "all"
? formatMessage(messages.transfersTotalMethod, {
amount: $formatMoney(totalAmount),
method: selectedMethod,
})
: formatMessage(messages.transfersTotal, { amount: $formatMoney(totalAmount) })
}}
</p>
<div
@@ -58,7 +58,7 @@
<div class="payout-info">
<div>
<strong>
{{ $dayjs(payout.created).format('MMMM D, YYYY [at] h:mm A') }}
{{ $dayjs(payout.created).format("MMMM D, YYYY [at] h:mm A") }}
</strong>
</div>
<div>
@@ -94,96 +94,96 @@
</div>
</template>
<script setup>
import { DropdownSelect } from '@modrinth/ui'
import { XIcon, PayPalIcon, UnknownIcon } from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils'
import { Badge, Breadcrumbs } from '@modrinth/ui'
import dayjs from 'dayjs'
import TremendousIcon from '~/assets/images/external/tremendous.svg?component'
import VenmoIcon from '~/assets/images/external/venmo-small.svg?component'
import { DropdownSelect } from "@modrinth/ui";
import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets";
import { capitalizeString } from "@modrinth/utils";
import { Badge, Breadcrumbs } from "@modrinth/ui";
import dayjs from "dayjs";
import TremendousIcon from "~/assets/images/external/tremendous.svg?component";
import VenmoIcon from "~/assets/images/external/venmo-small.svg?component";
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
useHead({
title: 'Transfer history - Modrinth',
})
title: "Transfer history - Modrinth",
});
const data = await useNuxtApp()
const auth = await useAuth()
const data = await useNuxtApp();
const auth = await useAuth();
const { data: payouts, refresh } = await useAsyncData(`payout`, () =>
useBaseFetch(`payout`, {
apiVersion: 3,
})
)
}),
);
const sortedPayouts = computed(() =>
payouts.value.sort((a, b) => dayjs(b.created) - dayjs(a.created))
)
payouts.value.sort((a, b) => dayjs(b.created) - dayjs(a.created)),
);
const years = computed(() => {
const values = sortedPayouts.value.map((x) => dayjs(x.created).year())
return ['all', ...new Set(values)]
})
const values = sortedPayouts.value.map((x) => dayjs(x.created).year());
return ["all", ...new Set(values)];
});
const selectedYear = ref('all')
const selectedYear = ref("all");
const methods = computed(() => {
const values = sortedPayouts.value.filter((x) => x.method).map((x) => x.method)
return ['all', ...new Set(values)]
})
const values = sortedPayouts.value.filter((x) => x.method).map((x) => x.method);
return ["all", ...new Set(values)];
});
const selectedMethod = ref('all')
const selectedMethod = ref("all");
const filteredPayouts = computed(() =>
sortedPayouts.value
.filter((x) => selectedYear.value === 'all' || dayjs(x.created).year() === selectedYear.value)
.filter((x) => selectedMethod.value === 'all' || x.method === selectedMethod.value)
)
.filter((x) => selectedYear.value === "all" || dayjs(x.created).year() === selectedYear.value)
.filter((x) => selectedMethod.value === "all" || x.method === selectedMethod.value),
);
const totalAmount = computed(() =>
filteredPayouts.value.reduce((sum, payout) => sum + payout.amount, 0)
)
filteredPayouts.value.reduce((sum, payout) => sum + payout.amount, 0),
);
async function cancelPayout(id) {
startLoading()
startLoading();
try {
await useBaseFetch(`payout/${id}`, {
method: 'DELETE',
method: "DELETE",
apiVersion: 3,
})
await refresh()
await useAuth(auth.value.token)
});
await refresh();
await useAuth(auth.value.token);
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const messages = defineMessages({
transfersTotal: {
id: 'revenue.transfers.total',
defaultMessage: 'You have withdrawn {amount} in total.',
id: "revenue.transfers.total",
defaultMessage: "You have withdrawn {amount} in total.",
},
transfersTotalYear: {
id: 'revenue.transfers.total.year',
defaultMessage: 'You have withdrawn {amount} in {year}.',
id: "revenue.transfers.total.year",
defaultMessage: "You have withdrawn {amount} in {year}.",
},
transfersTotalMethod: {
id: 'revenue.transfers.total.method',
defaultMessage: 'You have withdrawn {amount} through {method}.',
id: "revenue.transfers.total.method",
defaultMessage: "You have withdrawn {amount} through {method}.",
},
transfersTotalYearMethod: {
id: 'revenue.transfers.total.year_method',
defaultMessage: 'You have withdrawn {amount} in {year} through {method}.',
id: "revenue.transfers.total.year_method",
defaultMessage: "You have withdrawn {amount} in {year} through {method}.",
},
})
});
</script>
<style lang="scss" scoped>
.payout {

View File

@@ -39,7 +39,7 @@
<div class="withdraw-options">
<button
v-for="method in payoutMethods.filter((x) =>
x.name.toLowerCase().includes(search.toLowerCase())
x.name.toLowerCase().includes(search.toLowerCase()),
)"
:key="method.id"
class="withdraw-option button-base"
@@ -53,8 +53,8 @@
{{
getRangeOfMethod(method)
.map($formatMoney)
.map((i) => i.replace('.00', ''))
.join('')
.map((i) => i.replace(".00", ""))
.join("")
}}
</span>
</div>
@@ -183,7 +183,7 @@
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Multiselect } from "vue-multiselect";
import {
PayPalIcon,
SearchIcon,
@@ -191,182 +191,182 @@ import {
RadioButtonChecked,
XIcon,
TransferIcon,
} from '@modrinth/assets'
import { Chips, Checkbox, Breadcrumbs } from '@modrinth/ui'
import { all } from 'iso-3166-1'
import VenmoIcon from '~/assets/images/external/venmo.svg?component'
} from "@modrinth/assets";
import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui";
import { all } from "iso-3166-1";
import VenmoIcon from "~/assets/images/external/venmo.svg?component";
const auth = await useAuth()
const data = useNuxtApp()
const auth = await useAuth();
const data = useNuxtApp();
const countries = computed(() =>
all().map((x) => ({
id: x.alpha2,
name: x.alpha2 === 'TW' ? 'Taiwan' : x.country,
}))
)
const search = ref('')
name: x.alpha2 === "TW" ? "Taiwan" : x.country,
})),
);
const search = ref("");
const amount = ref('')
const amount = ref("");
const country = ref(
countries.value.find((x) => x.id === (auth.value.user.payout_data.paypal_region ?? 'US'))
)
countries.value.find((x) => x.id === (auth.value.user.payout_data.paypal_region ?? "US")),
);
const { data: payoutMethods, refresh: refreshPayoutMethods } = await useAsyncData(
`payout/methods?country=${country.value.id}`,
() => useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 })
)
() => useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
);
const selectedMethodId = ref(payoutMethods.value[0].id)
const selectedMethodId = ref(payoutMethods.value[0].id);
const selectedMethod = computed(() =>
payoutMethods.value.find((x) => x.id === selectedMethodId.value)
)
payoutMethods.value.find((x) => x.id === selectedMethodId.value),
);
const parsedAmount = computed(() => {
const regex = /^\$?(\d*(\.\d{2})?)$/gm
const matches = regex.exec(amount.value)
return matches && matches[1] ? parseFloat(matches[1]) : 0.0
})
const regex = /^\$?(\d*(\.\d{2})?)$/gm;
const matches = regex.exec(amount.value);
return matches && matches[1] ? parseFloat(matches[1]) : 0.0;
});
const fees = computed(() => {
return Math.min(
Math.max(
selectedMethod.value.fee.min,
selectedMethod.value.fee.percentage * parsedAmount.value
selectedMethod.value.fee.percentage * parsedAmount.value,
),
selectedMethod.value.fee.max ?? Number.MAX_VALUE
)
})
selectedMethod.value.fee.max ?? Number.MAX_VALUE,
);
});
const getIntervalRange = (intervalType) => {
if (!intervalType) {
return []
return [];
}
const { min, max, values } = intervalType
const { min, max, values } = intervalType;
if (values) {
const first = values[0]
const last = values.slice(-1)[0]
return first === last ? [first] : [first, last]
const first = values[0];
const last = values.slice(-1)[0];
return first === last ? [first] : [first, last];
}
return min === max ? [min] : [min, max]
}
return min === max ? [min] : [min, max];
};
const getRangeOfMethod = (method) => {
return getIntervalRange(method.interval?.fixed || method.interval?.standard)
}
return getIntervalRange(method.interval?.fixed || method.interval?.standard);
};
const maxWithdrawAmount = computed(() => {
const interval = selectedMethod.value.interval
return interval?.standard ? interval.standard.max : interval?.fixed?.values.slice(-1)[0] ?? 0
})
const interval = selectedMethod.value.interval;
return interval?.standard ? interval.standard.max : interval?.fixed?.values.slice(-1)[0] ?? 0;
});
const minWithdrawAmount = computed(() => {
const interval = selectedMethod.value.interval
return interval?.standard ? interval.standard.min : interval?.fixed?.values?.[0] ?? fees.value
})
const interval = selectedMethod.value.interval;
return interval?.standard ? interval.standard.min : interval?.fixed?.values?.[0] ?? fees.value;
});
const withdrawAccount = computed(() => {
if (selectedMethod.value.type === 'paypal') {
return auth.value.user.payout_data.paypal_address
} else if (selectedMethod.value.type === 'venmo') {
return auth.value.user.payout_data.venmo_handle
if (selectedMethod.value.type === "paypal") {
return auth.value.user.payout_data.paypal_address;
} else if (selectedMethod.value.type === "venmo") {
return auth.value.user.payout_data.venmo_handle;
} else {
return auth.value.user.email
return auth.value.user.email;
}
})
});
const knownErrors = computed(() => {
const errors = []
if (selectedMethod.value.type === 'paypal' && !auth.value.user.payout_data.paypal_address) {
errors.push('Please link your PayPal account in the dashboard to proceed.')
const errors = [];
if (selectedMethod.value.type === "paypal" && !auth.value.user.payout_data.paypal_address) {
errors.push("Please link your PayPal account in the dashboard to proceed.");
}
if (selectedMethod.value.type === 'venmo' && !auth.value.user.payout_data.venmo_handle) {
errors.push('Please set your Venmo handle in the dashboard to proceed.')
if (selectedMethod.value.type === "venmo" && !auth.value.user.payout_data.venmo_handle) {
errors.push("Please set your Venmo handle in the dashboard to proceed.");
}
if (selectedMethod.value.type === 'tremendous') {
if (selectedMethod.value.type === "tremendous") {
if (!auth.value.user.email) {
errors.push('Please set your email address in your account settings to proceed.')
errors.push("Please set your email address in your account settings to proceed.");
}
if (!auth.value.user.email_verified) {
errors.push('Please verify your email address to proceed.')
errors.push("Please verify your email address to proceed.");
}
}
if (!parsedAmount.value && amount.value.length > 0) {
errors.push(`${amount.value} is not a valid amount`)
errors.push(`${amount.value} is not a valid amount`);
} else if (
parsedAmount.value > auth.value.user.payout_data.balance ||
parsedAmount.value > maxWithdrawAmount.value
) {
const maxAmount = Math.min(auth.value.user.payout_data.balance, maxWithdrawAmount.value)
errors.push(`The amount must be no more than ${data.$formatMoney(maxAmount)}`)
const maxAmount = Math.min(auth.value.user.payout_data.balance, maxWithdrawAmount.value);
errors.push(`The amount must be no more than ${data.$formatMoney(maxAmount)}`);
} else if (parsedAmount.value <= fees.value || parsedAmount.value < minWithdrawAmount.value) {
const minAmount = Math.max(fees.value + 0.01, minWithdrawAmount.value)
errors.push(`The amount must be at least ${data.$formatMoney(minAmount)}`)
const minAmount = Math.max(fees.value + 0.01, minWithdrawAmount.value);
errors.push(`The amount must be at least ${data.$formatMoney(minAmount)}`);
}
return errors
})
return errors;
});
const agreedTransfer = ref(false)
const agreedFees = ref(false)
const agreedTerms = ref(false)
const agreedTransfer = ref(false);
const agreedFees = ref(false);
const agreedTerms = ref(false);
watch(country, async () => {
await refreshPayoutMethods()
await refreshPayoutMethods();
if (payoutMethods.value && payoutMethods.value[0]) {
selectedMethodId.value = payoutMethods.value[0].id
selectedMethodId.value = payoutMethods.value[0].id;
}
})
});
watch(selectedMethod, () => {
if (selectedMethod.value.interval?.fixed) {
amount.value = selectedMethod.value.interval.fixed.values[0]
amount.value = selectedMethod.value.interval.fixed.values[0];
}
if (maxWithdrawAmount.value === minWithdrawAmount.value) {
amount.value = maxWithdrawAmount.value
amount.value = maxWithdrawAmount.value;
}
agreedTransfer.value = false
agreedFees.value = false
agreedTerms.value = false
})
agreedTransfer.value = false;
agreedFees.value = false;
agreedTerms.value = false;
});
async function withdraw() {
startLoading()
startLoading();
try {
const auth = await useAuth()
const auth = await useAuth();
await useBaseFetch(`payout`, {
method: 'POST',
method: "POST",
body: {
amount: parsedAmount.value,
method: selectedMethod.value.type,
method_id: selectedMethod.value.id,
},
apiVersion: 3,
})
await useAuth(auth.value.token)
await navigateTo('/dashboard/revenue')
});
await useAuth(auth.value.token);
await navigateTo("/dashboard/revenue");
data.$notify({
group: 'main',
title: 'Withdrawal complete',
group: "main",
title: "Withdrawal complete",
text:
selectedMethod.value.type === 'tremendous'
? 'An email has been sent to your account with further instructions on how to redeem your payout!'
selectedMethod.value.type === "tremendous"
? "An email has been sent to your account with further instructions on how to redeem your payout!"
: `Payment has been sent to your ${data.$formatWallet(
selectedMethod.value.type
selectedMethod.value.type,
)} account!`,
type: 'success',
})
type: "success",
});
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>