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

@@ -199,7 +199,7 @@
<BoxIcon />
<span>{{
$formatProjectType(
$getProjectTypeForDisplay(project.actualProjectType, project.loaders)
$getProjectTypeForDisplay(project.actualProjectType, project.loaders),
)
}}</span>
</nuxt-link>
@@ -759,7 +759,7 @@
$router.push(
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
}/version/${encodeURI(version.displayUrlEnding)}`,
)
"
>
@@ -784,7 +784,7 @@
{{ version.name }}
</nuxt-link>
<div v-if="version.game_versions.length > 0" class="game-version item">
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
{{ version.loaders.map((x) => $formatCategory(x)).join(", ") }}
{{ $formatVersion(version.game_versions) }}
</div>
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
@@ -1071,89 +1071,89 @@ import {
EyeIcon,
CheckIcon,
XIcon,
} from '@modrinth/assets'
import { Checkbox, Promotion, OverflowMenu, PopoutMenu } from '@modrinth/ui'
import { renderString, isRejected, isUnderReview, isStaff } from '@modrinth/utils'
import CrownIcon from '~/assets/images/utils/crown.svg?component'
import CalendarIcon from '~/assets/images/utils/calendar.svg?component'
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import UpdateIcon from '~/assets/images/utils/updated.svg?component'
import QueuedIcon from '~/assets/images/utils/list-end.svg?component'
import CodeIcon from '~/assets/images/sidebar/mod.svg?component'
import ExternalIcon from '~/assets/images/utils/external.svg?component'
import ReportIcon from '~/assets/images/utils/report.svg?component'
import HeartIcon from '~/assets/images/utils/heart.svg?component'
import IssuesIcon from '~/assets/images/utils/issues.svg?component'
import WikiIcon from '~/assets/images/utils/wiki.svg?component'
import DiscordIcon from '~/assets/images/external/discord.svg?component'
import BuyMeACoffeeLogo from '~/assets/images/external/bmac.svg?component'
import PatreonIcon from '~/assets/images/external/patreon.svg?component'
import KoFiIcon from '~/assets/images/external/kofi.svg?component'
import PayPalIcon from '~/assets/images/external/paypal.svg?component'
import OpenCollectiveIcon from '~/assets/images/external/opencollective.svg?component'
import UnknownIcon from '~/assets/images/utils/unknown-donation.svg?component'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
import BoxIcon from '~/assets/images/utils/box.svg?component'
import Badge from '~/components/ui/Badge.vue'
import Categories from '~/components/ui/search/Categories.vue'
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
import Modal from '~/components/ui/Modal.vue'
import NavRow from '~/components/ui/NavRow.vue'
import CopyCode from '~/components/ui/CopyCode.vue'
import Avatar from '~/components/ui/Avatar.vue'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import SettingsIcon from '~/assets/images/utils/settings.svg?component'
import UsersIcon from '~/assets/images/utils/users.svg?component'
import CategoriesIcon from '~/assets/images/utils/tags.svg?component'
import DescriptionIcon from '~/assets/images/utils/align-left.svg?component'
import LinksIcon from '~/assets/images/utils/link.svg?component'
import CopyrightIcon from '~/assets/images/utils/copyright.svg?component'
import LicenseIcon from '~/assets/images/utils/book-text.svg?component'
import GalleryIcon from '~/assets/images/utils/image.svg?component'
import VersionIcon from '~/assets/images/utils/version.svg?component'
import { reportProject } from '~/utils/report-helpers.ts'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import { userCollectProject } from '~/composables/user.js'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import OrganizationIcon from '~/assets/images/utils/organization.svg?component'
import ModerationChecklist from '~/components/ui/ModerationChecklist.vue'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg?component'
import { getVersionsToDisplay } from '~/helpers/projects.js'
} from "@modrinth/assets";
import { Checkbox, Promotion, OverflowMenu, PopoutMenu } from "@modrinth/ui";
import { renderString, isRejected, isUnderReview, isStaff } from "@modrinth/utils";
import CrownIcon from "~/assets/images/utils/crown.svg?component";
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import UpdateIcon from "~/assets/images/utils/updated.svg?component";
import QueuedIcon from "~/assets/images/utils/list-end.svg?component";
import CodeIcon from "~/assets/images/sidebar/mod.svg?component";
import ExternalIcon from "~/assets/images/utils/external.svg?component";
import ReportIcon from "~/assets/images/utils/report.svg?component";
import HeartIcon from "~/assets/images/utils/heart.svg?component";
import IssuesIcon from "~/assets/images/utils/issues.svg?component";
import WikiIcon from "~/assets/images/utils/wiki.svg?component";
import DiscordIcon from "~/assets/images/external/discord.svg?component";
import BuyMeACoffeeLogo from "~/assets/images/external/bmac.svg?component";
import PatreonIcon from "~/assets/images/external/patreon.svg?component";
import KoFiIcon from "~/assets/images/external/kofi.svg?component";
import PayPalIcon from "~/assets/images/external/paypal.svg?component";
import OpenCollectiveIcon from "~/assets/images/external/opencollective.svg?component";
import UnknownIcon from "~/assets/images/utils/unknown-donation.svg?component";
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
import BoxIcon from "~/assets/images/utils/box.svg?component";
import Badge from "~/components/ui/Badge.vue";
import Categories from "~/components/ui/search/Categories.vue";
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import Modal from "~/components/ui/Modal.vue";
import NavRow from "~/components/ui/NavRow.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Avatar from "~/components/ui/Avatar.vue";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
import MessageBanner from "~/components/ui/MessageBanner.vue";
import SettingsIcon from "~/assets/images/utils/settings.svg?component";
import UsersIcon from "~/assets/images/utils/users.svg?component";
import CategoriesIcon from "~/assets/images/utils/tags.svg?component";
import DescriptionIcon from "~/assets/images/utils/align-left.svg?component";
import LinksIcon from "~/assets/images/utils/link.svg?component";
import CopyrightIcon from "~/assets/images/utils/copyright.svg?component";
import LicenseIcon from "~/assets/images/utils/book-text.svg?component";
import GalleryIcon from "~/assets/images/utils/image.svg?component";
import VersionIcon from "~/assets/images/utils/version.svg?component";
import { reportProject } from "~/utils/report-helpers.ts";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import { userCollectProject } from "~/composables/user.js";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
import ModeratorIcon from "~/assets/images/sidebar/admin.svg?component";
import { getVersionsToDisplay } from "~/helpers/projects.js";
const data = useNuxtApp()
const route = useNativeRoute()
const config = useRuntimeConfig()
const data = useNuxtApp();
const route = useNativeRoute();
const config = useRuntimeConfig();
const auth = await useAuth()
const user = await useUser()
const cosmetics = useCosmetics()
const tags = useTags()
const flags = useFeatureFlags()
const auth = await useAuth();
const user = await useUser();
const cosmetics = useCosmetics();
const tags = useTags();
const flags = useFeatureFlags();
const displayCollectionsSearch = ref('')
const displayCollectionsSearch = ref("");
const collections = computed(() =>
user.value && user.value.collections
? user.value.collections.filter((x) =>
x.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase())
x.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
)
: []
)
: [],
);
if (
!route.params.id ||
!(
tags.value.projectTypes.find((x) => x.id === route.params.type) ||
route.params.type === 'project'
route.params.type === "project"
)
) {
throw createError({
fatal: true,
statusCode: 404,
message: 'The page could not be found',
})
message: "The page could not be found",
});
}
let project,
@@ -1164,9 +1164,9 @@ let project,
featuredVersions,
versions,
organization,
resetOrganization
resetOrganization;
try {
;[
[
{ data: project, refresh: resetProject },
{ data: allMembers, refresh: resetMembers },
{ data: dependencies },
@@ -1177,15 +1177,15 @@ try {
useAsyncData(`project/${route.params.id}`, () => useBaseFetch(`project/${route.params.id}`), {
transform: (project) => {
if (project) {
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type));
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.loaders,
tags.value
)
tags.value,
);
}
return project
return project;
},
}),
useAsyncData(
@@ -1194,89 +1194,89 @@ try {
{
transform: (members) => {
members.forEach((it, index) => {
members[index].avatar_url = it.user.avatar_url
members[index].name = it.user.username
})
members[index].avatar_url = it.user.avatar_url;
members[index].name = it.user.username;
});
return members
return members;
},
}
},
),
useAsyncData(`project/${route.params.id}/dependencies`, () =>
useBaseFetch(`project/${route.params.id}/dependencies`)
useBaseFetch(`project/${route.params.id}/dependencies`),
),
useAsyncData(`project/${route.params.id}/version?featured=true`, () =>
useBaseFetch(`project/${route.params.id}/version?featured=true`)
useBaseFetch(`project/${route.params.id}/version?featured=true`),
),
useAsyncData(`project/${route.params.id}/version`, () =>
useBaseFetch(`project/${route.params.id}/version`)
useBaseFetch(`project/${route.params.id}/version`),
),
useAsyncData(`project/${route.params.id}/organization`, () =>
useBaseFetch(`project/${route.params.id}/organization`, { apiVersion: 3 })
useBaseFetch(`project/${route.params.id}/organization`, { apiVersion: 3 }),
),
])
]);
versions = shallowRef(toRaw(versions))
featuredVersions = shallowRef(toRaw(featuredVersions))
versions = shallowRef(toRaw(versions));
featuredVersions = shallowRef(toRaw(featuredVersions));
} catch (error) {
throw createError({
fatal: true,
statusCode: 404,
message: 'Project not found',
})
message: "Project not found",
});
}
if (!project.value) {
throw createError({
fatal: true,
statusCode: 404,
message: 'Project not found',
})
message: "Project not found",
});
}
if (project.value.project_type !== route.params.type || route.params.id !== project.value.slug) {
let path = route.fullPath.split('/')
path.splice(0, 3)
path = path.filter((x) => x)
let path = route.fullPath.split("/");
path.splice(0, 3);
path = path.filter((x) => x);
await navigateTo(
`/${project.value.project_type}/${project.value.slug}${
path.length > 0 ? `/${path.join('/')}` : ''
path.length > 0 ? `/${path.join("/")}` : ""
}`,
{ redirectCode: 301, replace: true }
)
{ redirectCode: 301, replace: true },
);
}
// Members should be an array of all members, without the accepted ones, and with the user with the Owner role at the start
// The rest of the members should be sorted by role, then by name
const members = computed(() => {
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
const acceptedMembers = allMembers.value.filter((x) => x.accepted);
const owner = acceptedMembers.find((x) =>
organization.value
? organization.value.members.some(
(orgMember) => orgMember.user.id === x.user.id && orgMember.is_owner
(orgMember) => orgMember.user.id === x.user.id && orgMember.is_owner,
)
: x.is_owner
)
: x.is_owner,
);
const rest = acceptedMembers.filter((x) => !owner || x.user.id !== owner.user.id) || []
const rest = acceptedMembers.filter((x) => !owner || x.user.id !== owner.user.id) || [];
rest.sort((a, b) => {
if (a.role === b.role) {
return a.user.username.localeCompare(b.user.username)
return a.user.username.localeCompare(b.user.username);
} else {
return a.role.localeCompare(b.role)
return a.role.localeCompare(b.role);
}
})
});
return owner ? [owner, ...rest] : rest
})
return owner ? [owner, ...rest] : rest;
});
const currentMember = computed(() => {
let val = auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null
let val = auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null;
if (!val && auth.value.user && organization.value && organization.value.members) {
val = organization.value.members.find((x) => x.user.id === auth.value.user.id)
val = organization.value.members.find((x) => x.user.id === auth.value.user.id);
}
if (!val && auth.value.user && tags.value.staffRoles.includes(auth.value.user.role)) {
@@ -1284,195 +1284,195 @@ const currentMember = computed(() => {
team_id: project.team_id,
user: auth.value.user,
role: auth.value.role,
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
permissions: auth.value.user.role === "admin" ? 1023 : 12,
accepted: true,
payouts_split: 0,
avatar_url: auth.value.user.avatar_url,
name: auth.value.user.username,
}
};
}
return val
})
return val;
});
versions.value = data.$computeVersions(versions.value, allMembers.value)
versions.value = data.$computeVersions(versions.value, allMembers.value);
// Q: Why do this instead of computing the versions of featuredVersions?
// A: It will incorrectly generate the version slugs because it doesn't have the full context of
// all the versions. For example, if version 1.1.0 for Forge is featured but 1.1.0 for Fabric
// is not, but the Fabric one was uploaded first, the Forge version would link to the Fabric
/// version
const featuredIds = featuredVersions.value.map((x) => x.id)
featuredVersions.value = versions.value.filter((version) => featuredIds.includes(version.id))
const featuredIds = featuredVersions.value.map((x) => x.id);
featuredVersions.value = versions.value.filter((version) => featuredIds.includes(version.id));
featuredVersions.value.sort((a, b) => {
const aLatest = a.game_versions[a.game_versions.length - 1]
const bLatest = b.game_versions[b.game_versions.length - 1]
const gameVersions = tags.value.gameVersions.map((e) => e.version)
return gameVersions.indexOf(aLatest) - gameVersions.indexOf(bLatest)
})
const aLatest = a.game_versions[a.game_versions.length - 1];
const bLatest = b.game_versions[b.game_versions.length - 1];
const gameVersions = tags.value.gameVersions.map((e) => e.version);
return gameVersions.indexOf(aLatest) - gameVersions.indexOf(bLatest);
});
const licenseIdDisplay = computed(() => {
const id = project.value.license.id
const id = project.value.license.id;
if (id === 'LicenseRef-All-Rights-Reserved') {
return 'ARR'
} else if (id.includes('LicenseRef')) {
return id.replaceAll('LicenseRef-', '').replaceAll('-', ' ')
if (id === "LicenseRef-All-Rights-Reserved") {
return "ARR";
} else if (id.includes("LicenseRef")) {
return id.replaceAll("LicenseRef-", "").replaceAll("-", " ");
} else {
return id
return id;
}
})
const featuredGalleryImage = computed(() => project.value.gallery.find((img) => img.featured))
});
const featuredGalleryImage = computed(() => project.value.gallery.find((img) => img.featured));
const projectTypeDisplay = computed(() =>
data.$formatProjectType(
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders)
)
)
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
),
);
const title = computed(() => `${project.value.title} - Minecraft ${projectTypeDisplay.value}`)
const title = computed(() => `${project.value.title} - Minecraft ${projectTypeDisplay.value}`);
const description = computed(
() =>
`${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${
project.value.title
} by ${members.value.find((x) => x.is_owner)?.user?.username || 'a Creator'} on Modrinth`
)
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
);
if (!route.name.startsWith('type-id-settings')) {
if (!route.name.startsWith("type-id-settings")) {
useSeoMeta({
title: () => title.value,
description: () => description.value,
ogTitle: () => title.value,
ogDescription: () => project.value.description,
ogImage: () => project.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
ogImage: () => project.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
robots: () =>
project.value.status === 'approved' || project.value.status === 'archived'
? 'all'
: 'noindex',
})
project.value.status === "approved" || project.value.status === "archived"
? "all"
: "noindex",
});
}
const onUserCollectProject = useClientTry(userCollectProject)
const onUserCollectProject = useClientTry(userCollectProject);
async function setProcessing() {
startLoading()
startLoading();
try {
await useBaseFetch(`project/${project.value.id}`, {
method: 'PATCH',
method: "PATCH",
body: {
status: 'processing',
status: "processing",
},
})
});
project.value.status = 'processing'
project.value.status = "processing";
} 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 modalLicense = ref(null)
const licenseText = ref('')
const modalLicense = ref(null);
const licenseText = ref("");
async function getLicenseData() {
try {
const text = await useBaseFetch(`tag/license/${project.value.license.id}`)
licenseText.value = text.body || 'License text could not be retrieved.'
const text = await useBaseFetch(`tag/license/${project.value.license.id}`);
licenseText.value = text.body || "License text could not be retrieved.";
} catch {
licenseText.value = 'License text could not be retrieved.'
licenseText.value = "License text could not be retrieved.";
}
modalLicense.value.show()
modalLicense.value.show();
}
async function patchProject(resData, quiet = false) {
let result = false
startLoading()
let result = false;
startLoading();
try {
await useBaseFetch(`project/${project.value.id}`, {
method: 'PATCH',
method: "PATCH",
body: resData,
})
});
for (const key in resData) {
project.value[key] = resData[key]
project.value[key] = resData[key];
}
if (resData.license_id) {
project.value.license.id = resData.license_id
project.value.license.id = resData.license_id;
}
if (resData.license_url) {
project.value.license.url = resData.license_url
project.value.license.url = resData.license_url;
}
result = true
result = true;
if (!quiet) {
data.$notify({
group: 'main',
title: 'Project updated',
text: 'Your project has been updated.',
type: 'success',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
group: "main",
title: "Project updated",
text: "Your project has been updated.",
type: "success",
});
window.scrollTo({ top: 0, behavior: "smooth" });
}
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
type: "error",
});
window.scrollTo({ top: 0, behavior: "smooth" });
}
stopLoading()
stopLoading();
return result
return result;
}
async function patchIcon(icon) {
let result = false
startLoading()
let result = false;
startLoading();
try {
await useBaseFetch(
`project/${project.value.id}/icon?ext=${
icon.type.split('/')[icon.type.split('/').length - 1]
icon.type.split("/")[icon.type.split("/").length - 1]
}`,
{
method: 'PATCH',
method: "PATCH",
body: icon,
}
)
await resetProject()
result = true
},
);
await resetProject();
result = true;
data.$notify({
group: 'main',
title: 'Project icon updated',
group: "main",
title: "Project icon updated",
text: "Your project's icon has been updated.",
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",
});
window.scrollTo({ top: 0, behavior: 'smooth' })
window.scrollTo({ top: 0, behavior: "smooth" });
}
stopLoading()
return result
stopLoading();
return result;
}
async function updateMembers() {
@@ -1482,32 +1482,32 @@ async function updateMembers() {
{
transform: (members) => {
members.forEach((it, index) => {
members[index].avatar_url = it.user.avatar_url
members[index].name = it.user.username
})
members[index].avatar_url = it.user.avatar_url;
members[index].name = it.user.username;
});
return members
return members;
},
}
)
},
);
}
async function copyId() {
await navigator.clipboard.writeText(project.value.id)
await navigator.clipboard.writeText(project.value.id);
}
const collapsedChecklist = ref(false)
const collapsedChecklist = ref(false);
const showModerationChecklist = ref(false)
const futureProjects = ref([])
const showModerationChecklist = ref(false);
const futureProjects = ref([]);
if (process.client && history && history.state && history.state.showChecklist) {
showModerationChecklist.value = true
futureProjects.value = history.state.projects
showModerationChecklist.value = true;
futureProjects.value = history.state.projects;
}
const showFeaturedVersions = computed(
() => !flags.value.removeFeaturedVersions && featuredVersions.value.length > 0
)
() => !flags.value.removeFeaturedVersions && featuredVersions.value.length > 0,
);
</script>
<style lang="scss" scoped>
.header {
@@ -1599,7 +1599,9 @@ const showFeaturedVersions = computed(
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
margin-left: -4px;
z-index: 1;
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
box-shadow:
-2px -2px 0 2px var(--color-raised-bg),
2px -2px 0 2px var(--color-raised-bg);
}
}
@@ -1706,7 +1708,7 @@ const showFeaturedVersions = computed(
}
&:not(:last-child)::after {
content: '•';
content: "•";
margin: 0 0.25rem;
}
}

View File

@@ -37,7 +37,7 @@
</span>
<span>
on
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
{{ $dayjs(version.date_published).format("MMM D, YYYY") }}</span
>
</div>
<a
@@ -67,73 +67,73 @@
</div>
</template>
<script setup>
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import { renderHighlightedString } from '~/helpers/highlight.js'
import VersionFilterControl from '~/components/ui/VersionFilterControl.vue'
import Pagination from '~/components/ui/Pagination.vue'
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import { renderHighlightedString } from "~/helpers/highlight.js";
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
import Pagination from "~/components/ui/Pagination.vue";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
versions: {
type: Array,
default() {
return []
return [];
},
},
members: {
type: Array,
default() {
return []
return [];
},
},
})
});
const title = `${props.project.title} - Changelog`
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`
const title = `${props.project.title} - Changelog`;
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`;
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
})
});
const router = useNativeRouter()
const route = useNativeRoute()
const router = useNativeRouter();
const route = useNativeRoute();
const currentPage = ref(Number(route.query.p ?? 1))
const currentPage = ref(Number(route.query.p ?? 1));
const filteredVersions = computed(() => {
const selectedGameVersions = getArrayOrString(route.query.g) ?? []
const selectedLoaders = getArrayOrString(route.query.l) ?? []
const selectedVersionTypes = getArrayOrString(route.query.c) ?? []
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
return props.versions.filter(
(projectVersion) =>
(selectedGameVersions.length === 0 ||
selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion)
projectVersion.game_versions.includes(gameVersion),
)) &&
(selectedLoaders.length === 0 ||
selectedLoaders.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionTypes.length === 0 ||
selectedVersionTypes.includes(projectVersion.version_type))
)
})
selectedVersionTypes.includes(projectVersion.version_type)),
);
});
function switchPage(page) {
currentPage.value = page
currentPage.value = page;
router.replace({
query: {
...route.query,
p: currentPage.value !== 1 ? currentPage.value : undefined,
},
})
});
}
</script>
@@ -181,7 +181,7 @@ function switchPage(page) {
background-color: var(--color);
&:before {
content: '';
content: "";
width: 1rem;
height: 1rem;
position: absolute;

View File

@@ -9,7 +9,7 @@
<div class="gallery-file-input">
<div class="file-header">
<ImageIcon />
<strong>{{ editFile ? editFile.name : 'Current image' }}</strong>
<strong>{{ editFile ? editFile.name : "Current image" }}</strong>
<FileInput
v-if="editIndex === -1"
class="iconified-button raised-button"
@@ -19,8 +19,8 @@
should-always-reset
@change="
(x) => {
editFile = x[0]
showPreviewImage()
editFile = x[0];
showPreviewImage();
}
"
>
@@ -32,8 +32,8 @@
previewImage
? previewImage
: project.gallery[editIndex] && project.gallery[editIndex].url
? project.gallery[editIndex].url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
? project.gallery[editIndex].url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
alt="gallery-preview"
/>
@@ -235,20 +235,20 @@
<div class="gallery-bottom">
<div class="gallery-created">
<CalendarIcon />
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
{{ $dayjs(item.created).format("MMMM D, YYYY") }}
</div>
<div v-if="currentMember" class="gallery-buttons input-group">
<button
class="iconified-button"
@click="
() => {
resetEdit()
editIndex = index
editTitle = item.title
editDescription = item.description
editFeatured = item.featured
editOrder = item.ordering
$refs.modal_edit_item.show()
resetEdit();
editIndex = index;
editTitle = item.title;
editDescription = item.description;
editFeatured = item.featured;
editOrder = item.ordering;
$refs.modal_edit_item.show();
}
"
>
@@ -259,8 +259,8 @@
class="iconified-button"
@click="
() => {
deleteIndex = index
$refs.modal_confirm.show()
deleteIndex = index;
$refs.modal_confirm.show();
}
"
>
@@ -292,25 +292,25 @@ import {
InfoIcon,
ImageIcon,
TransferIcon,
} from '@modrinth/assets'
import { ConfirmModal } from '@modrinth/ui'
import FileInput from '~/components/ui/FileInput.vue'
import DropArea from '~/components/ui/DropArea.vue'
import Modal from '~/components/ui/Modal.vue'
} from "@modrinth/assets";
import { ConfirmModal } from "@modrinth/ui";
import FileInput from "~/components/ui/FileInput.vue";
import DropArea from "~/components/ui/DropArea.vue";
import Modal from "~/components/ui/Modal.vue";
import { isPermission } from '~/utils/permissions.ts'
import { isPermission } from "~/utils/permissions.ts";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
resetProject: {
@@ -318,17 +318,17 @@ const props = defineProps({
required: true,
default: () => {},
},
})
});
const title = `${props.project.title} - Gallery`
const description = `View ${props.project.gallery.length} images of ${props.project.title} on Modrinth.`
const title = `${props.project.title} - Gallery`;
const description = `View ${props.project.gallery.length} images of ${props.project.title} on Modrinth.`;
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
})
});
</script>
<script>
@@ -342,185 +342,185 @@ export default defineNuxtComponent({
deleteIndex: -1,
editIndex: -1,
editTitle: '',
editDescription: '',
editTitle: "",
editDescription: "",
editFeatured: false,
editOrder: null,
editFile: null,
previewImage: null,
shouldPreventActions: false,
}
};
},
computed: {
acceptFileTypes() {
return 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
return "image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp";
},
},
mounted() {
this._keyListener = function (e) {
if (this.expandedGalleryItem) {
e.preventDefault()
if (e.key === 'Escape') {
this.expandedGalleryItem = null
} else if (e.key === 'ArrowLeft') {
this.previousImage()
} else if (e.key === 'ArrowRight') {
this.nextImage()
e.preventDefault();
if (e.key === "Escape") {
this.expandedGalleryItem = null;
} else if (e.key === "ArrowLeft") {
this.previousImage();
} else if (e.key === "ArrowRight") {
this.nextImage();
}
}
}
};
document.addEventListener('keydown', this._keyListener.bind(this))
document.addEventListener("keydown", this._keyListener.bind(this));
},
methods: {
nextImage() {
this.expandedGalleryIndex++
this.expandedGalleryIndex++;
if (this.expandedGalleryIndex >= this.project.gallery.length) {
this.expandedGalleryIndex = 0
this.expandedGalleryIndex = 0;
}
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex];
},
previousImage() {
this.expandedGalleryIndex--
this.expandedGalleryIndex--;
if (this.expandedGalleryIndex < 0) {
this.expandedGalleryIndex = this.project.gallery.length - 1
this.expandedGalleryIndex = this.project.gallery.length - 1;
}
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex];
},
expandImage(item, index) {
this.expandedGalleryItem = item
this.expandedGalleryIndex = index
this.zoomedIn = false
this.expandedGalleryItem = item;
this.expandedGalleryIndex = index;
this.zoomedIn = false;
},
resetEdit() {
this.editIndex = -1
this.editTitle = ''
this.editDescription = ''
this.editFeatured = false
this.editOrder = null
this.editFile = null
this.previewImage = null
this.editIndex = -1;
this.editTitle = "";
this.editDescription = "";
this.editFeatured = false;
this.editOrder = null;
this.editFile = null;
this.previewImage = null;
},
handleFiles(files) {
this.resetEdit()
this.editFile = files[0]
this.resetEdit();
this.editFile = files[0];
this.showPreviewImage()
this.$refs.modal_edit_item.show()
this.showPreviewImage();
this.$refs.modal_edit_item.show();
},
showPreviewImage() {
const reader = new FileReader()
const reader = new FileReader();
if (this.editFile instanceof Blob) {
reader.readAsDataURL(this.editFile)
reader.readAsDataURL(this.editFile);
reader.onload = (event) => {
this.previewImage = event.target.result
}
this.previewImage = event.target.result;
};
}
},
async createGalleryItem() {
this.shouldPreventActions = true
startLoading()
this.shouldPreventActions = true;
startLoading();
try {
let url = `project/${this.project.id}/gallery?ext=${
this.editFile
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
? this.editFile.type.split("/")[this.editFile.type.split("/").length - 1]
: null
}&featured=${this.editFeatured}`
}&featured=${this.editFeatured}`;
if (this.editTitle) {
url += `&title=${encodeURIComponent(this.editTitle)}`
url += `&title=${encodeURIComponent(this.editTitle)}`;
}
if (this.editDescription) {
url += `&description=${encodeURIComponent(this.editDescription)}`
url += `&description=${encodeURIComponent(this.editDescription)}`;
}
if (this.editOrder) {
url += `&ordering=${this.editOrder}`
url += `&ordering=${this.editOrder}`;
}
await useBaseFetch(url, {
method: 'POST',
method: "POST",
body: this.editFile,
})
await this.resetProject()
});
await this.resetProject();
this.$refs.modal_edit_item.hide()
this.$refs.modal_edit_item.hide();
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
this.shouldPreventActions = false
stopLoading();
this.shouldPreventActions = false;
},
async editGalleryItem() {
this.shouldPreventActions = true
startLoading()
this.shouldPreventActions = true;
startLoading();
try {
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
this.project.gallery[this.editIndex].url
)}&featured=${this.editFeatured}`
this.project.gallery[this.editIndex].url,
)}&featured=${this.editFeatured}`;
if (this.editTitle) {
url += `&title=${encodeURIComponent(this.editTitle)}`
url += `&title=${encodeURIComponent(this.editTitle)}`;
}
if (this.editDescription) {
url += `&description=${encodeURIComponent(this.editDescription)}`
url += `&description=${encodeURIComponent(this.editDescription)}`;
}
if (this.editOrder) {
url += `&ordering=${this.editOrder}`
url += `&ordering=${this.editOrder}`;
}
await useBaseFetch(url, {
method: 'PATCH',
})
method: "PATCH",
});
await this.resetProject()
this.$refs.modal_edit_item.hide()
await this.resetProject();
this.$refs.modal_edit_item.hide();
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
this.shouldPreventActions = false
stopLoading();
this.shouldPreventActions = false;
},
async deleteGalleryImage() {
startLoading()
startLoading();
try {
await useBaseFetch(
`project/${this.project.id}/gallery?url=${encodeURIComponent(
this.project.gallery[this.deleteIndex].url
this.project.gallery[this.deleteIndex].url,
)}`,
{
method: 'DELETE',
}
)
method: "DELETE",
},
);
await this.resetProject()
await this.resetProject();
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
},
},
})
});
</script>
<style lang="scss" scoped>
@@ -637,7 +637,9 @@ export default defineNuxtComponent({
display: flex;
flex-direction: column;
max-width: 40rem;
transition: opacity 0.25s ease-in-out, transform 0.25s ease-in-out;
transition:
opacity 0.25s ease-in-out,
transform 0.25s ease-in-out;
text-shadow: 1px 1px 10px #000000d4;
margin-bottom: 0.25rem;
gap: 0.5rem;
@@ -658,7 +660,9 @@ export default defineNuxtComponent({
background-color: var(--color-raised-bg);
padding: var(--spacing-card-md);
border-radius: var(--size-rounded-card);
transition: opacity 0.25s ease-in-out, transform 0.25s ease-in-out;
transition:
opacity 0.25s ease-in-out,
transform 0.25s ease-in-out;
}
}
}

View File

@@ -7,17 +7,17 @@
</template>
<script>
import { renderHighlightedString } from '~/helpers/highlight.js'
import { renderHighlightedString } from "~/helpers/highlight.js";
export default defineNuxtComponent({
props: {
project: {
type: Object,
default() {
return {}
return {};
},
},
},
methods: { renderHighlightedString },
})
});
</script>

View File

@@ -92,9 +92,9 @@
</div>
</template>
<script setup>
import { ExitIcon, CheckIcon, IssuesIcon } from '@modrinth/assets'
import { Badge } from '@modrinth/ui'
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
import { ExitIcon, CheckIcon, IssuesIcon } from "@modrinth/assets";
import { Badge } from "@modrinth/ui";
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import {
getProjectLink,
isApproved,
@@ -102,19 +102,19 @@ import {
isPrivate,
isRejected,
isUnderReview,
} from '~/helpers/projects.js'
} from "~/helpers/projects.js";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
resetProject: {
@@ -122,39 +122,39 @@ const props = defineProps({
required: true,
default: () => {},
},
})
});
const app = useNuxtApp()
const auth = await useAuth()
const app = useNuxtApp();
const auth = await useAuth();
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
useBaseFetch(`thread/${props.project.thread_id}`)
)
useBaseFetch(`thread/${props.project.thread_id}`),
);
async function setStatus(status) {
startLoading()
startLoading();
try {
const data = {}
data.status = status
const data = {};
data.status = status;
await useBaseFetch(`project/${props.project.id}`, {
method: 'PATCH',
method: "PATCH",
body: data,
})
});
const project = props.project
project.status = status
await props.resetProject()
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
const project = props.project;
project.status = status;
await props.resetProject();
thread.value = await useBaseFetch(`thread/${thread.value.id}`);
} catch (err) {
app.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -15,16 +15,16 @@
</template>
<script setup>
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
import ChartDisplay from "~/components/ui/charts/ChartDisplay.vue";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
})
});
</script>
<style scoped lang="scss">

View File

@@ -34,11 +34,11 @@
</template>
<script>
import { MarkdownEditor } from '@modrinth/ui'
import Chips from '~/components/ui/Chips.vue'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import { renderHighlightedString } from '~/helpers/highlight.js'
import { useImageUpload } from '~/composables/image-upload.ts'
import { MarkdownEditor } from "@modrinth/ui";
import Chips from "~/components/ui/Chips.vue";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { useImageUpload } from "~/composables/image-upload.ts";
export default defineNuxtComponent({
components: {
@@ -50,19 +50,19 @@ export default defineNuxtComponent({
project: {
type: Object,
default() {
return {}
return {};
},
},
allMembers: {
type: Array,
default() {
return []
return [];
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
patchProject: {
@@ -70,54 +70,54 @@ export default defineNuxtComponent({
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Patch project function not found',
type: 'error',
})
}
group: "main",
title: "An error occurred",
text: "Patch project function not found",
type: "error",
});
};
},
},
},
data() {
return {
description: this.project.body,
bodyViewMode: 'source',
}
bodyViewMode: "source",
};
},
computed: {
patchData() {
const data = {}
const data = {};
if (this.description !== this.project.body) {
data.body = this.description
data.body = this.description;
}
return data
return data;
},
hasChanges() {
return Object.keys(this.patchData).length > 0
return Object.keys(this.patchData).length > 0;
},
},
created() {
this.EDIT_BODY = 1 << 3
this.EDIT_BODY = 1 << 3;
},
methods: {
renderHighlightedString,
saveChanges() {
if (this.hasChanges) {
this.patchProject(this.patchData)
this.patchProject(this.patchData);
}
},
async onUploadHandler(file) {
const response = await useImageUpload(file, {
context: 'project',
context: "project",
projectID: this.project.id,
})
return response.url
});
return response.url;
},
},
})
});
</script>
<style scoped>

View File

@@ -163,7 +163,7 @@
class="good"
/>
<ExitIcon v-else class="bad" />
{{ hasModifiedVisibility() ? 'Will be v' : 'V' }}isible in search
{{ hasModifiedVisibility() ? "Will be v" : "V" }}isible in search
</li>
<li>
<ExitIcon
@@ -171,7 +171,7 @@
class="bad"
/>
<CheckIcon v-else class="good" />
{{ hasModifiedVisibility() ? 'Will be v' : 'V' }}isible on profile
{{ hasModifiedVisibility() ? "Will be v" : "V" }}isible on profile
</li>
<li>
<CheckIcon v-if="visibility !== 'private'" class="good" />
@@ -185,7 +185,7 @@
}"
class="warn"
/>
{{ hasModifiedVisibility() ? 'Will be v' : 'V' }}isible via URL
{{ hasModifiedVisibility() ? "Will be v" : "V" }}isible via URL
</li>
</ul>
</div>
@@ -241,18 +241,18 @@
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Multiselect } from "vue-multiselect";
import Avatar from '~/components/ui/Avatar.vue'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import FileInput from '~/components/ui/FileInput.vue'
import Avatar from "~/components/ui/Avatar.vue";
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import FileInput from "~/components/ui/FileInput.vue";
import UploadIcon from '~/assets/images/utils/upload.svg?component'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import TrashIcon from '~/assets/images/utils/trash.svg?component'
import ExitIcon from '~/assets/images/utils/x.svg?component'
import IssuesIcon from '~/assets/images/utils/issues.svg?component'
import CheckIcon from '~/assets/images/utils/check.svg?component'
import UploadIcon from "~/assets/images/utils/upload.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import TrashIcon from "~/assets/images/utils/trash.svg?component";
import ExitIcon from "~/assets/images/utils/x.svg?component";
import IssuesIcon from "~/assets/images/utils/issues.svg?component";
import CheckIcon from "~/assets/images/utils/check.svg?component";
const props = defineProps({
project: {
@@ -280,134 +280,134 @@ const props = defineProps({
required: true,
default: () => {},
},
})
});
const tags = useTags()
const router = useNativeRouter()
const tags = useTags();
const router = useNativeRouter();
const name = ref(props.project.title)
const slug = ref(props.project.slug)
const summary = ref(props.project.description)
const icon = ref(null)
const previewImage = ref(null)
const clientSide = ref(props.project.client_side)
const serverSide = ref(props.project.server_side)
const deletedIcon = ref(false)
const name = ref(props.project.title);
const slug = ref(props.project.slug);
const summary = ref(props.project.description);
const icon = ref(null);
const previewImage = ref(null);
const clientSide = ref(props.project.client_side);
const serverSide = ref(props.project.server_side);
const deletedIcon = ref(false);
const visibility = ref(
tags.value.approvedStatuses.includes(props.project.status)
? props.project.status
: props.project.requested_status
)
: props.project.requested_status,
);
const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2
return (props.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
})
const EDIT_DETAILS = 1 << 2;
return (props.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS;
});
const hasDeletePermission = computed(() => {
const DELETE_PROJECT = 1 << 7
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
})
const DELETE_PROJECT = 1 << 7;
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
});
const sideTypes = ['required', 'optional', 'unsupported']
const sideTypes = ["required", "optional", "unsupported"];
const patchData = computed(() => {
const data = {}
const data = {};
if (name.value !== props.project.title) {
data.title = name.value.trim()
data.title = name.value.trim();
}
if (slug.value !== props.project.slug) {
data.slug = slug.value.trim()
data.slug = slug.value.trim();
}
if (summary.value !== props.project.description) {
data.description = summary.value.trim()
data.description = summary.value.trim();
}
if (clientSide.value !== props.project.client_side) {
data.client_side = clientSide.value
data.client_side = clientSide.value;
}
if (serverSide.value !== props.project.server_side) {
data.server_side = serverSide.value
data.server_side = serverSide.value;
}
if (tags.value.approvedStatuses.includes(props.project.status)) {
if (visibility.value !== props.project.status) {
data.status = visibility.value
data.status = visibility.value;
}
} else if (visibility.value !== props.project.requested_status) {
data.requested_status = visibility.value
data.requested_status = visibility.value;
}
return data
})
return data;
});
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value
})
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value;
});
const hasModifiedVisibility = () => {
const originalVisibility = tags.value.approvedStatuses.includes(props.project.status)
? props.project.status
: props.project.requested_status
: props.project.requested_status;
return originalVisibility !== visibility.value
}
return originalVisibility !== visibility.value;
};
const saveChanges = async () => {
if (hasChanges.value) {
await props.patchProject(patchData.value)
await props.patchProject(patchData.value);
}
if (deletedIcon.value) {
await deleteIcon()
deletedIcon.value = false
await deleteIcon();
deletedIcon.value = false;
} else if (icon.value) {
await props.patchIcon(icon.value)
icon.value = null
await props.patchIcon(icon.value);
icon.value = null;
}
}
};
const showPreviewImage = (files) => {
const reader = new FileReader()
icon.value = files[0]
deletedIcon.value = false
reader.readAsDataURL(icon.value)
const reader = new FileReader();
icon.value = files[0];
deletedIcon.value = false;
reader.readAsDataURL(icon.value);
reader.onload = (event) => {
previewImage.value = event.target.result
}
}
previewImage.value = event.target.result;
};
};
const deleteProject = async () => {
await useBaseFetch(`project/${props.project.id}`, {
method: 'DELETE',
})
await initUserProjects()
await router.push('/dashboard/projects')
method: "DELETE",
});
await initUserProjects();
await router.push("/dashboard/projects");
addNotification({
group: 'main',
title: 'Project deleted',
text: 'Your project has been deleted.',
type: 'success',
})
}
group: "main",
title: "Project deleted",
text: "Your project has been deleted.",
type: "success",
});
};
const markIconForDeletion = () => {
deletedIcon.value = true
icon.value = null
previewImage.value = null
}
deletedIcon.value = true;
icon.value = null;
previewImage.value = null;
};
const deleteIcon = async () => {
await useBaseFetch(`project/${props.project.id}/icon`, {
method: 'DELETE',
})
await props.resetProject()
method: "DELETE",
});
await props.resetProject();
addNotification({
group: 'main',
title: 'Project icon removed',
group: "main",
title: "Project icon removed",
text: "Your project's icon has been removed.",
type: 'success',
})
}
type: "success",
});
};
</script>
<style lang="scss" scoped>
.visibility-info {

View File

@@ -100,9 +100,9 @@
</template>
<script>
import Multiselect from 'vue-multiselect'
import Checkbox from '~/components/ui/Checkbox'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import Multiselect from "vue-multiselect";
import Checkbox from "~/components/ui/Checkbox";
import SaveIcon from "~/assets/images/utils/save.svg?component";
export default defineNuxtComponent({
components: {
@@ -114,13 +114,13 @@ export default defineNuxtComponent({
project: {
type: Object,
default() {
return {}
return {};
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
patchProject: {
@@ -128,170 +128,170 @@ export default defineNuxtComponent({
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Patch project function not found',
type: 'error',
})
}
group: "main",
title: "An error occurred",
text: "Patch project function not found",
type: "error",
});
};
},
},
},
data() {
return {
licenseUrl: '',
license: { friendly: '', short: '', requiresOnlyOrLater: false },
allowOrLater: this.project.license.id.includes('-or-later'),
nonSpdxLicense: this.project.license.id.includes('LicenseRef-'),
licenseUrl: "",
license: { friendly: "", short: "", requiresOnlyOrLater: false },
allowOrLater: this.project.license.id.includes("-or-later"),
nonSpdxLicense: this.project.license.id.includes("LicenseRef-"),
showKnownErrors: false,
}
};
},
async setup(props) {
const defaultLicenses = shallowRef([
{ friendly: 'Custom', short: '' },
{ friendly: "Custom", short: "" },
{
friendly: 'All Rights Reserved/No License',
short: 'All-Rights-Reserved',
friendly: "All Rights Reserved/No License",
short: "All-Rights-Reserved",
},
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
{ friendly: "Apache License 2.0", short: "Apache-2.0" },
{
friendly: 'BSD 2-Clause "Simplified" License',
short: 'BSD-2-Clause',
short: "BSD-2-Clause",
},
{
friendly: 'BSD 3-Clause "New" or "Revised" License',
short: 'BSD-3-Clause',
short: "BSD-3-Clause",
},
{
friendly: 'CC Zero (Public Domain equivalent)',
short: 'CC0-1.0',
friendly: "CC Zero (Public Domain equivalent)",
short: "CC0-1.0",
},
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
{ friendly: "CC-BY 4.0", short: "CC-BY-4.0" },
{
friendly: 'CC-BY-SA 4.0',
short: 'CC-BY-SA-4.0',
friendly: "CC-BY-SA 4.0",
short: "CC-BY-SA-4.0",
},
{
friendly: 'CC-BY-NC 4.0',
short: 'CC-BY-NC-4.0',
friendly: "CC-BY-NC 4.0",
short: "CC-BY-NC-4.0",
},
{
friendly: 'CC-BY-NC-SA 4.0',
short: 'CC-BY-NC-SA-4.0',
friendly: "CC-BY-NC-SA 4.0",
short: "CC-BY-NC-SA-4.0",
},
{
friendly: 'CC-BY-ND 4.0',
short: 'CC-BY-ND-4.0',
friendly: "CC-BY-ND 4.0",
short: "CC-BY-ND-4.0",
},
{
friendly: 'CC-BY-NC-ND 4.0',
short: 'CC-BY-NC-ND-4.0',
friendly: "CC-BY-NC-ND 4.0",
short: "CC-BY-NC-ND-4.0",
},
{
friendly: 'GNU Affero General Public License v3',
short: 'AGPL-3.0',
friendly: "GNU Affero General Public License v3",
short: "AGPL-3.0",
requiresOnlyOrLater: true,
},
{
friendly: 'GNU Lesser General Public License v2.1',
short: 'LGPL-2.1',
friendly: "GNU Lesser General Public License v2.1",
short: "LGPL-2.1",
requiresOnlyOrLater: true,
},
{
friendly: 'GNU Lesser General Public License v3',
short: 'LGPL-3.0',
friendly: "GNU Lesser General Public License v3",
short: "LGPL-3.0",
requiresOnlyOrLater: true,
},
{
friendly: 'GNU General Public License v2',
short: 'GPL-2.0',
friendly: "GNU General Public License v2",
short: "GPL-2.0",
requiresOnlyOrLater: true,
},
{
friendly: 'GNU General Public License v3',
short: 'GPL-3.0',
friendly: "GNU General Public License v3",
short: "GPL-3.0",
requiresOnlyOrLater: true,
},
{ friendly: 'ISC License', short: 'ISC' },
{ friendly: 'MIT License', short: 'MIT' },
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
{ friendly: 'zlib License', short: 'Zlib' },
])
{ friendly: "ISC License", short: "ISC" },
{ friendly: "MIT License", short: "MIT" },
{ friendly: "Mozilla Public License 2.0", short: "MPL-2.0" },
{ friendly: "zlib License", short: "Zlib" },
]);
const licenseUrl = ref(props.project.license.url)
const licenseUrl = ref(props.project.license.url);
const licenseId = props.project.license.id
const licenseId = props.project.license.id;
const trimmedLicenseId = licenseId
.replaceAll('-only', '')
.replaceAll('-or-later', '')
.replaceAll('LicenseRef-', '')
.replaceAll("-only", "")
.replaceAll("-or-later", "")
.replaceAll("LicenseRef-", "");
const license = ref(
defaultLicenses.value.find((x) => x.short === trimmedLicenseId) ?? {
friendly: 'Custom',
short: licenseId.replaceAll('LicenseRef-', ''),
}
)
friendly: "Custom",
short: licenseId.replaceAll("LicenseRef-", ""),
},
);
if (licenseId === 'LicenseRef-Unknown') {
if (licenseId === "LicenseRef-Unknown") {
license.value = {
friendly: 'Unknown',
short: licenseId.replaceAll('LicenseRef-', ''),
}
friendly: "Unknown",
short: licenseId.replaceAll("LicenseRef-", ""),
};
}
return {
defaultLicenses,
licenseUrl,
license,
}
};
},
computed: {
hasPermission() {
const EDIT_DETAILS = 1 << 2
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
const EDIT_DETAILS = 1 << 2;
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS;
},
licenseId() {
let id = ''
if (this.license === null) return id
let id = "";
if (this.license === null) return id;
if (
(this.nonSpdxLicense && this.license.friendly === 'Custom') ||
this.license.short === 'All-Rights-Reserved' ||
this.license.short === 'Unknown'
(this.nonSpdxLicense && this.license.friendly === "Custom") ||
this.license.short === "All-Rights-Reserved" ||
this.license.short === "Unknown"
) {
id += 'LicenseRef-'
id += "LicenseRef-";
}
id += this.license.short
id += this.license.short;
if (this.license.requiresOnlyOrLater) {
id += this.allowOrLater ? '-or-later' : '-only'
id += this.allowOrLater ? "-or-later" : "-only";
}
if (this.nonSpdxLicense && this.license.friendly === 'Custom') {
id = id.replaceAll(' ', '-')
if (this.nonSpdxLicense && this.license.friendly === "Custom") {
id = id.replaceAll(" ", "-");
}
return id
return id;
},
patchData() {
const data = {}
const data = {};
if (this.licenseId !== this.project.license.id) {
data.license_id = this.licenseId
data.license_url = this.licenseUrl ? this.licenseUrl : null
data.license_id = this.licenseId;
data.license_url = this.licenseUrl ? this.licenseUrl : null;
} else if (this.licenseUrl !== this.project.license.url) {
data.license_url = this.licenseUrl ? this.licenseUrl : null
data.license_url = this.licenseUrl ? this.licenseUrl : null;
}
return data
return data;
},
hasChanges() {
return Object.keys(this.patchData).length > 0
return Object.keys(this.patchData).length > 0;
},
},
methods: {
saveChanges() {
if (this.hasChanges) {
this.patchProject(this.patchData)
this.patchProject(this.patchData);
}
},
},
})
});
</script>

View File

@@ -122,67 +122,67 @@
</template>
<script setup>
import { DropdownSelect } from '@modrinth/ui'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import { DropdownSelect } from "@modrinth/ui";
import SaveIcon from "~/assets/images/utils/save.svg?component";
const tags = useTags()
const tags = useTags();
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
patchProject: {
type: Function,
default() {
return () => {}
return () => {};
},
},
})
});
const issuesUrl = ref(props.project.issues_url)
const sourceUrl = ref(props.project.source_url)
const wikiUrl = ref(props.project.wiki_url)
const discordUrl = ref(props.project.discord_url)
const issuesUrl = ref(props.project.issues_url);
const sourceUrl = ref(props.project.source_url);
const wikiUrl = ref(props.project.wiki_url);
const discordUrl = ref(props.project.discord_url);
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls))
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
rawDonationLinks.push({
id: null,
platform: null,
url: null,
})
const donationLinks = ref(rawDonationLinks)
});
const donationLinks = ref(rawDonationLinks);
const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2
return (props.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
})
const EDIT_DETAILS = 1 << 2;
return (props.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS;
});
const patchData = computed(() => {
const data = {}
const data = {};
if (checkDifference(issuesUrl.value, props.project.issues_url)) {
data.issues_url = issuesUrl.value === '' ? null : issuesUrl.value.trim()
data.issues_url = issuesUrl.value === "" ? null : issuesUrl.value.trim();
}
if (checkDifference(sourceUrl.value, props.project.source_url)) {
data.source_url = sourceUrl.value === '' ? null : sourceUrl.value.trim()
data.source_url = sourceUrl.value === "" ? null : sourceUrl.value.trim();
}
if (checkDifference(wikiUrl.value, props.project.wiki_url)) {
data.wiki_url = wikiUrl.value === '' ? null : wikiUrl.value.trim()
data.wiki_url = wikiUrl.value === "" ? null : wikiUrl.value.trim();
}
if (checkDifference(discordUrl.value, props.project.discord_url)) {
data.discord_url = discordUrl.value === '' ? null : discordUrl.value.trim()
data.discord_url = discordUrl.value === "" ? null : discordUrl.value.trim();
}
const validDonationLinks = donationLinks.value.filter((link) => link.url && link.id)
const validDonationLinks = donationLinks.value.filter((link) => link.url && link.id);
if (
validDonationLinks !== props.project.donation_urls &&
@@ -192,69 +192,69 @@ const patchData = computed(() => {
validDonationLinks.length === 0
)
) {
data.donation_urls = validDonationLinks
data.donation_urls = validDonationLinks;
}
if (data.donation_urls) {
data.donation_urls.forEach((link) => {
const platform = tags.value.donationPlatforms.find((platform) => platform.short === link.id)
link.platform = platform.name
})
const platform = tags.value.donationPlatforms.find((platform) => platform.short === link.id);
link.platform = platform.name;
});
}
return data
})
return data;
});
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0
})
return Object.keys(patchData.value).length > 0;
});
async function saveChanges() {
if (patchData.value && (await props.patchProject(patchData.value))) {
donationLinks.value = JSON.parse(JSON.stringify(props.project.donation_urls))
donationLinks.value = JSON.parse(JSON.stringify(props.project.donation_urls));
donationLinks.value.push({
id: null,
platform: null,
url: null,
})
});
}
}
function updateDonationLinks() {
const links = donationLinks.value
const links = donationLinks.value;
links.forEach((link) => {
if (link.url) {
const url = link.url.toLowerCase()
if (url.includes('patreon.com')) {
link.id = 'patreon'
} else if (url.includes('ko-fi.com')) {
link.id = 'ko-fi'
} else if (url.includes('paypal.com') || url.includes('paypal.me')) {
link.id = 'paypal'
} else if (url.includes('buymeacoffee.com') || url.includes('buymeacoff.ee')) {
link.id = 'bmac'
} else if (url.includes('github.com/sponsors')) {
link.id = 'github'
const url = link.url.toLowerCase();
if (url.includes("patreon.com")) {
link.id = "patreon";
} else if (url.includes("ko-fi.com")) {
link.id = "ko-fi";
} else if (url.includes("paypal.com") || url.includes("paypal.me")) {
link.id = "paypal";
} else if (url.includes("buymeacoffee.com") || url.includes("buymeacoff.ee")) {
link.id = "bmac";
} else if (url.includes("github.com/sponsors")) {
link.id = "github";
}
}
})
});
if (!links.find((link) => !(link.url && link.id))) {
links.push({
id: null,
platform: null,
url: null,
})
});
}
donationLinks.value = links
donationLinks.value = links;
}
function checkDifference(newLink, existingLink) {
if (newLink === '' && existingLink !== null) {
return true
if (newLink === "" && existingLink !== null) {
return true;
}
if (!newLink && !existingLink) {
return false
return false;
}
return newLink !== existingLink
return newLink !== existingLink;
}
</script>
<style lang="scss" scoped>

View File

@@ -517,43 +517,43 @@
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import { TransferIcon, CheckIcon, UsersIcon } from '@modrinth/assets'
import { Avatar, Badge, Card, Checkbox } from '@modrinth/ui'
import { Multiselect } from "vue-multiselect";
import { TransferIcon, CheckIcon, UsersIcon } from "@modrinth/assets";
import { Avatar, Badge, Card, Checkbox } from "@modrinth/ui";
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import UserPlusIcon from '~/assets/images/utils/user-plus.svg?component'
import UserRemoveIcon from '~/assets/images/utils/user-x.svg?component'
import OrganizationIcon from '~/assets/images/utils/organization.svg?component'
import CrownIcon from '~/assets/images/utils/crown.svg?component'
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import UserPlusIcon from "~/assets/images/utils/user-plus.svg?component";
import UserRemoveIcon from "~/assets/images/utils/user-x.svg?component";
import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { removeSelfFromTeam } from '~/helpers/teams.js'
import { removeSelfFromTeam } from "~/helpers/teams.js";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
organization: {
type: Object,
default() {
return {}
return {};
},
},
allMembers: {
type: Array,
default() {
return []
return [];
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
resetProject: {
@@ -571,39 +571,39 @@ const props = defineProps({
required: true,
default: () => {},
},
})
});
const cosmetics = useCosmetics()
const auth = await useAuth()
const cosmetics = useCosmetics();
const auth = await useAuth();
const allTeamMembers = ref([])
const allOrgMembers = ref([])
const allTeamMembers = ref([]);
const allOrgMembers = ref([]);
const acceptedOrgMembers = computed(() => {
return props.organization?.members?.filter((x) => x.accepted) || []
})
return props.organization?.members?.filter((x) => x.accepted) || [];
});
function initMembers() {
const orgMembers = props.organization?.members || []
const orgMembers = props.organization?.members || [];
const selectedMembersForOrg = orgMembers.map((partialOrgMember) => {
const foundMember = props.allMembers.find((tM) => tM.user.id === partialOrgMember.user.id)
const returnVal = foundMember ?? partialOrgMember
const foundMember = props.allMembers.find((tM) => tM.user.id === partialOrgMember.user.id);
const returnVal = foundMember ?? partialOrgMember;
// If replacing a partial with a full member, we need to mark as such.
returnVal.override = !!foundMember
returnVal.oldOverride = !!foundMember
returnVal.override = !!foundMember;
returnVal.oldOverride = !!foundMember;
returnVal.is_owner = partialOrgMember.is_owner
returnVal.is_owner = partialOrgMember.is_owner;
return returnVal
})
return returnVal;
});
allOrgMembers.value = selectedMembersForOrg
allOrgMembers.value = selectedMembersForOrg;
allTeamMembers.value = props.allMembers.filter(
(x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id)
)
(x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id),
);
}
watch(
@@ -613,129 +613,129 @@ watch(
() => props.project,
() => props.currentMember,
],
initMembers
)
initMembers()
initMembers,
);
initMembers();
const currentUsername = ref('')
const openTeamMembers = ref([])
const selectedOrganization = ref(null)
const currentUsername = ref("");
const openTeamMembers = ref([]);
const selectedOrganization = ref(null);
const { data: organizations } = useAsyncData('organizations', () => {
return useBaseFetch('user/' + auth.value?.user.id + '/organizations', {
const { data: organizations } = useAsyncData("organizations", () => {
return useBaseFetch("user/" + auth.value?.user.id + "/organizations", {
apiVersion: 3,
})
})
});
});
const UPLOAD_VERSION = 1 << 0
const DELETE_VERSION = 1 << 1
const EDIT_DETAILS = 1 << 2
const EDIT_BODY = 1 << 3
const MANAGE_INVITES = 1 << 4
const REMOVE_MEMBER = 1 << 5
const EDIT_MEMBER = 1 << 6
const DELETE_PROJECT = 1 << 7
const VIEW_ANALYTICS = 1 << 8
const VIEW_PAYOUTS = 1 << 9
const UPLOAD_VERSION = 1 << 0;
const DELETE_VERSION = 1 << 1;
const EDIT_DETAILS = 1 << 2;
const EDIT_BODY = 1 << 3;
const MANAGE_INVITES = 1 << 4;
const REMOVE_MEMBER = 1 << 5;
const EDIT_MEMBER = 1 << 6;
const DELETE_PROJECT = 1 << 7;
const VIEW_ANALYTICS = 1 << 8;
const VIEW_PAYOUTS = 1 << 9;
const onAddToOrg = useClientTry(async () => {
if (!selectedOrganization.value) return
if (!selectedOrganization.value) return;
await useBaseFetch(`organization/${selectedOrganization.value.id}/projects`, {
method: 'POST',
method: "POST",
body: JSON.stringify({
project_id: props.project.id,
}),
apiVersion: 3,
})
});
await updateMembers()
await updateMembers();
addNotification({
group: 'main',
title: 'Project transferred',
text: 'Your project has been transferred to the organization.',
type: 'success',
})
})
group: "main",
title: "Project transferred",
text: "Your project has been transferred to the organization.",
type: "success",
});
});
const onRemoveFromOrg = useClientTry(async () => {
if (!props.project.organization || !auth.value?.user?.id) return
if (!props.project.organization || !auth.value?.user?.id) return;
await useBaseFetch(`organization/${props.project.organization}/projects/${props.project.id}`, {
method: 'DELETE',
method: "DELETE",
body: JSON.stringify({
new_owner: auth.value.user.id,
}),
apiVersion: 3,
})
});
await updateMembers()
await updateMembers();
addNotification({
group: 'main',
title: 'Project removed',
text: 'Your project has been removed from the organization.',
type: 'success',
})
})
group: "main",
title: "Project removed",
text: "Your project has been removed from the organization.",
type: "success",
});
});
const leaveProject = async () => {
await removeSelfFromTeam(props.project.team)
navigateTo('/dashboard/projects')
}
await removeSelfFromTeam(props.project.team);
navigateTo("/dashboard/projects");
};
const inviteTeamMember = async () => {
startLoading()
startLoading();
try {
const user = await useBaseFetch(`user/${currentUsername.value}`)
const user = await useBaseFetch(`user/${currentUsername.value}`);
const data = {
user_id: user.id.trim(),
}
};
await useBaseFetch(`team/${props.project.team}/members`, {
method: 'POST',
method: "POST",
body: data,
})
currentUsername.value = ''
await updateMembers()
});
currentUsername.value = "";
await updateMembers();
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
}
stopLoading()
}
stopLoading();
};
const removeTeamMember = async (index) => {
startLoading()
startLoading();
try {
await useBaseFetch(
`team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`,
{
method: 'DELETE',
}
)
await updateMembers()
method: "DELETE",
},
);
await updateMembers();
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
}
stopLoading()
}
stopLoading();
};
const updateTeamMember = async (index) => {
startLoading()
startLoading();
try {
const data = !allTeamMembers.value[index].is_owner
@@ -747,107 +747,107 @@ const updateTeamMember = async (index) => {
: {
payouts_split: allTeamMembers.value[index].payouts_split,
role: allTeamMembers.value[index].role,
}
};
await useBaseFetch(
`team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`,
{
method: 'PATCH',
method: "PATCH",
body: data,
}
)
await updateMembers()
},
);
await updateMembers();
addNotification({
group: 'main',
title: 'Member(s) updated',
group: "main",
title: "Member(s) updated",
text: "Your project's member(s) has been updated.",
type: 'success',
})
type: "success",
});
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
}
stopLoading()
}
stopLoading();
};
const transferOwnership = async (index) => {
startLoading()
startLoading();
try {
await useBaseFetch(`team/${props.project.team}/owner`, {
method: 'PATCH',
method: "PATCH",
body: {
user_id: allTeamMembers.value[index].user.id,
},
})
await updateMembers()
});
await updateMembers();
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
}
stopLoading()
}
stopLoading();
};
async function updateOrgMember(index) {
startLoading()
startLoading();
try {
if (allOrgMembers.value[index].override && !allOrgMembers.value[index].oldOverride) {
await useBaseFetch(`team/${props.project.team}/members`, {
method: 'POST',
method: "POST",
body: {
permissions: allOrgMembers.value[index].permissions,
role: allOrgMembers.value[index].role,
payouts_split: allOrgMembers.value[index].payouts_split,
user_id: allOrgMembers.value[index].user.id,
},
})
});
} else if (!allOrgMembers.value[index].override && allOrgMembers.value[index].oldOverride) {
await useBaseFetch(
`team/${props.project.team}/members/${allOrgMembers.value[index].user.id}`,
{
method: 'DELETE',
}
)
method: "DELETE",
},
);
} else {
await useBaseFetch(
`team/${props.project.team}/members/${allOrgMembers.value[index].user.id}`,
{
method: 'PATCH',
method: "PATCH",
body: {
permissions: allOrgMembers.value[index].permissions,
role: allOrgMembers.value[index].role,
payouts_split: allOrgMembers.value[index].payouts_split,
},
}
)
},
);
}
await updateMembers()
await updateMembers();
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
}
stopLoading()
stopLoading();
}
const updateMembers = async () => {
await Promise.all([props.resetProject(), props.resetOrganization(), props.resetMembers()])
}
await Promise.all([props.resetProject(), props.resetOrganization(), props.resetMembers()]);
};
</script>
<style lang="scss" scoped>

View File

@@ -113,9 +113,9 @@
</template>
<script>
import Checkbox from '~/components/ui/Checkbox.vue'
import StarIcon from '~/assets/images/utils/star.svg?component'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import Checkbox from "~/components/ui/Checkbox.vue";
import StarIcon from "~/assets/images/utils/star.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
export default defineNuxtComponent({
components: {
@@ -127,19 +127,19 @@ export default defineNuxtComponent({
project: {
type: Object,
default() {
return {}
return {};
},
},
allMembers: {
type: Array,
default() {
return []
return [];
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
patchProject: {
@@ -147,12 +147,12 @@ export default defineNuxtComponent({
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Patch project function not found',
type: 'error',
})
}
group: "main",
title: "An error occurred",
text: "Patch project function not found",
type: "error",
});
};
},
},
},
@@ -162,91 +162,91 @@ export default defineNuxtComponent({
(x) =>
x.project_type === this.project.actualProjectType &&
(this.project.categories.includes(x.name) ||
this.project.additional_categories.includes(x.name))
this.project.additional_categories.includes(x.name)),
),
featuredTags: this.$sortedCategories().filter(
(x) =>
x.project_type === this.project.actualProjectType &&
this.project.categories.includes(x.name)
this.project.categories.includes(x.name),
),
}
};
},
computed: {
categoryLists() {
const lists = {}
const lists = {};
this.$sortedCategories().forEach((x) => {
if (x.project_type === this.project.actualProjectType) {
const header = x.header
const header = x.header;
if (!lists[header]) {
lists[header] = []
lists[header] = [];
}
lists[header].push(x)
lists[header].push(x);
}
})
return lists
});
return lists;
},
patchData() {
const data = {}
const data = {};
// Promote selected categories to featured if there are less than 3 featured
const newFeaturedTags = this.featuredTags.slice()
const newFeaturedTags = this.featuredTags.slice();
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x))
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x));
nonFeaturedCategories
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
.forEach((x) => newFeaturedTags.push(x))
.forEach((x) => newFeaturedTags.push(x));
}
// Convert selected and featured categories to backend-usable arrays
const categories = newFeaturedTags.map((x) => x.name)
const categories = newFeaturedTags.map((x) => x.name);
const additionalCategories = this.selectedTags
.filter((x) => !newFeaturedTags.includes(x))
.map((x) => x.name)
.map((x) => x.name);
if (
categories.length !== this.project.categories.length ||
categories.some((value) => !this.project.categories.includes(value))
) {
data.categories = categories
data.categories = categories;
}
if (
additionalCategories.length !== this.project.additional_categories.length ||
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
) {
data.additional_categories = additionalCategories
data.additional_categories = additionalCategories;
}
return data
return data;
},
hasChanges() {
return Object.keys(this.patchData).length > 0
return Object.keys(this.patchData).length > 0;
},
},
methods: {
toggleCategory(category) {
if (this.selectedTags.includes(category)) {
this.selectedTags = this.selectedTags.filter((x) => x !== category)
this.selectedTags = this.selectedTags.filter((x) => x !== category);
if (this.featuredTags.includes(category)) {
this.featuredTags = this.featuredTags.filter((x) => x !== category)
this.featuredTags = this.featuredTags.filter((x) => x !== category);
}
} else {
this.selectedTags.push(category)
this.selectedTags.push(category);
}
},
toggleFeaturedCategory(category) {
if (this.featuredTags.includes(category)) {
this.featuredTags = this.featuredTags.filter((x) => x !== category)
this.featuredTags = this.featuredTags.filter((x) => x !== category);
} else {
this.featuredTags.push(category)
this.featuredTags.push(category);
}
},
saveChanges() {
if (this.hasChanges) {
this.patchProject(this.patchData)
this.patchProject(this.patchData);
}
},
},
})
});
</script>
<style lang="scss" scoped>
.label__title {

View File

@@ -220,7 +220,7 @@
/>
<nuxt-link v-if="!isEditing" :to="dependency.link" class="info">
<span class="project-title">
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
{{ dependency.project ? dependency.project.title : "Unknown Project" }}
</span>
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
Version {{ dependency.version.version_number }} is
@@ -232,7 +232,7 @@
</nuxt-link>
<div v-else class="info">
<span class="project-title">
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
{{ dependency.project ? dependency.project.title : "Unknown Project" }}
</span>
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
Version {{ dependency.version.version_number }} is
@@ -377,9 +377,9 @@
class="iconified-button raised-button"
@click="
() => {
deleteFiles.push(file.hashes.sha1)
version.files.splice(index, 1)
oldFileTypes.splice(index, 1)
deleteFiles.push(file.hashes.sha1);
version.files.splice(index, 1);
oldFileTypes.splice(index, 1);
}
"
>
@@ -421,8 +421,8 @@
class="iconified-button raised-button"
@click="
() => {
newFiles.splice(index, 1)
newFileTypes.splice(index, 1)
newFiles.splice(index, 1);
newFileTypes.splice(index, 1);
}
"
>
@@ -445,8 +445,8 @@
@change="
(x) =>
x.forEach((y) => {
newFiles.push(y)
newFileTypes.push(null)
newFiles.push(y);
newFileTypes.push(null);
})
"
>
@@ -516,7 +516,7 @@
:options="
tags.loaders
.filter((x) =>
x.supported_project_types.includes(project.actualProjectType.toLowerCase())
x.supported_project_types.includes(project.actualProjectType.toLowerCase()),
)
.map((it) => it.name)
"
@@ -574,7 +574,7 @@
<div v-if="!isEditing">
<h4>Publication date</h4>
<span>
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm A') }}
{{ $dayjs(version.date_published).format("MMMM D, YYYY [at] h:mm A") }}
</span>
</div>
<div v-if="!isEditing && version.author">
@@ -612,42 +612,42 @@
</div>
</template>
<script>
import { MarkdownEditor } from '@modrinth/ui'
import { Multiselect } from 'vue-multiselect'
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
import { inferVersionInfo } from '~/helpers/infer.js'
import { createDataPackVersion } from '~/helpers/package.js'
import { renderHighlightedString } from '~/helpers/highlight.js'
import { reportVersion } from '~/utils/report-helpers.ts'
import { useImageUpload } from '~/composables/image-upload.ts'
import { MarkdownEditor } from "@modrinth/ui";
import { Multiselect } from "vue-multiselect";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { inferVersionInfo } from "~/helpers/infer.js";
import { createDataPackVersion } from "~/helpers/package.js";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { reportVersion } from "~/utils/report-helpers.ts";
import { useImageUpload } from "~/composables/image-upload.ts";
import Avatar from '~/components/ui/Avatar.vue'
import Badge from '~/components/ui/Badge.vue'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import CopyCode from '~/components/ui/CopyCode.vue'
import Categories from '~/components/ui/search/Categories.vue'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import Chips from '~/components/ui/Chips.vue'
import Checkbox from '~/components/ui/Checkbox.vue'
import FileInput from '~/components/ui/FileInput.vue'
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Categories from "~/components/ui/search/Categories.vue";
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import Chips from "~/components/ui/Chips.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import FileInput from "~/components/ui/FileInput.vue";
import FileIcon from '~/assets/images/utils/file.svg?component'
import TrashIcon from '~/assets/images/utils/trash.svg?component'
import EditIcon from '~/assets/images/utils/edit.svg?component'
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import StarIcon from '~/assets/images/utils/star.svg?component'
import ReportIcon from '~/assets/images/utils/report.svg?component'
import SaveIcon from '~/assets/images/utils/save.svg?component'
import CrossIcon from '~/assets/images/utils/x.svg?component'
import HashIcon from '~/assets/images/utils/hash.svg?component'
import PlusIcon from '~/assets/images/utils/plus.svg?component'
import TransferIcon from '~/assets/images/utils/transfer.svg?component'
import UploadIcon from '~/assets/images/utils/upload.svg?component'
import BackIcon from '~/assets/images/utils/left-arrow.svg?component'
import BoxIcon from '~/assets/images/utils/box.svg?component'
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?component'
import Modal from '~/components/ui/Modal.vue'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
import FileIcon from "~/assets/images/utils/file.svg?component";
import TrashIcon from "~/assets/images/utils/trash.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import StarIcon from "~/assets/images/utils/star.svg?component";
import ReportIcon from "~/assets/images/utils/report.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import CrossIcon from "~/assets/images/utils/x.svg?component";
import HashIcon from "~/assets/images/utils/hash.svg?component";
import PlusIcon from "~/assets/images/utils/plus.svg?component";
import TransferIcon from "~/assets/images/utils/transfer.svg?component";
import UploadIcon from "~/assets/images/utils/upload.svg?component";
import BackIcon from "~/assets/images/utils/left-arrow.svg?component";
import BoxIcon from "~/assets/images/utils/box.svg?component";
import RightArrowIcon from "~/assets/images/utils/right-arrow.svg?component";
import Modal from "~/components/ui/Modal.vue";
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
export default defineNuxtComponent({
components: {
@@ -684,37 +684,37 @@ export default defineNuxtComponent({
project: {
type: Object,
default() {
return {}
return {};
},
},
versions: {
type: Array,
default() {
return []
return [];
},
},
featuredVersions: {
type: Array,
default() {
return []
return [];
},
},
members: {
type: Array,
default() {
return [{}]
return [{}];
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
dependencies: {
type: Object,
default() {
return {}
return {};
},
},
resetProject: {
@@ -724,93 +724,93 @@ export default defineNuxtComponent({
},
},
async setup(props) {
const data = useNuxtApp()
const route = useNativeRoute()
const data = useNuxtApp();
const route = useNativeRoute();
const auth = await useAuth()
const tags = useTags()
const auth = await useAuth();
const tags = useTags();
const path = route.name.split('-')
const mode = path[path.length - 1]
const path = route.name.split("-");
const mode = path[path.length - 1];
const fileTypes = [
{
display: 'Required resource pack',
value: 'required-resource-pack',
display: "Required resource pack",
value: "required-resource-pack",
},
{
display: 'Optional resource pack',
value: 'optional-resource-pack',
display: "Optional resource pack",
value: "optional-resource-pack",
},
]
let oldFileTypes = []
];
let oldFileTypes = [];
let isCreating = false
let isEditing = false
let isCreating = false;
let isEditing = false;
let version = {}
let primaryFile = {}
let alternateFile = {}
let version = {};
let primaryFile = {};
let alternateFile = {};
let replaceFile = null
let replaceFile = null;
if (mode === 'edit') {
isEditing = true
if (mode === "edit") {
isEditing = true;
}
if (route.params.version === 'create') {
isCreating = true
isEditing = true
if (route.params.version === "create") {
isCreating = true;
isEditing = true;
version = {
id: 'none',
id: "none",
project_id: props.project.id,
author_id: props.currentMember.user.id,
name: '',
version_number: '',
changelog: '',
name: "",
version_number: "",
changelog: "",
date_published: Date.now(),
downloads: 0,
version_type: 'release',
version_type: "release",
files: [],
dependencies: [],
game_versions: [],
loaders: [],
featured: false,
}
};
// For navigation from versions page / upload file prompt
if (process.client && history.state && history.state.newPrimaryFile) {
replaceFile = history.state.newPrimaryFile
replaceFile = history.state.newPrimaryFile;
try {
const inferredData = await inferVersionInfo(
replaceFile,
props.project,
tags.value.gameVersions
)
tags.value.gameVersions,
);
version = {
...version,
...inferredData,
}
};
} catch (err) {
console.error('Error parsing version file data', err)
console.error("Error parsing version file data", err);
}
}
} else if (route.params.version === 'latest') {
let versionList = props.versions
} else if (route.params.version === "latest") {
let versionList = props.versions;
if (route.query.loader) {
versionList = versionList.filter((x) => x.loaders.includes(route.query.loader))
versionList = versionList.filter((x) => x.loaders.includes(route.query.loader));
}
if (route.query.version) {
versionList = versionList.filter((x) => x.game_versions.includes(route.query.version))
versionList = versionList.filter((x) => x.game_versions.includes(route.query.version));
}
version = versionList.reduce((a, b) => (a.date_published > b.date_published ? a : b))
version = versionList.reduce((a, b) => (a.date_published > b.date_published ? a : b));
} else {
version = props.versions.find((x) => x.id === route.params.version)
version = props.versions.find((x) => x.id === route.params.version);
if (!version) {
version = props.versions.find((x) => x.displayUrlEnding === route.params.version)
version = props.versions.find((x) => x.displayUrlEnding === route.params.version);
}
}
@@ -818,58 +818,60 @@ export default defineNuxtComponent({
throw createError({
fatal: true,
statusCode: 404,
message: 'Version not found',
})
message: "Version not found",
});
}
version = JSON.parse(JSON.stringify(version))
primaryFile = version.files.find((file) => file.primary) ?? version.files[0]
version = JSON.parse(JSON.stringify(version));
primaryFile = version.files.find((file) => file.primary) ?? version.files[0];
alternateFile = version.files.find(
(file) => file.file_type && file.file_type.includes('resource-pack')
)
(file) => file.file_type && file.file_type.includes("resource-pack"),
);
for (const dependency of version.dependencies) {
dependency.version = props.dependencies.versions.find((x) => x.id === dependency.version_id)
dependency.version = props.dependencies.versions.find((x) => x.id === dependency.version_id);
if (dependency.version) {
dependency.project = props.dependencies.projects.find(
(x) => x.id === dependency.version.project_id
)
(x) => x.id === dependency.version.project_id,
);
}
if (!dependency.project) {
dependency.project = props.dependencies.projects.find((x) => x.id === dependency.project_id)
dependency.project = props.dependencies.projects.find(
(x) => x.id === dependency.project_id,
);
}
dependency.link = dependency.project
? `/${dependency.project.project_type}/${dependency.project.slug ?? dependency.project.id}${
dependency.version ? `/version/${encodeURI(dependency.version.version_number)}` : ''
dependency.version ? `/version/${encodeURI(dependency.version.version_number)}` : ""
}`
: ''
: "";
}
oldFileTypes = version.files.map((x) => fileTypes.find((y) => y.value === x.file_type))
oldFileTypes = version.files.map((x) => fileTypes.find((y) => y.value === x.file_type));
const title = computed(
() => `${isCreating ? 'Create Version' : version.name} - ${props.project.title}`
)
() => `${isCreating ? "Create Version" : version.name} - ${props.project.title}`,
);
const description = computed(
() =>
`Download ${props.project.title} ${
version.version_number
} on Modrinth. Supports ${data.$formatVersion(version.game_versions)} ${version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.join(' & ')}. Published on ${data
.join(" & ")}. Published on ${data
.$dayjs(version.date_published)
.format('MMM D, YYYY')}. ${version.downloads} downloads.`
)
.format("MMM D, YYYY")}. ${version.downloads} downloads.`,
);
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
})
});
return {
auth,
@@ -883,13 +885,13 @@ export default defineNuxtComponent({
alternateFile: ref(alternateFile),
replaceFile: ref(replaceFile),
uploadedImageIds: ref([]),
}
};
},
data() {
return {
dependencyAddMode: 'project',
newDependencyType: 'required',
newDependencyId: '',
dependencyAddMode: "project",
newDependencyType: "required",
newDependencyId: "",
showSnapshots: false,
@@ -899,103 +901,103 @@ export default defineNuxtComponent({
newFileTypes: [],
packageLoaders: ['forge', 'fabric', 'quilt'],
packageLoaders: ["forge", "fabric", "quilt"],
showKnownErrors: false,
shouldPreventActions: false,
}
};
},
computed: {
fieldErrors() {
return (
this.version.version_number === '' ||
this.version.version_number === "" ||
this.version.game_versions.length === 0 ||
(this.version.loaders.length === 0 && this.project.project_type !== 'resourcepack') ||
(this.version.loaders.length === 0 && this.project.project_type !== "resourcepack") ||
(this.newFiles.length === 0 && this.version.files.length === 0 && !this.replaceFile)
)
);
},
deps() {
const order = ['required', 'optional', 'incompatible', 'embedded']
const order = ["required", "optional", "incompatible", "embedded"];
return [...this.version.dependencies].sort(
(a, b) => order.indexOf(a.dependency_type) - order.indexOf(b.dependency_type)
)
(a, b) => order.indexOf(a.dependency_type) - order.indexOf(b.dependency_type),
);
},
},
watch: {
'$route.path'() {
const path = this.$route.name.split('-')
const mode = path[path.length - 1]
"$route.path"() {
const path = this.$route.name.split("-");
const mode = path[path.length - 1];
this.isEditing = mode === 'edit' || this.$route.params.version === 'create'
this.isEditing = mode === "edit" || this.$route.params.version === "create";
},
},
methods: {
async onImageUpload(file) {
const response = await useImageUpload(file, { context: 'version' })
const response = await useImageUpload(file, { context: "version" });
this.uploadedImageIds.push(response.id)
this.uploadedImageIds = this.uploadedImageIds.slice(-10)
this.uploadedImageIds.push(response.id);
this.uploadedImageIds = this.uploadedImageIds.slice(-10);
return response.url
return response.url;
},
getPreviousLink() {
if (this.$router.options.history.state.back) {
if (
this.$router.options.history.state.back.includes('/changelog') ||
this.$router.options.history.state.back.includes('/versions')
this.$router.options.history.state.back.includes("/changelog") ||
this.$router.options.history.state.back.includes("/versions")
) {
return this.$router.options.history.state.back
return this.$router.options.history.state.back;
}
}
return `/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.id
}/versions`
}/versions`;
},
getPreviousLabel() {
return this.$router.options.history.state.back &&
this.$router.options.history.state.back.endsWith('/changelog')
? 'Changelog'
: 'Versions'
this.$router.options.history.state.back.endsWith("/changelog")
? "Changelog"
: "Versions";
},
acceptFileFromProjectType,
renderHighlightedString,
async addDependency(dependencyAddMode, newDependencyId, newDependencyType, hideErrors) {
try {
if (dependencyAddMode === 'project') {
const project = await useBaseFetch(`project/${newDependencyId}`)
if (dependencyAddMode === "project") {
const project = await useBaseFetch(`project/${newDependencyId}`);
if (this.version.dependencies.some((dep) => project.id === dep.project_id)) {
this.$notify({
group: 'main',
title: 'Dependency already added',
text: 'You cannot add the same dependency twice.',
type: 'error',
})
group: "main",
title: "Dependency already added",
text: "You cannot add the same dependency twice.",
type: "error",
});
} else {
this.version.dependencies.push({
project,
project_id: project.id,
dependency_type: newDependencyType,
link: `/${project.project_type}/${project.slug ?? project.id}`,
})
});
this.$emit('update:dependencies', {
this.$emit("update:dependencies", {
projects: this.dependencies.projects.concat([project]),
versions: this.dependencies.versions,
})
});
}
} else if (dependencyAddMode === 'version') {
const version = await useBaseFetch(`version/${this.newDependencyId}`)
} else if (dependencyAddMode === "version") {
const version = await useBaseFetch(`version/${this.newDependencyId}`);
const project = await useBaseFetch(`project/${version.project_id}`)
const project = await useBaseFetch(`project/${version.project_id}`);
if (this.version.dependencies.some((dep) => version.id === dep.version_id)) {
this.$notify({
group: 'main',
title: 'Dependency already added',
text: 'You cannot add the same dependency twice.',
type: 'error',
})
group: "main",
title: "Dependency already added",
text: "You cannot add the same dependency twice.",
type: "error",
});
} else {
this.version.dependencies.push({
version,
@@ -1004,68 +1006,68 @@ export default defineNuxtComponent({
project_id: project.id,
dependency_type: this.newDependencyType,
link: `/${project.project_type}/${project.slug ?? project.id}/version/${encodeURI(
version.version_number
version.version_number,
)}`,
})
});
this.$emit('update:dependencies', {
this.$emit("update:dependencies", {
projects: this.dependencies.projects.concat([project]),
versions: this.dependencies.versions.concat([version]),
})
});
}
}
this.newDependencyId = ''
this.newDependencyId = "";
} catch {
if (!hideErrors) {
this.$notify({
group: 'main',
title: 'Invalid Dependency',
text: 'The specified dependency could not be found',
type: 'error',
})
group: "main",
title: "Invalid Dependency",
text: "The specified dependency could not be found",
type: "error",
});
}
}
},
async saveEditedVersion() {
startLoading()
startLoading();
if (this.fieldErrors) {
this.showKnownErrors = true
this.showKnownErrors = true;
stopLoading()
return
stopLoading();
return;
}
try {
if (this.newFiles.length > 0) {
const formData = new FormData()
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
const formData = new FormData();
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`);
formData.append(
'data',
"data",
JSON.stringify({
file_types: this.newFileTypes.reduce(
(acc, x, i) => ({
...acc,
[fileParts[i]]: x ? x.value : null,
}),
{}
{},
),
})
)
}),
);
for (let i = 0; i < this.newFiles.length; i++) {
formData.append(fileParts[i], new Blob([this.newFiles[i]]), this.newFiles[i].name)
formData.append(fileParts[i], new Blob([this.newFiles[i]]), this.newFiles[i].name);
}
await useBaseFetch(`version/${this.version.id}/file`, {
method: 'POST',
method: "POST",
body: formData,
headers: {
'Content-Disposition': formData,
"Content-Disposition": formData,
},
})
});
}
const body = {
@@ -1076,89 +1078,89 @@ export default defineNuxtComponent({
dependencies: this.version.dependencies,
game_versions: this.version.game_versions,
loaders: this.version.loaders,
primary_file: ['sha1', this.primaryFile.hashes.sha1],
primary_file: ["sha1", this.primaryFile.hashes.sha1],
featured: this.version.featured,
file_types: this.oldFileTypes.map((x, i) => {
return {
algorithm: 'sha1',
algorithm: "sha1",
hash: this.version.files[i].hashes.sha1,
file_type: x ? x.value : null,
}
};
}),
}
};
if (this.project.project_type === 'modpack') {
delete body.dependencies
if (this.project.project_type === "modpack") {
delete body.dependencies;
}
await useBaseFetch(`version/${this.version.id}`, {
method: 'PATCH',
method: "PATCH",
body,
})
});
for (const hash of this.deleteFiles) {
await useBaseFetch(`version_file/${hash}?version_id=${this.version.id}`, {
method: 'DELETE',
})
method: "DELETE",
});
}
await this.resetProjectVersions()
await this.resetProjectVersions();
await this.$router.replace(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.id
}/version/${encodeURI(
this.versions.find((x) => x.id === this.version.id).displayUrlEnding
)}`
)
this.versions.find((x) => x.id === this.version.id).displayUrlEnding,
)}`,
);
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
type: "error",
});
window.scrollTo({ top: 0, behavior: "smooth" });
}
stopLoading()
stopLoading();
},
reportVersion,
async createVersion() {
this.shouldPreventActions = true
startLoading()
this.shouldPreventActions = true;
startLoading();
if (this.fieldErrors) {
this.showKnownErrors = true
this.shouldPreventActions = false
this.showKnownErrors = true;
this.shouldPreventActions = false;
stopLoading()
return
stopLoading();
return;
}
try {
await this.createVersionRaw(this.version)
await this.createVersionRaw(this.version);
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
type: "error",
});
window.scrollTo({ top: 0, behavior: "smooth" });
}
stopLoading()
this.shouldPreventActions = false
stopLoading();
this.shouldPreventActions = false;
},
async createVersionRaw(version) {
const formData = new FormData()
const formData = new FormData();
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`);
if (this.replaceFile) {
fileParts.unshift(this.replaceFile.name.concat('-primary'))
fileParts.unshift(this.replaceFile.name.concat("-primary"));
}
if (this.project.project_type === 'resourcepack') {
version.loaders = ['minecraft']
if (this.project.project_type === "resourcepack") {
version.loaders = ["minecraft"];
}
const newVersion = {
@@ -1177,58 +1179,58 @@ export default defineNuxtComponent({
...acc,
[fileParts[this.replaceFile ? i + 1 : i]]: x ? x.value : null,
}),
{}
{},
),
}
};
formData.append('data', JSON.stringify(newVersion))
formData.append("data", JSON.stringify(newVersion));
if (this.replaceFile) {
formData.append(
this.replaceFile.name.concat('-primary'),
this.replaceFile.name.concat("-primary"),
new Blob([this.replaceFile]),
this.replaceFile.name
)
this.replaceFile.name,
);
}
for (let i = 0; i < this.newFiles.length; i++) {
formData.append(
fileParts[this.replaceFile ? i + 1 : i],
new Blob([this.newFiles[i]]),
this.newFiles[i].name
)
this.newFiles[i].name,
);
}
const data = await useBaseFetch('version', {
method: 'POST',
const data = await useBaseFetch("version", {
method: "POST",
body: formData,
headers: {
'Content-Disposition': formData,
"Content-Disposition": formData,
},
})
});
await this.resetProjectVersions()
await this.resetProjectVersions();
await this.$router.push(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.project_id
}/version/${data.id}`
)
}/version/${data.id}`,
);
},
async deleteVersion() {
startLoading()
startLoading();
await useBaseFetch(`version/${this.version.id}`, {
method: 'DELETE',
})
method: "DELETE",
});
await this.resetProjectVersions()
await this.$router.replace(`/${this.project.project_type}/${this.project.id}/versions`)
stopLoading()
await this.resetProjectVersions();
await this.$router.replace(`/${this.project.project_type}/${this.project.id}/versions`);
stopLoading();
},
async createDataPackVersion() {
this.shouldPreventActions = true
startLoading()
this.shouldPreventActions = true;
startLoading();
try {
const blob = await createDataPackVersion(
this.project,
@@ -1236,15 +1238,15 @@ export default defineNuxtComponent({
this.primaryFile,
this.members,
this.tags.gameVersions,
this.packageLoaders
)
this.packageLoaders,
);
this.newFiles = []
this.newFileTypes = []
this.newFiles = [];
this.newFileTypes = [];
this.replaceFile = new File(
[blob],
`${this.project.slug}-${this.version.version_number}.jar`
)
`${this.project.slug}-${this.version.version_number}.jar`,
);
await this.createVersionRaw({
project_id: this.project.id,
@@ -1257,26 +1259,26 @@ export default defineNuxtComponent({
game_versions: this.version.game_versions,
loaders: this.packageLoaders,
featured: this.version.featured,
})
});
this.$refs.modal_package_mod.hide()
this.$refs.modal_package_mod.hide();
this.$notify({
group: 'main',
title: 'Packaging Success',
text: 'Your data pack was successfully packaged as a mod! Make sure to playtest to check for errors.',
type: 'success',
})
group: "main",
title: "Packaging Success",
text: "Your data pack was successfully packaged as a mod! Make sure to playtest to check for errors.",
type: "success",
});
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
this.shouldPreventActions = false
stopLoading();
this.shouldPreventActions = false;
},
async resetProjectVersions() {
const [versions, featuredVersions, dependencies] = await Promise.all([
@@ -1284,21 +1286,21 @@ export default defineNuxtComponent({
useBaseFetch(`project/${this.version.project_id}/version?featured=true`),
useBaseFetch(`project/${this.version.project_id}/dependencies`),
this.resetProject(),
])
]);
const newCreatedVersions = this.$computeVersions(versions, this.members)
const featuredIds = featuredVersions.map((x) => x.id)
this.$emit('update:versions', newCreatedVersions)
const newCreatedVersions = this.$computeVersions(versions, this.members);
const featuredIds = featuredVersions.map((x) => x.id);
this.$emit("update:versions", newCreatedVersions);
this.$emit(
'update:featuredVersions',
newCreatedVersions.filter((version) => featuredIds.includes(version.id))
)
this.$emit('update:dependencies', dependencies)
"update:featuredVersions",
newCreatedVersions.filter((version) => featuredIds.includes(version.id)),
);
this.$emit("update:dependencies", dependencies);
return newCreatedVersions
return newCreatedVersions;
},
},
})
});
</script>
<style lang="scss" scoped>
@@ -1310,11 +1312,11 @@ export default defineNuxtComponent({
display: grid;
grid-template:
'title' auto
'changelog' auto
'dependencies' auto
'metadata' auto
'files' auto
"title" auto
"changelog" auto
"dependencies" auto
"metadata" auto
"files" auto
/ 1fr;
column-gap: var(--spacing-card-md);
@@ -1330,13 +1332,13 @@ export default defineNuxtComponent({
gap: var(--spacing-card-md);
h2,
input[type='text'] {
input[type="text"] {
margin: 0;
font-size: var(--font-size-2xl);
font-weight: bold;
}
input[type='text'] {
input[type="text"] {
max-width: 100%;
min-width: 0;
flex-grow: 1;
@@ -1536,11 +1538,11 @@ export default defineNuxtComponent({
@media (min-width: 1200px) {
.version-page {
grid-template:
'title title' auto
'changelog metadata' auto
'dependencies metadata' auto
'files metadata' auto
'dummy metadata' 1fr
"title title" auto
"changelog metadata" auto
"dependencies metadata" auto
"files metadata" auto
"dummy metadata" 1fr
/ 1fr 20rem;
}
}

View File

@@ -3,6 +3,6 @@
</template>
<script setup>
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
</script>

View File

@@ -39,7 +39,7 @@
$router.push(
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
}/version/${encodeURI(version.displayUrlEnding)}`,
)
"
>
@@ -72,7 +72,7 @@
</div>
<div class="version__supports">
<span>
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
{{ version.loaders.map((x) => $formatCategory(x)).join(", ") }}
</span>
<span>{{ $formatVersion(version.game_versions) }}</span>
</div>
@@ -83,7 +83,7 @@
</span>
<span>
Published on
<strong>{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</strong>
<strong>{{ $dayjs(version.date_published).format("MMM D, YYYY") }}</strong>
</span>
</div>
</div>
@@ -98,104 +98,104 @@
</div>
</template>
<script setup>
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import UploadIcon from '~/assets/images/utils/upload.svg?component'
import InfoIcon from '~/assets/images/utils/info.svg?component'
import VersionBadge from '~/components/ui/Badge.vue'
import FileInput from '~/components/ui/FileInput.vue'
import DropArea from '~/components/ui/DropArea.vue'
import Pagination from '~/components/ui/Pagination.vue'
import VersionFilterControl from '~/components/ui/VersionFilterControl.vue'
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import UploadIcon from "~/assets/images/utils/upload.svg?component";
import InfoIcon from "~/assets/images/utils/info.svg?component";
import VersionBadge from "~/components/ui/Badge.vue";
import FileInput from "~/components/ui/FileInput.vue";
import DropArea from "~/components/ui/DropArea.vue";
import Pagination from "~/components/ui/Pagination.vue";
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
const props = defineProps({
project: {
type: Object,
default() {
return {}
return {};
},
},
versions: {
type: Array,
default() {
return []
return [];
},
},
members: {
type: Array,
default() {
return []
return [];
},
},
currentMember: {
type: Object,
default() {
return null
return null;
},
},
})
});
const data = useNuxtApp()
const data = useNuxtApp();
const title = `${props.project.title} - Versions`
const title = `${props.project.title} - Versions`;
const description = `Download and browse ${props.versions.length} ${
props.project.title
} versions. ${data.$formatNumber(props.project.downloads)} total downloads. Last updated ${data
.$dayjs(props.project.updated)
.format('MMM D, YYYY')}.`
.format("MMM D, YYYY")}.`;
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
})
});
const router = useNativeRouter()
const route = useNativeRoute()
const router = useNativeRouter();
const route = useNativeRoute();
const currentPage = ref(Number(route.query.p ?? 1))
const currentPage = ref(Number(route.query.p ?? 1));
const filteredVersions = computed(() => {
const selectedGameVersions = getArrayOrString(route.query.g) ?? []
const selectedLoaders = getArrayOrString(route.query.l) ?? []
const selectedVersionTypes = getArrayOrString(route.query.c) ?? []
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
return props.versions.filter(
(projectVersion) =>
(selectedGameVersions.length === 0 ||
selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion)
projectVersion.game_versions.includes(gameVersion),
)) &&
(selectedLoaders.length === 0 ||
selectedLoaders.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionTypes.length === 0 ||
selectedVersionTypes.includes(projectVersion.version_type))
)
})
selectedVersionTypes.includes(projectVersion.version_type)),
);
});
function switchPage(page) {
currentPage.value = page
currentPage.value = page;
router.replace({
query: {
...route.query,
p: currentPage.value !== 1 ? currentPage.value : undefined,
},
})
});
}
async function handleFiles(files) {
await router.push({
name: 'type-id-version-version',
name: "type-id-version-version",
params: {
type: props.project.project_type,
id: props.project.slug ? props.project.slug : props.project.id,
version: 'create',
version: "create",
},
state: {
newPrimaryFile: files[0],
},
})
});
}
</script>
@@ -219,7 +219,7 @@ async function handleFiles(files) {
.header {
display: grid;
grid-template: 'download title supports stats';
grid-template: "download title supports stats";
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
color: var(--color-text-dark);
font-size: var(--font-size-md);
@@ -249,9 +249,9 @@ async function handleFiles(files) {
.version-button {
display: grid;
grid-template:
'download title supports stats'
'download metadata supports stats'
'download dummy supports stats';
"download title supports stats"
"download metadata supports stats"
"download dummy supports stats";
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
column-gap: var(--spacing-card-sm);
justify-content: left;
@@ -294,7 +294,7 @@ async function handleFiles(files) {
@media screen and (max-width: 1024px) {
.all-versions {
.header {
grid-template: 'download title';
grid-template: "download title";
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
div:nth-child(3) {
@@ -307,7 +307,7 @@ async function handleFiles(files) {
}
.version-button {
grid-template: 'download title' 'download metadata' 'download supports' 'download stats';
grid-template: "download title" "download metadata" "download supports" "download stats";
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
row-gap: var(--spacing-card-xs);

View File

@@ -7,119 +7,119 @@ import {
EditIcon,
DownloadIcon,
LinkIcon,
} from '@modrinth/assets'
import Avatar from '~/components/ui/Avatar.vue'
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
import Badge from '~/components/ui/Badge.vue'
import PrismIcon from '~/assets/images/external/prism.svg?component'
import ATLauncher from '~/assets/images/external/atlauncher.svg?component'
import CurseForge from '~/assets/images/external/curseforge.svg?component'
import Checkbox from '~/components/ui/Checkbox.vue'
} from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue";
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
import Badge from "~/components/ui/Badge.vue";
import PrismIcon from "~/assets/images/external/prism.svg?component";
import ATLauncher from "~/assets/images/external/atlauncher.svg?component";
import CurseForge from "~/assets/images/external/curseforge.svg?component";
import Checkbox from "~/components/ui/Checkbox.vue";
const os = ref(null)
const macValue = ref(null)
const downloadWindows = ref(null)
const downloadLinux = ref(null)
const downloadSection = ref(null)
const windowsLink = ref(null)
const os = ref(null);
const macValue = ref(null);
const downloadWindows = ref(null);
const downloadLinux = ref(null);
const downloadSection = ref(null);
const windowsLink = ref(null);
const linuxLinks = {
appImage: null,
deb: null,
thirdParty: 'https://support.modrinth.com/en/articles/9298760',
}
thirdParty: "https://support.modrinth.com/en/articles/9298760",
};
const macLinks = {
appleSilicon: null,
intel: null,
}
};
let downloadLauncher
let downloadLauncher;
const [{ data: rows }, { data: launcherUpdates }] = await Promise.all([
useAsyncData('projects', () => useBaseFetch('projects_random?count=40'), {
useAsyncData("projects", () => useBaseFetch("projects_random?count=40"), {
transform: (homepageProjects) => {
const val = Math.ceil(homepageProjects.length / 6)
const val = Math.ceil(homepageProjects.length / 6);
return [
homepageProjects.slice(0, val),
homepageProjects.slice(val, val * 2),
homepageProjects.slice(val * 2, val * 3),
homepageProjects.slice(val * 3, val * 4),
homepageProjects.slice(val * 4, val * 5),
]
];
},
}),
await useAsyncData('launcherUpdates', () =>
$fetch('https://launcher-files.modrinth.com/updates.json')
await useAsyncData("launcherUpdates", () =>
$fetch("https://launcher-files.modrinth.com/updates.json"),
),
])
]);
macLinks.appleSilicon = launcherUpdates.value.platforms['darwin-aarch64'].install_urls[0]
macLinks.intel = launcherUpdates.value.platforms['darwin-x86_64'].install_urls[0]
windowsLink.value = launcherUpdates.value.platforms['windows-x86_64'].install_urls[0]
linuxLinks.appImage = launcherUpdates.value.platforms['linux-x86_64'].install_urls[1]
linuxLinks.deb = launcherUpdates.value.platforms['linux-x86_64'].install_urls[0]
macLinks.appleSilicon = launcherUpdates.value.platforms["darwin-aarch64"].install_urls[0];
macLinks.intel = launcherUpdates.value.platforms["darwin-x86_64"].install_urls[0];
windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0];
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
onMounted(() => {
os.value = navigator?.platform.toString()
os.value = os.value?.includes('Mac')
? 'Mac'
: os.value?.includes('Win')
? 'Windows'
: os.value?.includes('Linux')
? 'Linux'
: null
os.value = navigator?.platform.toString();
os.value = os.value?.includes("Mac")
? "Mac"
: os.value?.includes("Win")
? "Windows"
: os.value?.includes("Linux")
? "Linux"
: null;
if (os.value === 'Windows') {
if (os.value === "Windows") {
downloadLauncher = () => {
downloadWindows.value.click()
}
} else if (os.value === 'Linux') {
downloadWindows.value.click();
};
} else if (os.value === "Linux") {
downloadLauncher = () => {
downloadLinux.value.click()
}
downloadLinux.value.click();
};
} else {
downloadLauncher = () => {
scrollToSection()
}
scrollToSection();
};
}
})
});
watch(macValue, () => {
if (macValue.value === 'Download for Apple Silicon') {
const link = document.createElement('a')
link.href = macLinks.appleSilicon
link.download = ''
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} else if (macValue.value === 'Download for Intel') {
const link = document.createElement('a')
link.href = macLinks.intel
link.download = ''
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
if (macValue.value === "Download for Apple Silicon") {
const link = document.createElement("a");
link.href = macLinks.appleSilicon;
link.download = "";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (macValue.value === "Download for Intel") {
const link = document.createElement("a");
link.href = macLinks.intel;
link.download = "";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
})
});
const scrollToSection = () => {
nextTick(() => {
window.scrollTo({
top: downloadSection.value.offsetTop,
behavior: 'smooth',
})
})
}
behavior: "smooth",
});
});
};
const title = 'Download the Modrinth App!'
const title = "Download the Modrinth App!";
const description =
'The Modrinth App is a unique, open source launcher that allows you to play your favorite mods, and keep them up to date, all in one neat little package.'
"The Modrinth App is a unique, open source launcher that allows you to play your favorite mods, and keep them up to date, all in one neat little package.";
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
})
});
</script>
<template>
@@ -128,7 +128,7 @@ useSeoMeta({
<h1 class="main-header">
Download Modrinth <br v-if="os" />
App
{{ os ? `for ${os}` : '' }}
{{ os ? `for ${os}` : "" }}
</h1>
<h2 class="main-subheader">
The Modrinth App is a unique, open source launcher that allows you to play your favorite
@@ -462,7 +462,7 @@ useSeoMeta({
</div>
<div class="cell important">Modrinth App</div>
<div class="cell important">Small</div>
<div class="cell important">{{ '< 150 MB' }}</div>
<div class="cell important">{{ "< 150 MB" }}</div>
</div>
<div class="row">
<div class="cell">
@@ -532,7 +532,7 @@ useSeoMeta({
started with the Modrinth App in seconds!
</p>
</div>
<div class="ring inner-ring">
<div class="inner-ring ring">
<div class="icon-logo">
<LogoAnimated class="icon" />
</div>
@@ -996,7 +996,7 @@ useSeoMeta({
<style scoped lang="scss">
.landing-hero {
position: relative;
background: #0f1121 url('https://cdn-raw.modrinth.com/app-landing/cube-black.png') no-repeat
background: #0f1121 url("https://cdn-raw.modrinth.com/app-landing/cube-black.png") no-repeat
center 4rem;
background-size: cover;
padding: 6rem 1rem 12rem 1rem;
@@ -1619,7 +1619,9 @@ useSeoMeta({
gap: 1rem;
border-radius: 1rem;
border: 1px solid var(--landing-border-color);
transition: background 0.5s ease-in-out, transform 0.05s ease-in-out;
transition:
background 0.5s ease-in-out,
transform 0.05s ease-in-out;
// Removed due to lag on mobile :(
background: var(--landing-blob-gradient);
@@ -1696,7 +1698,8 @@ useSeoMeta({
rgba(44, 48, 79, 0.35) 0%,
rgba(32, 35, 50, 0.27) 100%
);
box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.16),
box-shadow:
2px 2px 12px 0px rgba(0, 0, 0, 0.16),
2px 2px 64px 0px rgba(57, 61, 94, 0.45) inset;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
@@ -2003,7 +2006,7 @@ useSeoMeta({
border-radius: var(--radius-lg);
&:before {
content: '';
content: "";
position: absolute;
inset: 0;
padding: 1px;
@@ -2011,8 +2014,12 @@ useSeoMeta({
border-radius: 1rem;
background: var(--landing-border-gradient);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
@@ -2141,7 +2148,8 @@ useSeoMeta({
rgba(255, 255, 255, 0.35) 0%,
rgba(255, 255, 255, 0.27) 100%
) !important;
box-shadow: 2px 2px 64px 0px rgba(255, 255, 255, 0.45) inset,
box-shadow:
2px 2px 64px 0px rgba(255, 255, 255, 0.45) inset,
2px 2px 12px 0px rgba(0, 0, 0, 0.16) !important;
border: none !important;
}
@@ -2172,7 +2180,7 @@ useSeoMeta({
}
.landing-hero {
background: url('https://cdn-raw.modrinth.com/app-landing/cube-light.png') no-repeat center 4rem;
background: url("https://cdn-raw.modrinth.com/app-landing/cube-light.png") no-repeat center 4rem;
background-size: cover;
}

View File

@@ -80,159 +80,161 @@
</template>
<script setup>
import { Button, Avatar } from '@modrinth/ui'
import { XIcon, CheckIcon } from '@modrinth/assets'
import { useBaseFetch } from '@/composables/fetch.js'
import { useAuth } from '@/composables/auth.js'
import { Button, Avatar } from "@modrinth/ui";
import { XIcon, CheckIcon } from "@modrinth/assets";
import { useBaseFetch } from "@/composables/fetch.js";
import { useAuth } from "@/composables/auth.js";
import { useScopes } from '@/composables/auth/scopes.ts'
import { useScopes } from "@/composables/auth/scopes.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
appInfo: {
id: 'auth.authorize.app-info',
id: "auth.authorize.app-info",
defaultMessage:
'<strong>{appName}</strong> by <creator-link>{creator}</creator-link> will be able to:',
"<strong>{appName}</strong> by <creator-link>{creator}</creator-link> will be able to:",
},
authorize: {
id: 'auth.authorize.action.authorize',
defaultMessage: 'Authorize',
id: "auth.authorize.action.authorize",
defaultMessage: "Authorize",
},
decline: {
id: 'auth.authorize.action.decline',
defaultMessage: 'Decline',
id: "auth.authorize.action.decline",
defaultMessage: "Decline",
},
noRedirectUrlError: {
id: 'auth.authorize.error.no-redirect-url',
defaultMessage: 'No redirect location found in response',
id: "auth.authorize.error.no-redirect-url",
defaultMessage: "No redirect location found in response",
},
redirectUrl: {
id: 'auth.authorize.redirect-url',
defaultMessage: 'You will be redirected to <redirect-url>{url}</redirect-url>',
id: "auth.authorize.redirect-url",
defaultMessage: "You will be redirected to <redirect-url>{url}</redirect-url>",
},
title: {
id: 'auth.authorize.authorize-app-name',
defaultMessage: 'Authorize {appName}',
id: "auth.authorize.authorize-app-name",
defaultMessage: "Authorize {appName}",
},
})
});
const data = useNuxtApp()
const data = useNuxtApp();
const router = useNativeRoute()
const auth = await useAuth()
const { scopesToDefinitions } = useScopes()
const router = useNativeRoute();
const auth = await useAuth();
const { scopesToDefinitions } = useScopes();
const clientId = router.query?.client_id || false
const redirectUri = router.query?.redirect_uri || false
const scope = router.query?.scope || false
const state = router.query?.state || false
const clientId = router.query?.client_id || false;
const redirectUri = router.query?.redirect_uri || false;
const scope = router.query?.scope || false;
const state = router.query?.state || false;
const getFlowIdAuthorization = async () => {
const query = {
client_id: clientId,
redirect_uri: redirectUri,
scope,
}
};
if (state) {
query.state = state
query.state = state;
}
const authorization = await useBaseFetch('oauth/authorize', {
method: 'GET',
const authorization = await useBaseFetch("oauth/authorize", {
method: "GET",
internal: true,
query,
}) // This will contain the flow_id and oauth_client_id for accepting the oauth on behalf of the user
}); // This will contain the flow_id and oauth_client_id for accepting the oauth on behalf of the user
if (typeof authorization === 'string') {
if (typeof authorization === "string") {
await navigateTo(authorization, {
external: true,
})
});
}
return authorization
}
return authorization;
};
const {
data: authorizationData,
pending,
error,
} = await useAsyncData('authorization', getFlowIdAuthorization)
} = await useAsyncData("authorization", getFlowIdAuthorization);
const { data: app } = await useAsyncData('oauth/app/' + clientId, () =>
useBaseFetch('oauth/app/' + clientId, {
method: 'GET',
const { data: app } = await useAsyncData("oauth/app/" + clientId, () =>
useBaseFetch("oauth/app/" + clientId, {
method: "GET",
internal: true,
})
)
}),
);
const scopeDefinitions = scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0))
const scopeDefinitions = scopesToDefinitions(
BigInt(authorizationData.value?.requested_scopes || 0),
);
const { data: createdBy } = await useAsyncData('user/' + app.value.created_by, () =>
useBaseFetch('user/' + app.value.created_by, {
method: 'GET',
const { data: createdBy } = await useAsyncData("user/" + app.value.created_by, () =>
useBaseFetch("user/" + app.value.created_by, {
method: "GET",
apiVersion: 3,
})
)
}),
);
const onAuthorize = async () => {
try {
const res = await useBaseFetch('oauth/accept', {
method: 'POST',
const res = await useBaseFetch("oauth/accept", {
method: "POST",
internal: true,
body: {
flow: authorizationData.value.flow_id,
},
})
});
if (typeof res === 'string') {
if (typeof res === "string") {
navigateTo(res, {
external: true,
})
return
});
return;
}
throw new Error(formatMessage(messages.noRedirectUrlError))
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch (error) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
}
};
const onReject = async () => {
try {
const res = await useBaseFetch('oauth/reject', {
method: 'POST',
const res = await useBaseFetch("oauth/reject", {
method: "POST",
body: {
flow: authorizationData.value.flow_id,
},
})
});
if (typeof res === 'string') {
if (typeof res === "string") {
navigateTo(res, {
external: true,
})
return
});
return;
}
throw new Error(formatMessage(messages.noRedirectUrlError))
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch (error) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
}
};
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
</script>
<style scoped lang="scss">

View File

@@ -72,163 +72,163 @@
</div>
</template>
<script setup>
import { SendIcon, MailIcon, KeyIcon } from '@modrinth/assets'
import { SendIcon, MailIcon, KeyIcon } from "@modrinth/assets";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const methodChoiceMessages = defineMessages({
description: {
id: 'auth.reset-password.method-choice.description',
id: "auth.reset-password.method-choice.description",
defaultMessage:
"Enter your email below and we'll send a recovery link to allow you to recover your account.",
},
emailUsernameLabel: {
id: 'auth.reset-password.method-choice.email-username.label',
defaultMessage: 'Email or username',
id: "auth.reset-password.method-choice.email-username.label",
defaultMessage: "Email or username",
},
emailUsernamePlaceholder: {
id: 'auth.reset-password.method-choice.email-username.placeholder',
defaultMessage: 'Email',
id: "auth.reset-password.method-choice.email-username.placeholder",
defaultMessage: "Email",
},
action: {
id: 'auth.reset-password.method-choice.action',
defaultMessage: 'Send recovery email',
id: "auth.reset-password.method-choice.action",
defaultMessage: "Send recovery email",
},
})
});
const postChallengeMessages = defineMessages({
description: {
id: 'auth.reset-password.post-challenge.description',
defaultMessage: 'Enter your new password below to gain access to your account.',
id: "auth.reset-password.post-challenge.description",
defaultMessage: "Enter your new password below to gain access to your account.",
},
confirmPasswordLabel: {
id: 'auth.reset-password.post-challenge.confirm-password.label',
defaultMessage: 'Confirm password',
id: "auth.reset-password.post-challenge.confirm-password.label",
defaultMessage: "Confirm password",
},
action: {
id: 'auth.reset-password.post-challenge.action',
defaultMessage: 'Reset password',
id: "auth.reset-password.post-challenge.action",
defaultMessage: "Reset password",
},
})
});
// NOTE(Brawaru): Vite uses esbuild for minification so can't combine these
// because it'll keep the original prop names compared to consts, which names
// will be mangled.
const emailSentNotificationMessages = defineMessages({
title: {
id: 'auth.reset-password.notification.email-sent.title',
defaultMessage: 'Email sent',
id: "auth.reset-password.notification.email-sent.title",
defaultMessage: "Email sent",
},
text: {
id: 'auth.reset-password.notification.email-sent.text',
id: "auth.reset-password.notification.email-sent.text",
defaultMessage:
'An email with instructions has been sent to you if the email was previously saved on your account.',
"An email with instructions has been sent to you if the email was previously saved on your account.",
},
})
});
const passwordResetNotificationMessages = defineMessages({
title: {
id: 'auth.reset-password.notification.password-reset.title',
defaultMessage: 'Password successfully reset',
id: "auth.reset-password.notification.password-reset.title",
defaultMessage: "Password successfully reset",
},
text: {
id: 'auth.reset-password.notification.password-reset.text',
defaultMessage: 'You can now log-in into your account with your new password.',
id: "auth.reset-password.notification.password-reset.text",
defaultMessage: "You can now log-in into your account with your new password.",
},
})
});
const messages = defineMessages({
title: {
id: 'auth.reset-password.title',
defaultMessage: 'Reset Password',
id: "auth.reset-password.title",
defaultMessage: "Reset Password",
},
longTitle: {
id: 'auth.reset-password.title.long',
defaultMessage: 'Reset your password',
id: "auth.reset-password.title.long",
defaultMessage: "Reset your password",
},
})
});
useHead({
title: () => `${formatMessage(messages.title)} - Modrinth`,
})
});
const auth = await useAuth()
const auth = await useAuth();
if (auth.value.user) {
await navigateTo('/dashboard')
await navigateTo("/dashboard");
}
const route = useNativeRoute()
const route = useNativeRoute();
const step = ref('choose_method')
const step = ref("choose_method");
if (route.query.flow) {
step.value = 'passed_challenge'
step.value = "passed_challenge";
}
const turnstile = ref()
const turnstile = ref();
const email = ref('')
const token = ref('')
const email = ref("");
const token = ref("");
async function recovery() {
startLoading()
startLoading();
try {
await useBaseFetch('auth/password/reset', {
method: 'POST',
await useBaseFetch("auth/password/reset", {
method: "POST",
body: {
username: email.value,
challenge: token.value,
},
})
});
addNotification({
group: 'main',
group: "main",
title: formatMessage(emailSentNotificationMessages.title),
text: formatMessage(emailSentNotificationMessages.text),
type: 'success',
})
type: "success",
});
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
stopLoading()
stopLoading();
}
const newPassword = ref('')
const confirmNewPassword = ref('')
const newPassword = ref("");
const confirmNewPassword = ref("");
async function changePassword() {
startLoading()
startLoading();
try {
await useBaseFetch('auth/password', {
method: 'PATCH',
await useBaseFetch("auth/password", {
method: "PATCH",
body: {
new_password: newPassword.value,
flow: route.query.flow,
},
})
});
addNotification({
group: 'main',
group: "main",
title: formatMessage(passwordResetNotificationMessages.title),
text: formatMessage(passwordResetNotificationMessages.text),
type: 'success',
})
await navigateTo('/auth/sign-in')
type: "success",
});
await navigateTo("/auth/sign-in");
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
stopLoading()
stopLoading();
}
</script>

View File

@@ -126,154 +126,154 @@ import {
SSOGitLabIcon,
KeyIcon,
MailIcon,
} from '@modrinth/assets'
} from "@modrinth/assets";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
additionalOptionsLabel: {
id: 'auth.sign-in.additional-options',
id: "auth.sign-in.additional-options",
defaultMessage:
'<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>',
"<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>",
},
emailUsernameLabel: {
id: 'auth.sign-in.email-username.label',
defaultMessage: 'Email or username',
id: "auth.sign-in.email-username.label",
defaultMessage: "Email or username",
},
passwordLabel: {
id: 'auth.sign-in.password.label',
defaultMessage: 'Password',
id: "auth.sign-in.password.label",
defaultMessage: "Password",
},
signInWithLabel: {
id: 'auth.sign-in.sign-in-with',
defaultMessage: 'Sign in with',
id: "auth.sign-in.sign-in-with",
defaultMessage: "Sign in with",
},
signInTitle: {
id: 'auth.sign-in.title',
defaultMessage: 'Sign In',
id: "auth.sign-in.title",
defaultMessage: "Sign In",
},
twoFactorCodeInputPlaceholder: {
id: 'auth.sign-in.2fa.placeholder',
defaultMessage: 'Enter code...',
id: "auth.sign-in.2fa.placeholder",
defaultMessage: "Enter code...",
},
twoFactorCodeLabel: {
id: 'auth.sign-in.2fa.label',
defaultMessage: 'Enter two-factor code',
id: "auth.sign-in.2fa.label",
defaultMessage: "Enter two-factor code",
},
twoFactorCodeLabelDescription: {
id: 'auth.sign-in.2fa.description',
defaultMessage: 'Please enter a two-factor code to proceed.',
id: "auth.sign-in.2fa.description",
defaultMessage: "Please enter a two-factor code to proceed.",
},
usePasswordLabel: {
id: 'auth.sign-in.use-password',
defaultMessage: 'Or use a password',
id: "auth.sign-in.use-password",
defaultMessage: "Or use a password",
},
})
});
useHead({
title() {
return `${formatMessage(messages.signInTitle)} - Modrinth`
return `${formatMessage(messages.signInTitle)} - Modrinth`;
},
})
});
const auth = await useAuth()
const route = useNativeRoute()
const auth = await useAuth();
const route = useNativeRoute();
const redirectTarget = route.query.redirect || ''
const redirectTarget = route.query.redirect || "";
if (route.fullPath.includes('new_account=true')) {
if (route.fullPath.includes("new_account=true")) {
await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ''
}`
)
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ""
}`,
);
} else if (route.query.code) {
await finishSignIn()
await finishSignIn();
}
if (auth.value.user) {
await finishSignIn()
await finishSignIn();
}
const turnstile = ref()
const turnstile = ref();
const email = ref('')
const password = ref('')
const token = ref('')
const email = ref("");
const password = ref("");
const token = ref("");
const flow = ref(route.query.flow)
const flow = ref(route.query.flow);
const signUpLink = computed(
() => `/auth/sign-up${route.query.redirect ? `?redirect=${route.query.redirect}` : ''}`
)
() => `/auth/sign-up${route.query.redirect ? `?redirect=${route.query.redirect}` : ""}`,
);
async function beginPasswordSignIn() {
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/login', {
method: 'POST',
const res = await useBaseFetch("auth/login", {
method: "POST",
body: {
username: email.value,
password: password.value,
challenge: token.value,
},
})
});
if (res.flow) {
flow.value = res.flow
flow.value = res.flow;
} else {
await finishSignIn(res.session)
await finishSignIn(res.session);
}
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
stopLoading()
stopLoading();
}
const twoFactorCode = ref(null)
const twoFactorCode = ref(null);
async function begin2FASignIn() {
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/login/2fa', {
method: 'POST',
const res = await useBaseFetch("auth/login/2fa", {
method: "POST",
body: {
flow: flow.value,
code: twoFactorCode.value ? twoFactorCode.value.toString() : twoFactorCode.value,
},
})
});
await finishSignIn(res.session)
await finishSignIn(res.session);
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
stopLoading()
stopLoading();
}
async function finishSignIn(token) {
if (token) {
await useAuth(token)
await useUser()
await useAuth(token);
await useUser();
}
if (route.query.redirect) {
const redirect = decodeURIComponent(route.query.redirect)
const redirect = decodeURIComponent(route.query.redirect);
await navigateTo(redirect, {
replace: true,
})
});
} else {
await navigateTo('/dashboard')
await navigateTo("/dashboard");
}
}
</script>

View File

@@ -143,111 +143,111 @@ import {
KeyIcon,
MailIcon,
SSOGitLabIcon,
} from '@modrinth/assets'
import { Checkbox } from '@modrinth/ui'
} from "@modrinth/assets";
import { Checkbox } from "@modrinth/ui";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
title: {
id: 'auth.sign-up.title',
defaultMessage: 'Sign Up',
id: "auth.sign-up.title",
defaultMessage: "Sign Up",
},
signUpWithTitle: {
id: 'auth.sign-up.title.sign-up-with',
defaultMessage: 'Sign up with',
id: "auth.sign-up.title.sign-up-with",
defaultMessage: "Sign up with",
},
createAccountTitle: {
id: 'auth.sign-up.title.create-account',
defaultMessage: 'Or create an account yourself',
id: "auth.sign-up.title.create-account",
defaultMessage: "Or create an account yourself",
},
emailLabel: {
id: 'auth.sign-up.email.label',
defaultMessage: 'Email',
id: "auth.sign-up.email.label",
defaultMessage: "Email",
},
usernameLabel: {
id: 'auth.sign-up.label.username',
defaultMessage: 'Username',
id: "auth.sign-up.label.username",
defaultMessage: "Username",
},
passwordLabel: {
id: 'auth.sign-up.password.label',
defaultMessage: 'Password',
id: "auth.sign-up.password.label",
defaultMessage: "Password",
},
confirmPasswordLabel: {
id: 'auth.sign-up.confirm-password.label',
defaultMessage: 'Confirm password',
id: "auth.sign-up.confirm-password.label",
defaultMessage: "Confirm password",
},
subscribeLabel: {
id: 'auth.sign-up.subscribe.label',
defaultMessage: 'Subscribe to updates about Modrinth',
id: "auth.sign-up.subscribe.label",
defaultMessage: "Subscribe to updates about Modrinth",
},
legalDisclaimer: {
id: 'auth.sign-up.legal-dislaimer',
id: "auth.sign-up.legal-dislaimer",
defaultMessage:
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
},
createAccountButton: {
id: 'auth.sign-up.action.create-account',
defaultMessage: 'Create account',
id: "auth.sign-up.action.create-account",
defaultMessage: "Create account",
},
alreadyHaveAccountLabel: {
id: 'auth.sign-up.sign-in-option.title',
defaultMessage: 'Already have an account?',
id: "auth.sign-up.sign-in-option.title",
defaultMessage: "Already have an account?",
},
})
});
useHead({
title: () => `${formatMessage(messages.title)} - Modrinth`,
})
});
const auth = await useAuth()
const route = useNativeRoute()
const auth = await useAuth();
const route = useNativeRoute();
const redirectTarget = route.query.redirect
const redirectTarget = route.query.redirect;
if (route.fullPath.includes('new_account=true')) {
if (route.fullPath.includes("new_account=true")) {
await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ''
}`
)
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ""
}`,
);
}
if (auth.value.user) {
await navigateTo('/dashboard')
await navigateTo("/dashboard");
}
const turnstile = ref()
const turnstile = ref();
const email = ref('')
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const token = ref('')
const subscribe = ref(true)
const email = ref("");
const username = ref("");
const password = ref("");
const confirmPassword = ref("");
const token = ref("");
const subscribe = ref(true);
const signInLink = computed(
() => `/auth/sign-in${route.query.redirect ? `?redirect=${route.query.redirect}` : ''}`
)
() => `/auth/sign-in${route.query.redirect ? `?redirect=${route.query.redirect}` : ""}`,
);
async function createAccount() {
startLoading()
startLoading();
try {
if (confirmPassword.value !== password.value) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: formatMessage({
id: 'auth.sign-up.notification.password-mismatch.text',
defaultMessage: 'Passwords do not match!',
id: "auth.sign-up.notification.password-mismatch.text",
defaultMessage: "Passwords do not match!",
}),
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
const res = await useBaseFetch('auth/create', {
method: 'POST',
const res = await useBaseFetch("auth/create", {
method: "POST",
body: {
username: username.value,
password: password.value,
@@ -255,25 +255,25 @@ async function createAccount() {
challenge: token.value,
sign_up_newsletter: subscribe.value,
},
})
});
await useAuth(res.session)
await useUser()
await useAuth(res.session);
await useUser();
if (route.query.redirect) {
await navigateTo(route.query.redirect)
await navigateTo(route.query.redirect);
} else {
await navigateTo('/dashboard')
await navigateTo("/dashboard");
}
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
turnstile.value?.reset()
type: "error",
});
turnstile.value?.reset();
}
stopLoading()
stopLoading();
}
</script>

View File

@@ -52,101 +52,101 @@
</div>
</template>
<script setup>
import { SettingsIcon, RightArrowIcon } from '@modrinth/assets'
import { SettingsIcon, RightArrowIcon } from "@modrinth/assets";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
title: {
id: 'auth.verify-email.title',
defaultMessage: 'Verify Email',
id: "auth.verify-email.title",
defaultMessage: "Verify Email",
},
accountSettings: {
id: 'auth.verify-email.action.account-settings',
defaultMessage: 'Account settings',
id: "auth.verify-email.action.account-settings",
defaultMessage: "Account settings",
},
signIn: {
id: 'auth.verify-email.action.sign-in',
defaultMessage: 'Sign in',
id: "auth.verify-email.action.sign-in",
defaultMessage: "Sign in",
},
})
});
const alreadyVerifiedMessages = defineMessages({
title: {
id: 'auth.verify-email.already-verified.title',
defaultMessage: 'Email already verified',
id: "auth.verify-email.already-verified.title",
defaultMessage: "Email already verified",
},
description: {
id: 'auth.verify-email.already-verified.description',
defaultMessage: 'Your email is already verified!',
id: "auth.verify-email.already-verified.description",
defaultMessage: "Your email is already verified!",
},
})
});
const postVerificationMessages = defineMessages({
title: {
id: 'auth.verify-email.post-verification.title',
defaultMessage: 'Email verification',
id: "auth.verify-email.post-verification.title",
defaultMessage: "Email verification",
},
description: {
id: 'auth.verify-email.post-verification.description',
defaultMessage: 'Your email address has been successfully verified!',
id: "auth.verify-email.post-verification.description",
defaultMessage: "Your email address has been successfully verified!",
},
})
});
const failedVerificationMessages = defineMessages({
title: {
id: 'auth.verify-email.failed-verification.title',
defaultMessage: 'Email verification failed',
id: "auth.verify-email.failed-verification.title",
defaultMessage: "Email verification failed",
},
description: {
id: 'auth.verify-email.failed-verification.description',
id: "auth.verify-email.failed-verification.description",
defaultMessage:
'We were unable to verify your email. Try re-sending the verification email through your dashboard by signing in.',
"We were unable to verify your email. Try re-sending the verification email through your dashboard by signing in.",
},
loggedInDescription: {
id: 'auth.verify-email.failed-verification.description.logged-in',
id: "auth.verify-email.failed-verification.description.logged-in",
defaultMessage:
'We were unable to verify your email. Try re-sending the verification email through the button below.',
"We were unable to verify your email. Try re-sending the verification email through the button below.",
},
action: {
id: 'auth.verify-email.failed-verification.action',
defaultMessage: 'Resend verification email',
id: "auth.verify-email.failed-verification.action",
defaultMessage: "Resend verification email",
},
})
});
useHead({
title: () => `${formatMessage(messages.title)} - Modrinth`,
})
});
const auth = await useAuth()
const auth = await useAuth();
const success = ref(false)
const route = useNativeRoute()
const success = ref(false);
const route = useNativeRoute();
if (route.query.flow) {
try {
const emailVerified = useState('emailVerified', () => null)
const emailVerified = useState("emailVerified", () => null);
if (emailVerified.value === null) {
await useBaseFetch('auth/email/verify', {
method: 'POST',
await useBaseFetch("auth/email/verify", {
method: "POST",
body: {
flow: route.query.flow,
},
})
emailVerified.value = true
success.value = true
});
emailVerified.value = true;
success.value = true;
}
if (emailVerified.value) {
success.value = true
success.value = true;
if (auth.value.token) {
await useAuth(auth.value.token)
await useAuth(auth.value.token);
}
}
} catch (err) {
success.value = false
success.value = false;
}
}
</script>

View File

@@ -36,60 +36,62 @@
</div>
</template>
<script setup>
import { Checkbox } from '@modrinth/ui'
import { RightArrowIcon } from '@modrinth/assets'
import { Checkbox } from "@modrinth/ui";
import { RightArrowIcon } from "@modrinth/assets";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
subscribeCheckbox: {
id: 'auth.welcome.checkbox.subscribe',
defaultMessage: 'Subscribe to updates about Modrinth',
id: "auth.welcome.checkbox.subscribe",
defaultMessage: "Subscribe to updates about Modrinth",
},
tosLabel: {
id: 'auth.welcome.label.tos',
id: "auth.welcome.label.tos",
defaultMessage:
"By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
},
welcomeDescription: {
id: 'auth.welcome.description',
id: "auth.welcome.description",
defaultMessage:
'Thank you for creating an account. You can now follow and create projects, receive updates about your favorite projects, and more!',
"Thank you for creating an account. You can now follow and create projects, receive updates about your favorite projects, and more!",
},
welcomeLongTitle: {
id: 'auth.welcome.long-title',
defaultMessage: 'Welcome to Modrinth!',
id: "auth.welcome.long-title",
defaultMessage: "Welcome to Modrinth!",
},
welcomeTitle: {
id: 'auth.welcome.title',
defaultMessage: 'Welcome',
id: "auth.welcome.title",
defaultMessage: "Welcome",
},
})
});
useHead({
title: () => `${formatMessage(messages.welcomeTitle)} - Modrinth`,
})
});
const subscribe = ref(true)
const subscribe = ref(true);
async function continueSignUp() {
const route = useNativeRoute()
const route = useNativeRoute();
await useAuth(route.query.authToken)
await useUser()
await useAuth(route.query.authToken);
await useUser();
if (subscribe.value) {
try {
await useBaseFetch('auth/email/subscribe', {
method: 'POST',
})
} catch {}
await useBaseFetch("auth/email/subscribe", {
method: "POST",
});
} catch {
/* empty */
}
}
if (route.query.redirect) {
await navigateTo(route.query.redirect)
await navigateTo(route.query.redirect);
} else {
await navigateTo('/dashboard')
await navigateTo("/dashboard");
}
}
</script>

View File

@@ -47,8 +47,8 @@
transparent
@click="
() => {
deletedIcon = true
previewImage = null
deletedIcon = true;
previewImage = null;
}
"
>
@@ -94,8 +94,8 @@
:multiple="false"
:display-name="
(s) => {
if (s === 'listed') return formatMessage(commonMessages.publicLabel)
return formatMessage(commonMessages[`${s}Label`])
if (s === 'listed') return formatMessage(commonMessages.publicLabel);
return formatMessage(commonMessages[`${s}Label`]);
}
"
:searchable="false"
@@ -262,19 +262,19 @@
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/collection/${collection.id}/${x}s`,
}
};
}),
]"
/>
<button
v-tooltip="
formatMessage(
commonMessages[`${cosmetics.searchDisplayMode.collection || 'list'}InputView`]
commonMessages[`${cosmetics.searchDisplayMode.collection || 'list'}InputView`],
)
"
:aria-label="
formatMessage(
commonMessages[`${cosmetics.searchDisplayMode.collection || 'list'}InputView`]
commonMessages[`${cosmetics.searchDisplayMode.collection || 'list'}InputView`],
)
"
class="square-button"
@@ -297,7 +297,7 @@
? projects.filter(
(x) =>
x.project_type ===
route.params.projectType.substr(0, route.params.projectType.length - 1)
route.params.projectType.substr(0, route.params.projectType.length - 1),
)
: projects
)
@@ -326,8 +326,8 @@
class="iconified-button remove-btn"
@click="
() => {
removeProjects = [project]
saveChanges()
removeProjects = [project];
saveChanges();
}
"
>
@@ -378,184 +378,184 @@ import {
UpdatedIcon,
LibraryIcon,
BoxIcon,
} from '@modrinth/assets'
import { PopoutMenu, FileInput, DropdownSelect, Promotion, Avatar, Button } from '@modrinth/ui'
} from "@modrinth/assets";
import { PopoutMenu, FileInput, DropdownSelect, Promotion, Avatar, Button } from "@modrinth/ui";
import WorldIcon from 'assets/images/utils/world.svg'
import UpToDate from 'assets/images/illustrations/up_to_date.svg'
import { addNotification } from '~/composables/notifs.js'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import NavRow from '~/components/ui/NavRow.vue'
import ProjectCard from '~/components/ui/ProjectCard.vue'
import WorldIcon from "assets/images/utils/world.svg";
import UpToDate from "assets/images/illustrations/up_to_date.svg";
import { addNotification } from "~/composables/notifs.js";
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
const vintl = useVIntl()
const { formatMessage } = vintl
const formatRelativeTime = useRelativeTime()
const formatCompactNumber = useCompactNumber()
const vintl = useVIntl();
const { formatMessage } = vintl;
const formatRelativeTime = useRelativeTime();
const formatCompactNumber = useCompactNumber();
const messages = defineMessages({
collectionDescription: {
id: 'collection.description',
defaultMessage: '{description} - View the collection {name} by {username} on Modrinth',
id: "collection.description",
defaultMessage: "{description} - View the collection {name} by {username} on Modrinth",
},
collectionLabel: {
id: 'collection.label.collection',
defaultMessage: 'Collection',
id: "collection.label.collection",
defaultMessage: "Collection",
},
collectionTitle: {
id: 'collection.title',
defaultMessage: '{name} - Collection',
id: "collection.title",
defaultMessage: "{name} - Collection",
},
editIconButton: {
id: 'collection.button.edit-icon',
defaultMessage: 'Edit icon',
id: "collection.button.edit-icon",
defaultMessage: "Edit icon",
},
deleteIconButton: {
id: 'collection.button.delete-icon',
defaultMessage: 'Delete icon',
id: "collection.button.delete-icon",
defaultMessage: "Delete icon",
},
createdAtLabel: {
id: 'collection.label.created-at',
defaultMessage: 'Created {ago}',
id: "collection.label.created-at",
defaultMessage: "Created {ago}",
},
collectionNotFoundError: {
id: 'collection.error.not-found',
defaultMessage: 'Collection not found',
id: "collection.error.not-found",
defaultMessage: "Collection not found",
},
curatedByLabel: {
id: 'collection.label.curated-by',
defaultMessage: 'Curated by',
id: "collection.label.curated-by",
defaultMessage: "Curated by",
},
deleteModalDescription: {
id: 'collection.delete-modal.description',
defaultMessage: 'This will remove this collection forever. This action cannot be undone.',
id: "collection.delete-modal.description",
defaultMessage: "This will remove this collection forever. This action cannot be undone.",
},
deleteModalTitle: {
id: 'collection.delete-modal.title',
defaultMessage: 'Are you sure you want to delete this collection?',
id: "collection.delete-modal.title",
defaultMessage: "Are you sure you want to delete this collection?",
},
followingCollectionDescription: {
id: 'collection.description.following',
id: "collection.description.following",
defaultMessage: "Auto-generated collection of all the projects you're following.",
},
noProjectsLabel: {
id: 'collection.label.no-projects',
defaultMessage: 'This collection has no projects!',
id: "collection.label.no-projects",
defaultMessage: "This collection has no projects!",
},
noProjectsAuthLabel: {
id: 'collection.label.no-projects-auth',
id: "collection.label.no-projects-auth",
defaultMessage:
"You don't have any projects.\nWould you like to <create-link>add one</create-link>?",
},
ownerLabel: {
id: 'collection.label.owner',
defaultMessage: 'Owner',
id: "collection.label.owner",
defaultMessage: "Owner",
},
projectsCountLabel: {
id: 'collection.label.projects-count',
id: "collection.label.projects-count",
defaultMessage:
'{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}',
"{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}",
},
removeProjectButton: {
id: 'collection.button.remove-project',
defaultMessage: 'Remove project',
id: "collection.button.remove-project",
defaultMessage: "Remove project",
},
unfollowProjectButton: {
id: 'collection.button.unfollow-project',
defaultMessage: 'Unfollow project',
id: "collection.button.unfollow-project",
defaultMessage: "Unfollow project",
},
updatedAtLabel: {
id: 'collection.label.updated-at',
defaultMessage: 'Updated {ago}',
id: "collection.label.updated-at",
defaultMessage: "Updated {ago}",
},
uploadIconButton: {
id: 'collection.button.upload-icon',
defaultMessage: 'Upload icon',
id: "collection.button.upload-icon",
defaultMessage: "Upload icon",
},
})
});
const data = useNuxtApp()
const route = useNativeRoute()
const auth = await useAuth()
const cosmetics = useCosmetics()
const tags = useTags()
const data = useNuxtApp();
const route = useNativeRoute();
const auth = await useAuth();
const cosmetics = useCosmetics();
const tags = useTags();
const isEditing = ref(false)
const isEditing = ref(false);
function cycleSearchDisplayMode() {
cosmetics.value.searchDisplayMode.collection = data.$cycleValue(
cosmetics.value.searchDisplayMode.collection,
tags.value.projectViewModes
)
saveCosmetics()
tags.value.projectViewModes,
);
saveCosmetics();
}
let collection, refreshCollection, creator, projects, refreshProjects
let collection, refreshCollection, creator, projects, refreshProjects;
try {
if (route.params.id === 'following') {
if (route.params.id === "following") {
collection = ref({
id: 'following',
icon_url: 'https://cdn.modrinth.com/follow-collection.png',
id: "following",
icon_url: "https://cdn.modrinth.com/follow-collection.png",
name: formatMessage(commonMessages.followedProjectsLabel),
description: formatMessage(messages.followingCollectionDescription),
status: 'private',
status: "private",
user: auth.value.user.id,
created: auth.value.user.created,
updated: auth.value.user.created,
})
;[{ data: projects, refresh: refreshProjects }] = await Promise.all([
});
[{ data: projects, refresh: refreshProjects }] = await Promise.all([
useAsyncData(
`user/${auth.value.user.id}/follows`,
() => useBaseFetch(`user/${auth.value.user.id}/follows`),
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.categories = project.categories.concat(project.loaders);
}
return projects
return projects;
},
}
},
),
])
creator = ref(auth.value.user)
refreshCollection = async () => {}
]);
creator = ref(auth.value.user);
refreshCollection = async () => {};
} else {
const val = await useAsyncData(`collection/${route.params.id}`, () =>
useBaseFetch(`collection/${route.params.id}`, { apiVersion: 3 })
)
collection = val.data
refreshCollection = val.refresh
;[{ data: creator }, { data: projects, refresh: refreshProjects }] = await Promise.all([
useBaseFetch(`collection/${route.params.id}`, { apiVersion: 3 }),
);
collection = val.data;
refreshCollection = val.refresh;
[{ data: creator }, { data: projects, refresh: refreshProjects }] = await Promise.all([
await useAsyncData(`user/${collection.value.user}`, () =>
useBaseFetch(`user/${collection.value.user}`)
useBaseFetch(`user/${collection.value.user}`),
),
await useAsyncData(
`projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}]`,
() =>
useBaseFetch(
`projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`
`projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`,
),
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.categories = project.categories.concat(project.loaders);
}
return projects
return projects;
},
}
},
),
])
]);
}
} catch (err) {
console.error(err)
console.error(err);
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.collectionNotFoundError),
})
});
}
if (!collection.value) {
@@ -563,12 +563,12 @@ if (!collection.value) {
fatal: true,
statusCode: 404,
message: formatMessage(messages.collectionNotFoundError),
})
});
}
const title = computed(() =>
formatMessage(messages.collectionTitle, { name: collection.value.name })
)
formatMessage(messages.collectionTitle, { name: collection.value.name }),
);
useSeoMeta({
title,
@@ -580,65 +580,65 @@ useSeoMeta({
}),
ogTitle: title,
ogDescription: collection.value.description,
ogImage: collection.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
robots: collection.value.status === 'listed' ? 'all' : 'noindex',
})
ogImage: collection.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
robots: collection.value.status === "listed" ? "all" : "noindex",
});
const canEdit = computed(
() =>
auth.value.user &&
auth.value.user.id === collection.value.user &&
collection.value.id !== 'following'
)
collection.value.id !== "following",
);
const projectTypes = computed(() => {
const projectSet = new Set(
projects.value?.map((project) => project?.project_type).filter((x) => x !== undefined) || []
)
projectSet.delete('project')
return Array.from(projectSet)
})
projects.value?.map((project) => project?.project_type).filter((x) => x !== undefined) || [],
);
projectSet.delete("project");
return Array.from(projectSet);
});
const icon = ref(null)
const deletedIcon = ref(false)
const previewImage = ref(null)
const icon = ref(null);
const deletedIcon = ref(false);
const previewImage = ref(null);
const name = ref(collection.value.name)
const summary = ref(collection.value.description)
const visibility = ref(collection.value.status)
const removeProjects = ref([])
const name = ref(collection.value.name);
const summary = ref(collection.value.description);
const visibility = ref(collection.value.status);
const removeProjects = ref([]);
async function unfollowProject(project) {
await userUnfollowProject(project)
projects.value = projects.value.filter((x) => x.id !== project.id)
await userUnfollowProject(project);
projects.value = projects.value.filter((x) => x.id !== project.id);
}
async function saveChanges() {
startLoading()
startLoading();
try {
if (deletedIcon.value) {
await useBaseFetch(`collection/${collection.value.id}/icon`, {
method: 'DELETE',
method: "DELETE",
apiVersion: 3,
})
});
} else if (icon.value) {
const ext = icon.value?.type?.split('/').pop()
if (!ext) throw new Error('Invalid file type')
const ext = icon.value?.type?.split("/").pop();
if (!ext) throw new Error("Invalid file type");
await useBaseFetch(`collection/${collection.value.id}/icon?ext=${ext}`, {
method: 'PATCH',
method: "PATCH",
body: icon.value,
apiVersion: 3,
})
});
}
const projectsToRemove = removeProjects.value?.map((p) => p.id) ?? []
const projectsToRemove = removeProjects.value?.map((p) => p.id) ?? [];
const newProjects = projects.value
.filter((p) => !projectsToRemove.includes(p.id))
.map((p) => p.id)
const newProjectIds = projectsToRemove.length > 0 ? newProjects : undefined
.map((p) => p.id);
const newProjectIds = projectsToRemove.length > 0 ? newProjects : undefined;
await useBaseFetch(`collection/${collection.value.id}`, {
method: 'PATCH',
method: "PATCH",
body: {
name: name.value,
description: summary.value,
@@ -646,57 +646,57 @@ async function saveChanges() {
new_projects: newProjectIds,
},
apiVersion: 3,
})
});
await refreshCollection()
await refreshProjects()
await refreshCollection();
await refreshProjects();
name.value = collection.value.name
summary.value = collection.value.description
visibility.value = collection.value.status
removeProjects.value = []
name.value = collection.value.name;
summary.value = collection.value.description;
visibility.value = collection.value.status;
removeProjects.value = [];
isEditing.value = false
isEditing.value = false;
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err,
type: 'error',
})
type: "error",
});
}
await initUserCollections()
stopLoading()
await initUserCollections();
stopLoading();
}
async function deleteCollection() {
startLoading()
startLoading();
try {
await useBaseFetch(`collection/${collection.value.id}`, {
method: 'DELETE',
method: "DELETE",
apiVersion: 3,
})
await navigateTo('/dashboard/collections')
});
await navigateTo("/dashboard/collections");
} catch (err) {
addNotification({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data.description,
type: 'error',
})
type: "error",
});
}
await initUserCollections()
stopLoading()
await initUserCollections();
stopLoading();
}
function showPreviewImage(files) {
const reader = new FileReader()
icon.value = files[0]
deletedIcon.value = false
reader.readAsDataURL(icon.value)
const reader = new FileReader();
icon.value = files[0];
deletedIcon.value = false;
reader.readAsDataURL(icon.value);
reader.onload = (event) => {
previewImage.value = event.target.result
}
previewImage.value = event.target.result;
};
}
</script>

View File

@@ -42,22 +42,22 @@
</div>
</template>
<script setup>
import { LibraryIcon, ChartIcon } from '@modrinth/assets'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import { LibraryIcon, ChartIcon } from "@modrinth/assets";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import DashboardIcon from '~/assets/images/utils/dashboard.svg?component'
import CurrencyIcon from '~/assets/images/utils/currency.svg?component'
import ListIcon from '~/assets/images/utils/list.svg?component'
import ReportIcon from '~/assets/images/utils/report.svg?component'
import NotificationsIcon from '~/assets/images/utils/bell.svg?component'
import OrganizationIcon from '~/assets/images/utils/organization.svg?component'
import DashboardIcon from "~/assets/images/utils/dashboard.svg?component";
import CurrencyIcon from "~/assets/images/utils/currency.svg?component";
import ListIcon from "~/assets/images/utils/list.svg?component";
import ReportIcon from "~/assets/images/utils/report.svg?component";
import NotificationsIcon from "~/assets/images/utils/bell.svg?component";
import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const route = useNativeRoute()
const route = useNativeRoute();
</script>

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>

View File

@@ -3,9 +3,9 @@ import {
type FeatureFlag,
DEFAULT_FEATURE_FLAGS,
saveFeatureFlags,
} from '~/composables/featureFlags.ts'
} from "~/composables/featureFlags.ts";
const flags = shallowReactive(useFeatureFlags().value)
const flags = shallowReactive(useFeatureFlags().value);
</script>
<template>
@@ -19,7 +19,7 @@ const flags = shallowReactive(useFeatureFlags().value)
>
<label :for="`toggle-${flag}`">
<span class="label__title">
{{ flag.replaceAll('_', ' ') }}
{{ flag.replaceAll("_", " ") }}
</span>
<span class="label__description">
<p>

View File

@@ -1,50 +1,50 @@
<script setup lang="ts">
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
const messages = defineMessages({
frogTitle: {
id: 'frog.title',
defaultMessage: 'Frog',
id: "frog.title",
defaultMessage: "Frog",
},
frogDescription: {
id: 'frog',
id: "frog",
defaultMessage: "You've been frogged! 🐸",
},
frogAltText: {
id: 'frog.altText',
defaultMessage: 'A photorealistic painting of a frog labyrinth',
id: "frog.altText",
defaultMessage: "A photorealistic painting of a frog labyrinth",
},
frogSinceOpened: {
id: 'frog.sinceOpened',
defaultMessage: 'This page was opened {ago}',
id: "frog.sinceOpened",
defaultMessage: "This page was opened {ago}",
},
frogFroggedPeople: {
id: 'frog.froggedPeople',
id: "frog.froggedPeople",
defaultMessage:
'{count, plural, one {{count} more person} other {{count} more people}} were also frogged!',
"{count, plural, one {{count} more person} other {{count} more people}} were also frogged!",
},
})
});
const formatCompactNumber = useCompactNumber()
const formatCompactNumber = useCompactNumber();
const formatRelativeTime = useRelativeTime()
const formatRelativeTime = useRelativeTime();
const pageOpen = useState('frogPageOpen', () => Date.now())
const peopleFrogged = useState('frogPeopleFrogged', () => Math.round(Math.random() * 100_000_000))
const peopleFroggedCount = computed(() => formatCompactNumber(peopleFrogged.value))
const pageOpen = useState("frogPageOpen", () => Date.now());
const peopleFrogged = useState("frogPeopleFrogged", () => Math.round(Math.random() * 100_000_000));
const peopleFroggedCount = computed(() => formatCompactNumber(peopleFrogged.value));
let interval: ReturnType<typeof setTimeout>
let interval: ReturnType<typeof setTimeout>;
const formattedOpenedCounter = ref(formatRelativeTime(Date.now()))
const formattedOpenedCounter = ref(formatRelativeTime(Date.now()));
onMounted(() => {
interval = setInterval(() => {
formattedOpenedCounter.value = formatRelativeTime(pageOpen.value)
}, 1000)
})
formattedOpenedCounter.value = formatRelativeTime(pageOpen.value);
}, 1000);
});
onUnmounted(() => clearInterval(interval))
onUnmounted(() => clearInterval(interval));
</script>
<template>

View File

@@ -163,12 +163,12 @@
{{ notification.title }} has been updated!
</nuxt-link>
<p class="notif-desc">
Version {{ ['1.1.2', '1.0.3', '15.1'][index] }} has been released for
Version {{ ["1.1.2", "1.0.3", "15.1"][index] }} has been released for
{{
$capitalizeString(
notification.display_categories[
notification.display_categories.length - 1
]
],
)
}}
{{ notification.versions[notification.versions.length - 1] }}
@@ -504,43 +504,43 @@
</div>
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import SearchIcon from '~/assets/images/utils/search.svg?component'
import CalendarIcon from '~/assets/images/utils/calendar.svg?component'
import ModrinthIcon from '~/assets/images/logo.svg?component'
import PrismLauncherLogo from '~/assets/images/external/prism.svg?component'
import ATLauncherLogo from '~/assets/images/external/atlauncher.svg?component'
import Avatar from '~/components/ui/Avatar.vue'
import ProjectCard from '~/components/ui/ProjectCard.vue'
import { Multiselect } from "vue-multiselect";
import SearchIcon from "~/assets/images/utils/search.svg?component";
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import ModrinthIcon from "~/assets/images/logo.svg?component";
import PrismLauncherLogo from "~/assets/images/external/prism.svg?component";
import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component";
import Avatar from "~/components/ui/Avatar.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
const searchQuery = ref('better')
const sortType = ref('relevance')
const searchQuery = ref("better");
const sortType = ref("relevance");
const auth = await useAuth()
const tags = useTags()
const auth = await useAuth();
const tags = useTags();
const [
{ data: rows },
{ data: searchProjects, refresh: updateSearchProjects },
{ data: notifications },
] = await Promise.all([
useAsyncData('projects', () => useBaseFetch('projects_random?count=40'), {
useAsyncData("projects", () => useBaseFetch("projects_random?count=40"), {
transform: (result) => {
const val = Math.ceil(result.length / 3)
return [result.slice(0, val), result.slice(val, val * 2), result.slice(val * 2, val * 3)]
const val = Math.ceil(result.length / 3);
return [result.slice(0, val), result.slice(val, val * 2), result.slice(val * 2, val * 3)];
},
}),
useAsyncData(
'demoSearchProjects',
"demoSearchProjects",
() => useBaseFetch(`search?limit=3&query=${searchQuery.value}&index=${sortType.value}`),
{
transform: (result) => result.hits,
}
},
),
useAsyncData('updatedProjects', () => useBaseFetch(`search?limit=3&query=&index=updated`), {
useAsyncData("updatedProjects", () => useBaseFetch(`search?limit=3&query=&index=updated`), {
transform: (result) => result.hits,
}),
])
]);
</script>
<style lang="scss" scoped>
@@ -592,7 +592,7 @@ const [
width: 100%;
&:before {
content: '';
content: "";
position: absolute;
z-index: 1;
inset: 0;
@@ -683,7 +683,9 @@ const [
gap: 1rem;
border-radius: 1rem;
border: 1px solid var(--landing-border-color);
transition: background 0.5s ease-in-out, transform 0.05s ease-in-out;
transition:
background 0.5s ease-in-out,
transform 0.05s ease-in-out;
// Removed due to lag on mobile :(
&:hover {
@@ -793,7 +795,7 @@ const [
z-index: 1;
&:after {
content: '';
content: "";
position: absolute;
z-index: -1;
inset: 0 0 -0.75rem -0.75rem;
@@ -824,7 +826,9 @@ const [
}
input {
box-shadow: inset 0 0 0 transparent, 0 0 0 0.25rem var(--color-brand-shadow);
box-shadow:
inset 0 0 0 transparent,
0 0 0 0.25rem var(--color-brand-shadow);
color: var(--color-button-text-active);
}
}
@@ -990,7 +994,9 @@ const [
width: 4rem;
height: 4rem;
background: #020305;
box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 32px #393d5e;
box-shadow:
2px 2px 12px rgba(0, 0, 0, 0.16),
inset 2px 2px 32px #393d5e;
border-radius: 1rem;
svg {
@@ -1063,7 +1069,7 @@ const [
border-radius: 1rem;
&:before {
content: '';
content: "";
position: absolute;
inset: 0;
padding: 1px;
@@ -1071,8 +1077,12 @@ const [
border-radius: 1rem;
background: var(--landing-border-gradient);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}

View File

@@ -50,11 +50,11 @@ import {
ShieldIcon,
CurrencyIcon,
CopyrightIcon,
} from '@modrinth/assets'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
} from "@modrinth/assets";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
const route = useNativeRoute()
const route = useNativeRoute();
</script>
<style lang="scss" scoped>

View File

@@ -453,12 +453,12 @@
<script setup>
const description =
'The California Privacy Notice of Modrinth, an open source modding platform focused on Minecraft.'
"The California Privacy Notice of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'California Privacy Notice - Modrinth',
title: "California Privacy Notice - Modrinth",
description,
ogTitle: 'California Privacy Notice',
ogTitle: "California Privacy Notice",
ogDescription: description,
})
});
</script>

View File

@@ -90,12 +90,12 @@
<script setup>
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.";
useSeoMeta({
title: 'Rewards Program Information - Modrinth',
title: "Rewards Program Information - Modrinth",
description,
ogTitle: 'Rewards Program Information',
ogTitle: "Rewards Program Information",
ogDescription: description,
})
});
</script>

View File

@@ -75,12 +75,12 @@
<script setup>
const description =
'The Rewards Program Terms of Modrinth, an open source modding platform focused on Minecraft.'
"The Rewards Program Terms of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Rewards Program Terms - Modrinth',
title: "Rewards Program Terms - Modrinth",
description,
ogTitle: 'Rewards Program Terms',
ogTitle: "Rewards Program Terms",
ogDescription: description,
})
});
</script>

View File

@@ -103,12 +103,12 @@
<script setup>
const description =
'The Copyright Policy of Modrinth, an open source modding platform focused on Minecraft.'
"The Copyright Policy of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Copyright Policy - Modrinth',
title: "Copyright Policy - Modrinth",
description,
ogTitle: 'Copyright Policy',
ogTitle: "Copyright Policy",
ogDescription: description,
})
});
</script>

View File

@@ -310,12 +310,12 @@
<script setup>
const description =
'The Privacy Policy of Modrinth, an open source modding platform focused on Minecraft.'
"The Privacy Policy of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Privacy Policy - Modrinth',
title: "Privacy Policy - Modrinth",
description,
ogTitle: 'Privacy Policy',
ogTitle: "Privacy Policy",
ogDescription: description,
})
});
</script>

View File

@@ -177,12 +177,12 @@
<script setup>
const description =
'The Content Rules of Modrinth, an open source modding platform focused on Minecraft.'
"The Content Rules of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Content Rules - Modrinth',
title: "Content Rules - Modrinth",
description,
ogTitle: 'Content Rules',
ogTitle: "Content Rules",
ogDescription: description,
})
});
</script>

View File

@@ -54,12 +54,12 @@
<script setup>
const description =
'The Security Notice of Modrinth, an open source modding platform focused on Minecraft.'
"The Security Notice of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Security Notice - Modrinth',
title: "Security Notice - Modrinth",
description,
ogTitle: 'Security Notice',
ogTitle: "Security Notice",
ogDescription: description,
})
});
</script>

View File

@@ -548,12 +548,12 @@
<script setup>
const description =
'The Terms of Use of Modrinth, an open source modding platform focused on Minecraft.'
"The Terms of Use of Modrinth, an open source modding platform focused on Minecraft.";
useSeoMeta({
title: 'Terms of Use - Modrinth',
title: "Terms of Use - Modrinth",
description,
ogTitle: 'Terms of Use',
ogTitle: "Terms of Use",
ogDescription: description,
})
});
</script>

View File

@@ -22,14 +22,14 @@
</div>
</template>
<script setup>
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import ModrinthIcon from '~/assets/images/utils/modrinth.svg?component'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component'
import ReportIcon from '~/assets/images/utils/report.svg?component'
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import ReportIcon from "~/assets/images/utils/report.svg?component";
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
</script>

View File

@@ -32,11 +32,11 @@
</div>
</template>
<script setup>
import { formatNumber } from '~/plugins/shorthands.js'
import { formatNumber } from "~/plugins/shorthands.js";
useHead({
title: 'Staff overview - Modrinth',
})
title: "Staff overview - Modrinth",
});
const { data: stats } = await useAsyncData('statistics', () => useBaseFetch('statistics'))
const { data: stats } = await useAsyncData("statistics", () => useBaseFetch("statistics"));
</script>

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 auth = await useAuth()
const route = useNativeRoute()
const auth = await useAuth();
const route = useNativeRoute();
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: 'Reports - Modrinth',
})
title: "Reports - Modrinth",
});
</script>

View File

@@ -37,9 +37,9 @@
<div
v-for="project in projectsFiltered.sort((a, b) => {
if (oldestFirst) {
return b.age - a.age
return b.age - a.age;
} else {
return a.age - b.age
return a.age - b.age;
}
})"
:key="`project-${project.id}`"
@@ -101,105 +101,105 @@
</section>
</template>
<script setup>
import Chips from '~/components/ui/Chips.vue'
import Avatar from '~/components/ui/Avatar.vue'
import UnknownIcon from '~/assets/images/utils/unknown.svg?component'
import EyeIcon from '~/assets/images/utils/eye.svg?component'
import SortAscIcon from '~/assets/images/utils/sort-asc.svg?component'
import SortDescIcon from '~/assets/images/utils/sort-desc.svg?component'
import WarningIcon from '~/assets/images/utils/issues.svg?component'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component'
import Badge from '~/components/ui/Badge.vue'
import { formatProjectType } from '~/plugins/shorthands.js'
import Chips from "~/components/ui/Chips.vue";
import Avatar from "~/components/ui/Avatar.vue";
import UnknownIcon from "~/assets/images/utils/unknown.svg?component";
import EyeIcon from "~/assets/images/utils/eye.svg?component";
import SortAscIcon from "~/assets/images/utils/sort-asc.svg?component";
import SortDescIcon from "~/assets/images/utils/sort-desc.svg?component";
import WarningIcon from "~/assets/images/utils/issues.svg?component";
import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import Badge from "~/components/ui/Badge.vue";
import { formatProjectType } from "~/plugins/shorthands.js";
useHead({
title: 'Review projects - Modrinth',
})
title: "Review projects - Modrinth",
});
const app = useNuxtApp()
const app = useNuxtApp();
const router = useRouter()
const router = useRouter();
const now = app.$dayjs()
const TIME_24H = 86400000
const TIME_48H = TIME_24H * 2
const now = app.$dayjs();
const TIME_24H = 86400000;
const TIME_48H = TIME_24H * 2;
const { data: projects } = await useAsyncData('moderation/projects?count=1000', () =>
useBaseFetch('moderation/projects?count=1000', { internal: true })
)
const members = ref([])
const projectType = ref('all')
const oldestFirst = ref(true)
const { data: projects } = await useAsyncData("moderation/projects?count=1000", () =>
useBaseFetch("moderation/projects?count=1000", { internal: true }),
);
const members = ref([]);
const projectType = ref("all");
const oldestFirst = ref(true);
const projectsFiltered = computed(() =>
projects.value.filter(
(x) =>
projectType.value === 'all' ||
app.$getProjectTypeForUrl(x.project_types[0], x.loaders) === projectType.value
)
)
projectType.value === "all" ||
app.$getProjectTypeForUrl(x.project_types[0], x.loaders) === projectType.value,
),
);
const projectsOver24Hours = computed(() =>
projectsFiltered.value.filter((project) => project.age >= TIME_24H && project.age < TIME_48H)
)
projectsFiltered.value.filter((project) => project.age >= TIME_24H && project.age < TIME_48H),
);
const projectsOver48Hours = computed(() =>
projectsFiltered.value.filter((project) => project.age >= TIME_48H)
)
projectsFiltered.value.filter((project) => project.age >= TIME_48H),
);
const projectTypePlural = computed(() =>
projectType.value === 'all'
? 'projects'
: (formatProjectType(projectType.value) + 's').toLowerCase()
)
projectType.value === "all"
? "projects"
: (formatProjectType(projectType.value) + "s").toLowerCase(),
);
const projectTypes = computed(() => {
const set = new Set()
set.add('all')
const set = new Set();
set.add("all");
if (projects.value) {
for (const project of projects.value) {
set.add(project.inferred_project_type)
set.add(project.inferred_project_type);
}
}
return [...set]
})
return [...set];
});
if (projects.value) {
const teamIds = projects.value.map((x) => x.team_id)
const organizationIds = projects.value.filter((x) => x.organization).map((x) => x.organization)
const teamIds = projects.value.map((x) => x.team_id);
const organizationIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
const url = `teams?ids=${encodeURIComponent(JSON.stringify(teamIds))}`
const orgUrl = `organizations?ids=${encodeURIComponent(JSON.stringify(organizationIds))}`
const { data: result } = await useAsyncData(url, () => useBaseFetch(url))
const { data: orgs } = await useAsyncData(orgUrl, () => useBaseFetch(orgUrl, { apiVersion: 3 }))
const url = `teams?ids=${encodeURIComponent(JSON.stringify(teamIds))}`;
const orgUrl = `organizations?ids=${encodeURIComponent(JSON.stringify(organizationIds))}`;
const { data: result } = await useAsyncData(url, () => useBaseFetch(url));
const { data: orgs } = await useAsyncData(orgUrl, () => useBaseFetch(orgUrl, { apiVersion: 3 }));
if (result.value) {
members.value = result.value
members.value = result.value;
projects.value = projects.value.map((project) => {
project.owner = members.value
.flat()
.find((x) => x.team_id === project.team_id && x.role === 'Owner')
project.org = orgs.value.find((x) => x.id === project.organization)
project.age = project.queued ? now - app.$dayjs(project.queued) : Number.MAX_VALUE
project.age_warning = ''
.find((x) => x.team_id === project.team_id && x.role === "Owner");
project.org = orgs.value.find((x) => x.id === project.organization);
project.age = project.queued ? now - app.$dayjs(project.queued) : Number.MAX_VALUE;
project.age_warning = "";
if (project.age > TIME_24H * 2) {
project.age_warning = 'danger'
project.age_warning = "danger";
} else if (project.age > TIME_24H) {
project.age_warning = 'warning'
project.age_warning = "warning";
}
project.inferred_project_type = app.$getProjectTypeForUrl(
project.project_types[0],
project.loaders
)
return project
})
project.loaders,
);
return project;
});
}
}
async function goToProjects() {
const project = projectsFiltered.value[0]
const project = projectsFiltered.value[0];
await router.push({
name: 'type-id',
name: "type-id",
params: {
type: project.project_types[0],
id: project.slug ? project.slug : project.id,
@@ -208,7 +208,7 @@ async function goToProjects() {
showChecklist: true,
projects: projectsFiltered.value.slice(1).map((x) => (x.slug ? x.slug : x.id)),
},
})
});
}
</script>
<style lang="scss" scoped>
@@ -218,7 +218,7 @@ async function goToProjects() {
gap: var(--spacing-card-sm);
@media screen and (min-width: 650px) {
display: grid;
grid-template: 'title action' 'date action';
grid-template: "title action" "date action";
grid-template-columns: 1fr auto;
}
}

View File

@@ -153,7 +153,7 @@
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.slug}/${x}s`,
}
};
}),
]"
/>
@@ -170,8 +170,8 @@
v-for="project in (route.params.projectType !== undefined
? projects.filter((x) =>
x.project_types.includes(
route.params.projectType.substr(0, route.params.projectType.length - 1)
)
route.params.projectType.substr(0, route.params.projectType.length - 1),
),
)
: projects
)
@@ -227,42 +227,42 @@ import {
ChartIcon,
CheckIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, Breadcrumbs, Promotion } from '@modrinth/ui'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import NavRow from '~/components/ui/NavRow.vue'
import ModalCreation from '~/components/ui/ModalCreation.vue'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import ProjectCard from '~/components/ui/ProjectCard.vue'
} from "@modrinth/assets";
import { Avatar, Breadcrumbs, Promotion } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import OrganizationIcon from '~/assets/images/utils/organization.svg?component'
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import CrownIcon from '~/assets/images/utils/crown.svg?component'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
const formatCompactNumber = useCompactNumber()
const formatCompactNumber = useCompactNumber();
const auth = await useAuth()
const user = await useUser()
const cosmetics = useCosmetics()
const route = useNativeRoute()
const tags = useTags()
const auth = await useAuth();
const user = await useUser();
const cosmetics = useCosmetics();
const route = useNativeRoute();
const tags = useTags();
let orgId = useRouteId()
let orgId = useRouteId();
// hacky way to show the edit button on the corner of the card.
const routeHasSettings = computed(() => route.path.includes('settings'))
const routeHasSettings = computed(() => route.path.includes("settings"));
const [
{ data: organization, refresh: refreshOrganization },
{ data: projects, refresh: refreshProjects },
] = await Promise.all([
useAsyncData(`organization/${orgId}`, () =>
useBaseFetch(`organization/${orgId}`, { apiVersion: 3 })
useBaseFetch(`organization/${orgId}`, { apiVersion: 3 }),
),
useAsyncData(
`organization/${orgId}/projects`,
@@ -270,162 +270,162 @@ const [
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.categories = project.categories.concat(project.loaders);
if (project.mrpack_loaders) {
project.categories = project.categories.concat(project.mrpack_loaders)
project.categories = project.categories.concat(project.mrpack_loaders);
}
const singleplayer = project.singleplayer && project.singleplayer[0]
const clientAndServer = project.client_and_server && project.client_and_server[0]
const clientOnly = project.client_only && project.client_only[0]
const serverOnly = project.server_only && project.server_only[0]
const singleplayer = project.singleplayer && project.singleplayer[0];
const clientAndServer = project.client_and_server && project.client_and_server[0];
const clientOnly = project.client_only && project.client_only[0];
const serverOnly = project.server_only && project.server_only[0];
// quick and dirty hack to show envs as legacy
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
project.client_side = 'required'
project.server_side = 'required'
project.client_side = "required";
project.server_side = "required";
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
project.client_side = 'required'
project.server_side = 'unsupported'
project.client_side = "required";
project.server_side = "unsupported";
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
project.client_side = 'unsupported'
project.server_side = 'required'
project.client_side = "unsupported";
project.server_side = "required";
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
project.client_side = 'optional'
project.server_side = 'optional'
project.client_side = "optional";
project.server_side = "optional";
}
}
return projects
return projects;
},
}
},
),
])
]);
const refresh = async () => {
await Promise.all([refreshOrganization(), refreshProjects()])
}
await Promise.all([refreshOrganization(), refreshProjects()]);
};
if (!organization.value) {
throw createError({
fatal: true,
statusCode: 404,
message: 'Organization not found',
})
message: "Organization not found",
});
}
// Filter accepted, sort by role, then by name and Owner role always goes first
const acceptedMembers = computed(() => {
const acceptedMembers = organization.value.members?.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
const acceptedMembers = organization.value.members?.filter((x) => x.accepted);
const owner = acceptedMembers.find((x) => x.is_owner);
const rest = acceptedMembers.filter((x) => !x.is_owner) || [];
rest.sort((a, b) => {
if (a.role === b.role) {
return a.user.username.localeCompare(b.user.username)
return a.user.username.localeCompare(b.user.username);
} else {
return a.role.localeCompare(b.role)
return a.role.localeCompare(b.role);
}
})
});
return [owner, ...rest]
})
return [owner, ...rest];
});
const currentMember = computed(() => {
if (auth.value.user && organization.value) {
const member = organization.value.members.find((x) => x.user.id === auth.value.user.id)
const member = organization.value.members.find((x) => x.user.id === auth.value.user.id);
if (member) {
return member
return member;
}
if (tags.value.staffRoles.includes(auth.value.user.role)) {
return {
user: auth.value.user,
role: auth.value.user.role,
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
permissions: auth.value.user.role === "admin" ? 1023 : 12,
accepted: true,
payouts_split: 0,
avatar_url: auth.value.user.avatar_url,
name: auth.value.user.username,
}
};
}
}
return null
})
return null;
});
const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2
return currentMember.value && (currentMember.value.permissions & EDIT_DETAILS) === EDIT_DETAILS
})
const EDIT_DETAILS = 1 << 2;
return currentMember.value && (currentMember.value.permissions & EDIT_DETAILS) === EDIT_DETAILS;
});
const isInvited = computed(() => {
return currentMember.value?.accepted === false
})
return currentMember.value?.accepted === false;
});
const projectTypes = computed(() => {
const obj = {}
const obj = {};
for (const project of projects.value) {
obj[project.project_types[0] ?? 'project'] = true
obj[project.project_types[0] ?? "project"] = true;
}
delete obj.project
delete obj.project;
return Object.keys(obj)
})
return Object.keys(obj);
});
const sumDownloads = computed(() => {
let sum = 0
let sum = 0;
for (const project of projects.value) {
sum += project.downloads
sum += project.downloads;
}
return sum
})
return sum;
});
const patchIcon = async (icon) => {
const ext = icon.name.split('.').pop()
const ext = icon.name.split(".").pop();
await useBaseFetch(`organization/${organization.value.id}/icon`, {
method: 'PATCH',
method: "PATCH",
body: icon,
query: { ext },
apiVersion: 3,
})
}
});
};
const deleteIcon = async () => {
await useBaseFetch(`organization/${organization.value.id}/icon`, {
method: 'DELETE',
method: "DELETE",
apiVersion: 3,
})
}
});
};
const patchOrganization = async (id, newData) => {
await useBaseFetch(`organization/${id}`, {
method: 'PATCH',
method: "PATCH",
body: newData,
apiVersion: 3,
})
});
if (newData.slug) {
orgId = newData.slug
orgId = newData.slug;
}
}
};
const onAcceptInvite = useClientTry(async () => {
await acceptTeamInvite(organization.value.team_id)
await refreshOrganization()
})
await acceptTeamInvite(organization.value.team_id);
await refreshOrganization();
});
const onDeclineInvite = useClientTry(async () => {
await removeTeamMember(organization.value.team_id, auth.value?.user.id)
await refreshOrganization()
})
await removeTeamMember(organization.value.team_id, auth.value?.user.id);
await refreshOrganization();
});
provide('organizationContext', {
provide("organizationContext", {
organization,
projects,
refresh,
@@ -434,18 +434,18 @@ provide('organizationContext', {
patchIcon,
deleteIcon,
patchOrganization,
})
});
const title = `${organization.value.name} - Organization`
const description = `${organization.value.description} - View the organization ${organization.value.name} on Modrinth`
const title = `${organization.value.name} - Organization`;
const description = `${organization.value.description} - View the organization ${organization.value.name} on Modrinth`;
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: organization.value.description,
ogImage: organization.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
})
ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
});
</script>
<style scoped lang="scss">
@@ -493,8 +493,8 @@ useSeoMeta({
margin-left: -0.5rem;
border-radius: var(--radius-lg);
grid-template:
'avatar name' auto
'avatar role' auto
"avatar name" auto
"avatar role" auto
/ auto 1fr;
p {
margin: 0;

View File

@@ -15,9 +15,9 @@
</template>
<script setup>
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
import ChartDisplay from "~/components/ui/charts/ChartDisplay.vue";
const { projects } = inject('organizationContext')
const { projects } = inject("organizationContext");
</script>
<style scoped lang="scss">

View File

@@ -1,6 +1,6 @@
<script setup>
import { Button, FileInput, Avatar, ConfirmModal } from '@modrinth/ui'
import { UploadIcon, SaveIcon, TrashIcon } from '@modrinth/assets'
import { Button, FileInput, Avatar, ConfirmModal } from "@modrinth/ui";
import { UploadIcon, SaveIcon, TrashIcon } from "@modrinth/assets";
const {
organization,
@@ -9,93 +9,93 @@ const {
deleteIcon,
patchIcon,
patchOrganization,
} = inject('organizationContext')
} = inject("organizationContext");
const icon = ref(null)
const deletedIcon = ref(false)
const previewImage = ref(null)
const icon = ref(null);
const deletedIcon = ref(false);
const previewImage = ref(null);
const name = ref(organization.value.name)
const slug = ref(organization.value.slug)
const name = ref(organization.value.name);
const slug = ref(organization.value.slug);
const summary = ref(organization.value.description)
const summary = ref(organization.value.description);
const patchData = computed(() => {
const data = {}
const data = {};
if (name.value !== organization.value.name) {
data.name = name.value
data.name = name.value;
}
if (slug.value !== organization.value.slug) {
data.slug = slug.value
data.slug = slug.value;
}
if (summary.value !== organization.value.description) {
data.description = summary.value
data.description = summary.value;
}
return data
})
return data;
});
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value
})
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value;
});
const markIconForDeletion = () => {
deletedIcon.value = true
icon.value = null
previewImage.value = null
}
deletedIcon.value = true;
icon.value = null;
previewImage.value = null;
};
const showPreviewImage = (files) => {
const reader = new FileReader()
const reader = new FileReader();
icon.value = files[0]
deletedIcon.value = false
icon.value = files[0];
deletedIcon.value = false;
reader.readAsDataURL(icon.value)
reader.readAsDataURL(icon.value);
reader.onload = (event) => {
previewImage.value = event.target.result
}
}
previewImage.value = event.target.result;
};
};
const orgId = useRouteId()
const orgId = useRouteId();
const onSaveChanges = useClientTry(async () => {
if (hasChanges.value) {
await patchOrganization(orgId, patchData.value)
await patchOrganization(orgId, patchData.value);
}
if (deletedIcon.value) {
await deleteIcon()
deletedIcon.value = false
await deleteIcon();
deletedIcon.value = false;
} else if (icon.value) {
await patchIcon(icon.value)
icon.value = null
await patchIcon(icon.value);
icon.value = null;
}
await refreshOrganization()
await refreshOrganization();
addNotification({
group: 'main',
title: 'Organization updated',
text: 'Your organization has been updated.',
type: 'success',
})
})
group: "main",
title: "Organization updated",
text: "Your organization has been updated.",
type: "success",
});
});
const onDeleteOrganization = useClientTry(async () => {
await useBaseFetch(`organization/${orgId}`, {
method: 'DELETE',
method: "DELETE",
apiVersion: 3,
})
});
addNotification({
group: 'main',
title: 'Organization deleted',
text: 'Your organization has been deleted.',
type: 'success',
})
group: "main",
title: "Organization deleted",
text: "Your organization has been deleted.",
type: "success",
});
await navigateTo('/dashboard/organizations')
})
await navigateTo("/dashboard/organizations");
});
</script>
<template>

View File

@@ -22,7 +22,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.MANAGE_INVITES
organizationPermissions.MANAGE_INVITES,
)
"
@keypress.enter="() => onInviteTeamMember(organization.team, currentUsername)"
@@ -33,7 +33,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.MANAGE_INVITES
organizationPermissions.MANAGE_INVITES,
)
"
@click="() => onInviteTeamMember(organization.team_id, currentUsername)"
@@ -108,7 +108,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER
organizationPermissions.EDIT_MEMBER,
)
"
/>
@@ -128,7 +128,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER
organizationPermissions.EDIT_MEMBER,
)
"
/>
@@ -145,7 +145,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER_DEFAULT_PERMISSIONS
organizationPermissions.EDIT_MEMBER_DEFAULT_PERMISSIONS,
) || !isPermission(currentMember.permissions, permission)
"
:label="permToLabel(label)"
@@ -165,7 +165,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER
organizationPermissions.EDIT_MEMBER,
) || !isPermission(currentMember.organization_permissions, permission)
"
:label="permToLabel(label)"
@@ -179,7 +179,7 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER
organizationPermissions.EDIT_MEMBER,
)
"
@click="onUpdateTeamMember(organization.team_id, member)"
@@ -193,11 +193,11 @@
:disabled="
!isPermission(
currentMember.organization_permissions,
organizationPermissions.EDIT_MEMBER
organizationPermissions.EDIT_MEMBER,
) &&
!isPermission(
currentMember.organization_permissions,
organizationPermissions.REMOVE_MEMBER
organizationPermissions.REMOVE_MEMBER,
)
"
@click="onRemoveMember(organization.team_id, member)"
@@ -225,29 +225,29 @@ import {
UserPlusIcon,
UserXIcon as UserRemoveIcon,
DropdownIcon,
} from '@modrinth/assets'
import { Button, Badge, Avatar, Checkbox } from '@modrinth/ui'
import { ref } from 'vue'
import CrownIcon from '~/assets/images/utils/crown.svg?component'
} from "@modrinth/assets";
import { Button, Badge, Avatar, Checkbox } from "@modrinth/ui";
import { ref } from "vue";
import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { removeTeamMember } from '~/helpers/teams.js'
import { isPermission } from '~/utils/permissions.ts'
import { removeTeamMember } from "~/helpers/teams.js";
import { isPermission } from "~/utils/permissions.ts";
const { organization, refresh: refreshOrganization, currentMember } = inject('organizationContext')
const { organization, refresh: refreshOrganization, currentMember } = inject("organizationContext");
const auth = await useAuth()
const auth = await useAuth();
const currentUsername = ref('')
const openTeamMembers = ref([])
const currentUsername = ref("");
const openTeamMembers = ref([]);
const allTeamMembers = ref(organization.value.members)
const allTeamMembers = ref(organization.value.members);
watch(
() => organization.value,
() => {
allTeamMembers.value = organization.value.members
}
)
allTeamMembers.value = organization.value.members;
},
);
const projectPermissions = {
UPLOAD_VERSION: 1 << 0,
@@ -260,7 +260,7 @@ const projectPermissions = {
DELETE_PROJECT: 1 << 7,
VIEW_ANALYTICS: 1 << 8,
VIEW_PAYOUTS: 1 << 9,
}
};
const organizationPermissions = {
EDIT_DETAILS: 1 << 0,
@@ -271,49 +271,49 @@ const organizationPermissions = {
REMOVE_PROJECT: 1 << 5,
DELETE_ORGANIZATION: 1 << 6,
EDIT_MEMBER_DEFAULT_PERMISSIONS: 1 << 7,
}
};
const permToLabel = (key) => {
const o = key.split('_').join(' ')
return o.charAt(0).toUpperCase() + o.slice(1).toLowerCase()
}
const o = key.split("_").join(" ");
return o.charAt(0).toUpperCase() + o.slice(1).toLowerCase();
};
const leaveProject = async (teamId, uid) => {
await removeTeamMember(teamId, uid)
await navigateTo(`/organization/${organization.value.id}`)
}
await removeTeamMember(teamId, uid);
await navigateTo(`/organization/${organization.value.id}`);
};
const onLeaveProject = useClientTry(leaveProject)
const onLeaveProject = useClientTry(leaveProject);
const onInviteTeamMember = useClientTry(async (teamId, username) => {
const user = await useBaseFetch(`user/${username}`)
const user = await useBaseFetch(`user/${username}`);
const data = {
user_id: user.id.trim(),
}
};
await useBaseFetch(`team/${teamId}/members`, {
method: 'POST',
method: "POST",
body: data,
})
await refreshOrganization()
currentUsername.value = ''
});
await refreshOrganization();
currentUsername.value = "";
addNotification({
group: 'main',
title: 'Member invited',
group: "main",
title: "Member invited",
text: `${user.username} has been invited to the organization.`,
type: 'success',
})
})
type: "success",
});
});
const onRemoveMember = useClientTry(async (teamId, member) => {
await removeTeamMember(teamId, member.user.id)
await refreshOrganization()
await removeTeamMember(teamId, member.user.id);
await refreshOrganization();
addNotification({
group: 'main',
title: 'Member removed',
group: "main",
title: "Member removed",
text: `${member.user.username} has been removed from the organization.`,
type: 'success',
})
})
type: "success",
});
});
const onUpdateTeamMember = useClientTry(async (teamId, member) => {
const data = !member.is_owner
@@ -326,36 +326,36 @@ const onUpdateTeamMember = useClientTry(async (teamId, member) => {
: {
payouts_split: member.payouts_split,
role: member.role,
}
};
await useBaseFetch(`team/${teamId}/members/${member.user.id}`, {
method: 'PATCH',
method: "PATCH",
body: data,
})
await refreshOrganization()
});
await refreshOrganization();
addNotification({
group: 'main',
title: 'Member updated',
group: "main",
title: "Member updated",
text: `${member.user.username} has been updated.`,
type: 'success',
})
})
type: "success",
});
});
const onTransferOwnership = useClientTry(async (teamId, uid) => {
const data = {
user_id: uid,
}
};
await useBaseFetch(`team/${teamId}/owner`, {
method: 'PATCH',
method: "PATCH",
body: data,
})
await refreshOrganization()
});
await refreshOrganization();
addNotification({
group: 'main',
title: 'Ownership transferred',
group: "main",
title: "Ownership transferred",
text: `The ownership of ${organization.value.name} has been successfully transferred.`,
type: 'success',
})
})
type: "success",
});
});
</script>
<style lang="scss" scoped>

View File

@@ -120,14 +120,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"
>
@@ -208,8 +208,8 @@
</div>
</div>
<div class="table">
<div class="table-row table-head">
<div class="table-cell check-cell">
<div class="table-head table-row">
<div class="check-cell table-cell">
<Checkbox
:model-value="selectedProjects === sortedProjects"
@update:model-value="
@@ -227,7 +227,7 @@
<div class="table-cell" />
</div>
<div v-for="project in sortedProjects" :key="`project-${project.id}`" class="table-row">
<div class="table-cell check-cell">
<div class="check-cell table-cell">
<Checkbox
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:model-value="selectedProjects.includes(project)"
@@ -273,7 +273,7 @@
<BoxIcon />
<span>{{
$formatProjectType(
$getProjectTypeForDisplay(project.project_types[0] ?? 'project', project.loaders)
$getProjectTypeForDisplay(project.project_types[0] ?? "project", project.loaders),
)
}}</span>
</div>
@@ -298,7 +298,7 @@
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Multiselect } from "vue-multiselect";
import {
BoxIcon,
SettingsIcon,
@@ -310,215 +310,215 @@ import {
SaveIcon,
SortAscendingIcon,
SortDescendingIcon,
} from '@modrinth/assets'
import { Button, Modal, Avatar, CopyCode, Badge, Checkbox } from '@modrinth/ui'
} from "@modrinth/assets";
import { Button, Modal, Avatar, CopyCode, Badge, Checkbox } from "@modrinth/ui";
import ModalCreation from '~/components/ui/ModalCreation.vue'
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
import ModalCreation from "~/components/ui/ModalCreation.vue";
import OrganizationProjectTransferModal from "~/components/ui/OrganizationProjectTransferModal.vue";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const { organization, projects, refresh } = inject('organizationContext')
const { organization, projects, refresh } = inject("organizationContext");
const auth = await useAuth()
const auth = await useAuth();
const { data: userProjects, refresh: refreshUserProjects } = await useAsyncData(
`user/${auth.value.user.id}/projects`,
() => useBaseFetch(`user/${auth.value.user.id}/projects`),
{
watch: [auth],
}
)
},
);
const usersOwnedProjects = ref([])
const usersOwnedProjects = ref([]);
watch(
() => userProjects.value,
async () => {
if (!userProjects.value) return
if (!userProjects.value.length) return
if (!userProjects.value) return;
if (!userProjects.value.length) return;
const projects = userProjects.value.filter((project) => project.organization === null)
const projects = userProjects.value.filter((project) => project.organization === null);
const teamIds = projects.map((project) => project?.team).filter((x) => x)
const teamIds = projects.map((project) => project?.team).filter((x) => x);
// Shape of teams is member[][]
const teams = await useBaseFetch(`teams?ids=${JSON.stringify(teamIds)}`, {
apiVersion: 3,
})
});
// for each team id, figure out if the user is a member, and is_owner. Then filter the projects to only include those that are owned by the user
const ownedTeamIds = teamIds.filter((_tid, i) => {
const team = teams?.[i]
if (!team) return false
const member = team.find((member) => member.user.id === auth.value.user.id)
return member && member.is_owner
})
const ownedProjects = projects.filter((project) => ownedTeamIds.includes(project.team))
usersOwnedProjects.value = ownedProjects
const team = teams?.[i];
if (!team) return false;
const member = team.find((member) => member.user.id === auth.value.user.id);
return member && member.is_owner;
});
const ownedProjects = projects.filter((project) => ownedTeamIds.includes(project.team));
usersOwnedProjects.value = ownedProjects;
}, // watch options
{ immediate: true, deep: true }
)
{ immediate: true, deep: true },
);
const onProjectTransferSubmit = async (projects) => {
try {
for (const project of projects) {
await useBaseFetch(`organization/${organization.value.id}/projects`, {
method: 'POST',
method: "POST",
body: JSON.stringify({
project_id: project.id,
}),
apiVersion: 3,
})
});
}
await refresh()
await refreshUserProjects()
await refresh();
await refreshUserProjects();
addNotification({
group: 'main',
title: 'Success',
text: 'Transferred selected projects to organization.',
type: 'success',
})
group: "main",
title: "Success",
text: "Transferred selected projects to organization.",
type: "success",
});
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
text: err?.data?.description || err?.message || err || 'Unknown error',
type: 'error',
})
console.error(err)
group: "main",
title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error",
type: "error",
});
console.error(err);
}
}
};
const EDIT_DETAILS = 1 << 2
const EDIT_DETAILS = 1 << 2;
const updateSort = (inputProjects, sort, descending) => {
let sortedArray = inputProjects
let sortedArray = inputProjects;
switch (sort) {
case 'Name':
case "Name":
sortedArray = inputProjects.slice().sort((a, b) => {
return a.name.localeCompare(b.name)
})
break
case 'Status':
return a.name.localeCompare(b.name);
});
break;
case "Status":
sortedArray = inputProjects.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 = inputProjects.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;
};
const sortedProjects = ref(updateSort(projects.value, 'Name'))
const selectedProjects = ref([])
const sortBy = ref('Name')
const descending = ref(false)
const editLinksModal = ref(null)
const sortedProjects = ref(updateSort(projects.value, "Name"));
const selectedProjects = ref([]);
const sortBy = ref("Name");
const descending = ref(false);
const editLinksModal = ref(null);
watch(
() => projects.value,
(newVal) => {
sortedProjects.value = updateSort(newVal, sortBy.value, descending.value)
}
)
sortedProjects.value = updateSort(newVal, sortBy.value, descending.value);
},
);
const emptyLinksData = {
showAffected: false,
source: {
val: '',
val: "",
clear: false,
},
discord: {
val: '',
val: "",
clear: false,
},
wiki: {
val: '',
val: "",
clear: false,
},
issues: {
val: '',
val: "",
clear: false,
},
}
};
const editLinks = ref(emptyLinksData)
const editLinks = ref(emptyLinksData);
const updateDescending = () => {
descending.value = !descending.value
sortedProjects.value = updateSort(sortedProjects.value, sortBy.value, descending.value)
}
descending.value = !descending.value;
sortedProjects.value = updateSort(sortedProjects.value, sortBy.value, descending.value);
};
const onBulkEditLinks = useClientTry(async () => {
const linkData = editLinks.value
const linkData = editLinks.value;
const baseData = {}
const baseData = {};
if (linkData.issues.clear) {
baseData.issues_url = null
baseData.issues_url = null;
} else if (linkData.issues.val.trim().length > 0) {
baseData.issues_url = linkData.issues.val.trim()
baseData.issues_url = linkData.issues.val.trim();
}
if (linkData.source.clear) {
baseData.source_url = null
baseData.source_url = null;
} else if (linkData.source.val.trim().length > 0) {
baseData.source_url = linkData.source.val.trim()
baseData.source_url = linkData.source.val.trim();
}
if (linkData.wiki.clear) {
baseData.wiki_url = null
baseData.wiki_url = null;
} else if (linkData.wiki.val.trim().length > 0) {
baseData.wiki_url = linkData.wiki.val.trim()
baseData.wiki_url = linkData.wiki.val.trim();
}
if (linkData.discord.clear) {
baseData.discord_url = null
baseData.discord_url = null;
} else if (linkData.discord.val.trim().length > 0) {
baseData.discord_url = linkData.discord.val.trim()
baseData.discord_url = linkData.discord.val.trim();
}
await useBaseFetch(`projects?ids=${JSON.stringify(selectedProjects.value.map((x) => x.id))}`, {
method: 'PATCH',
method: "PATCH",
body: JSON.stringify(baseData),
})
});
editLinksModal.value.hide()
editLinksModal.value.hide();
addNotification({
group: 'main',
title: 'Success',
group: "main",
title: "Success",
text: "Bulk edited selected project's links.",
type: 'success',
})
type: "success",
});
selectedProjects.value = []
editLinks.value = emptyLinksData
})
selectedProjects.value = [];
editLinks.value = emptyLinksData;
});
</script>
<style lang="scss" scoped>
.table {
@@ -551,7 +551,7 @@ const onBulkEditLinks = useClientTry(async () => {
.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;
@@ -588,7 +588,7 @@ const onBulkEditLinks = useClientTry(async () => {
}
.table-head {
grid-template: 'checkbox settings';
grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr);
:nth-child(2),
@@ -604,7 +604,7 @@ const onBulkEditLinks = useClientTry(async () => {
@media screen and (max-width: 560px) {
.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) {
@@ -613,7 +613,7 @@ const onBulkEditLinks = useClientTry(async () => {
}
.table-head {
grid-template: 'checkbox settings';
grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr);
}
}
@@ -652,7 +652,7 @@ const onBulkEditLinks = useClientTry(async () => {
width: -moz-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

@@ -92,139 +92,139 @@
</template>
<script setup lang="ts">
import { Card, Button, MarkdownEditor, DropdownSelect } from '@modrinth/ui'
import { SaveIcon } from '@modrinth/assets'
import { useImageUpload } from '~/composables/image-upload.ts'
import { Card, Button, MarkdownEditor, DropdownSelect } from "@modrinth/ui";
import { SaveIcon } from "@modrinth/assets";
import { useImageUpload } from "~/composables/image-upload.ts";
const tags = useTags()
const route = useNativeRoute()
const tags = useTags();
const route = useNativeRoute();
const accessQuery = (id: string): string => {
return route.query?.[id]?.toString() || ''
}
return route.query?.[id]?.toString() || "";
};
const submitLoading = ref<boolean>(false)
const submitLoading = ref<boolean>(false);
const uploadedImageIDs = ref<string[]>([])
const uploadedImageIDs = ref<string[]>([]);
const reportBody = ref<string>(accessQuery('body'))
const reportItem = ref<string>(accessQuery('item'))
const reportItemID = ref<string>(accessQuery('itemID'))
const reportType = ref<string>('')
const reportBody = ref<string>(accessQuery("body"));
const reportItem = ref<string>(accessQuery("item"));
const reportItemID = ref<string>(accessQuery("itemID"));
const reportType = ref<string>("");
const reportItems = ['project', 'version', 'user']
const reportTypes = computed(() => tags.value.reportTypes)
const reportItems = ["project", "version", "user"];
const reportTypes = computed(() => tags.value.reportTypes);
const canSubmit = computed(() => {
return (
reportItem.value !== '' &&
reportItemID.value !== '' &&
reportType.value !== '' &&
reportBody.value !== ''
)
})
reportItem.value !== "" &&
reportItemID.value !== "" &&
reportType.value !== "" &&
reportBody.value !== ""
);
});
const submissionValidation = () => {
if (!canSubmit.value) {
throw new Error('Please fill out all required fields')
throw new Error("Please fill out all required fields");
}
if (reportItem.value === '') {
throw new Error('Please select a report item')
if (reportItem.value === "") {
throw new Error("Please select a report item");
}
if (reportItemID.value === '') {
throw new Error('Please enter a report item ID')
if (reportItemID.value === "") {
throw new Error("Please enter a report item ID");
}
if (reportType.value === '') {
throw new Error('Please select a report type')
if (reportType.value === "") {
throw new Error("Please select a report type");
}
if (reportBody.value === '') {
throw new Error('Please enter a report body')
if (reportBody.value === "") {
throw new Error("Please enter a report body");
}
return true
}
return true;
};
const capitalizeString = (value?: string) => {
if (!value) return ''
return value?.charAt(0).toUpperCase() + value?.slice(1)
}
if (!value) return "";
return value?.charAt(0).toUpperCase() + value?.slice(1);
};
const submitReport = async () => {
submitLoading.value = true
submitLoading.value = true;
let data: {
[key: string]: unknown
[key: string]: unknown;
} = {
report_type: reportType.value,
item_type: reportItem.value,
item_id: reportItemID.value,
body: reportBody.value,
}
};
function takeNLast<T>(arr: T[], n: number): T[] {
return arr.slice(Math.max(arr.length - n, 0))
return arr.slice(Math.max(arr.length - n, 0));
}
if (uploadedImageIDs.value.length > 0) {
data = {
...data,
uploaded_images: takeNLast(uploadedImageIDs.value, 10),
}
};
}
try {
submissionValidation()
submissionValidation();
} catch (error) {
submitLoading.value = false
submitLoading.value = false;
if (error instanceof Error) {
addNotification({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: error.message,
type: 'error',
})
type: "error",
});
}
return
return;
}
try {
const response = (await useBaseFetch('report', {
method: 'POST',
const response = (await useBaseFetch("report", {
method: "POST",
body: data,
})) as { id: string }
})) as { id: string };
submitLoading.value = false
submitLoading.value = false;
if (response?.id) {
navigateTo(`/dashboard/report/${response.id}`)
navigateTo(`/dashboard/report/${response.id}`);
}
} catch (error) {
submitLoading.value = false
submitLoading.value = false;
if (error instanceof Error) {
addNotification({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: error.message,
type: 'error',
})
type: "error",
});
}
throw error
throw error;
}
}
};
const onImageUpload = async (file: File) => {
const item = await useImageUpload(file, { context: 'report' })
uploadedImageIDs.value.push(item.id)
return item.url
}
const item = await useImageUpload(file, { context: "report" });
uploadedImageIDs.value.push(item.id);
return item.url;
};
</script>
<style scoped lang="scss">

View File

@@ -44,7 +44,7 @@
<template v-if="header === 'resolutions'">
<SearchFilter
v-for="category in categories.filter(
(x) => x.project_type === projectType.actual
(x) => x.project_type === projectType.actual,
)"
:key="category.name"
:active-filters="orFacets"
@@ -57,7 +57,7 @@
<template v-else>
<SearchFilter
v-for="category in categories.filter(
(x) => x.project_type === projectType.actual
(x) => x.project_type === projectType.actual,
)"
:key="category.name"
:active-filters="facets"
@@ -88,13 +88,13 @@
return (
tags.loaderData.modLoaders.includes(x.name) &&
!tags.loaderData.hiddenModLoaders.includes(x.name)
)
);
} else if (projectType.id === 'plugin') {
return tags.loaderData.pluginLoaders.includes(x.name)
return tags.loaderData.pluginLoaders.includes(x.name);
} else if (projectType.id === 'datapack') {
return tags.loaderData.dataPackLoaders.includes(x.name)
return tags.loaderData.dataPackLoaders.includes(x.name);
} else {
return x.supported_project_types.includes(projectType.actual)
return x.supported_project_types.includes(projectType.actual);
}
})"
:key="loader.name"
@@ -111,7 +111,7 @@
return (
tags.loaderData.modLoaders.includes(x.name) &&
tags.loaderData.hiddenModLoaders.includes(x.name)
)
);
})"
:key="loader.name"
ref="loaderFilters"
@@ -144,7 +144,7 @@
</h3>
<SearchFilter
v-for="loader in tags.loaders.filter((x) =>
tags.loaderData.pluginPlatformLoaders.includes(x.name)
tags.loaderData.pluginPlatformLoaders.includes(x.name),
)"
:key="loader.name"
ref="platformFilters"
@@ -295,7 +295,7 @@
class="pagination-before"
@switch-page="onSearchChange"
/>
<LogoAnimated v-if="searchLoading && !noLoad"></LogoAnimated>
<LogoAnimated v-if="searchLoading && !noLoad" />
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
<p>No results found for your query!</p>
</div>
@@ -343,134 +343,134 @@
</div>
</template>
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Promotion } from '@modrinth/ui'
import ProjectCard from '~/components/ui/ProjectCard.vue'
import Pagination from '~/components/ui/Pagination.vue'
import SearchFilter from '~/components/ui/search/SearchFilter.vue'
import Checkbox from '~/components/ui/Checkbox.vue'
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
import { Multiselect } from "vue-multiselect";
import { Promotion } from "@modrinth/ui";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import Pagination from "~/components/ui/Pagination.vue";
import SearchFilter from "~/components/ui/search/SearchFilter.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
import ClientIcon from '~/assets/images/categories/client.svg?component'
import ServerIcon from '~/assets/images/categories/server.svg?component'
import ClientIcon from "~/assets/images/categories/client.svg?component";
import ServerIcon from "~/assets/images/categories/server.svg?component";
import SearchIcon from '~/assets/images/utils/search.svg?component'
import ClearIcon from '~/assets/images/utils/clear.svg?component'
import FilterIcon from '~/assets/images/utils/filter.svg?component'
import GridIcon from '~/assets/images/utils/grid.svg?component'
import ListIcon from '~/assets/images/utils/list.svg?component'
import ImageIcon from '~/assets/images/utils/image.svg?component'
import SearchIcon from "~/assets/images/utils/search.svg?component";
import ClearIcon from "~/assets/images/utils/clear.svg?component";
import FilterIcon from "~/assets/images/utils/filter.svg?component";
import GridIcon from "~/assets/images/utils/grid.svg?component";
import ListIcon from "~/assets/images/utils/list.svg?component";
import ImageIcon from "~/assets/images/utils/image.svg?component";
const sidebarMenuOpen = ref(false)
const showAllLoaders = ref(false)
const sidebarMenuOpen = ref(false);
const showAllLoaders = ref(false);
const data = useNuxtApp()
const route = useNativeRoute()
const data = useNuxtApp();
const route = useNativeRoute();
const cosmetics = useCosmetics()
const tags = useTags()
const cosmetics = useCosmetics();
const tags = useTags();
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const selectedEnvironments = ref([])
const query = ref("");
const facets = ref([]);
const orFacets = ref([]);
const selectedVersions = ref([]);
const onlyOpenSource = ref(false);
const showSnapshots = ref(false);
const selectedEnvironments = ref([]);
const sortTypes = shallowReadonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref({ display: 'Relevance', name: 'relevance' })
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref({ id: 'mod', display: 'mod', actual: 'mod' })
{ display: "Relevance", name: "relevance" },
{ display: "Download count", name: "downloads" },
{ display: "Follow count", name: "follows" },
{ display: "Recently published", name: "newest" },
{ display: "Recently updated", name: "updated" },
]);
const sortType = ref({ display: "Relevance", name: "relevance" });
const maxResults = ref(20);
const currentPage = ref(1);
const projectType = ref({ id: "mod", display: "mod", actual: "mod" });
const ogTitle = computed(
() => `Search ${projectType.value.display}s${query.value ? ' | ' + query.value : ''}`
)
() => `Search ${projectType.value.display}s${query.value ? " | " + query.value : ""}`,
);
const description = computed(
() =>
`Search and browse thousands of Minecraft ${projectType.value.display}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${projectType.value.display}s.`
)
`Search and browse thousands of Minecraft ${projectType.value.display}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${projectType.value.display}s.`,
);
useSeoMeta({
description,
ogTitle,
ogDescription: description,
})
});
if (route.query.q) {
query.value = route.query.q
query.value = route.query.q;
}
if (route.query.f) {
facets.value = getArrayOrString(route.query.f)
facets.value = getArrayOrString(route.query.f);
}
if (route.query.g) {
orFacets.value = getArrayOrString(route.query.g)
orFacets.value = getArrayOrString(route.query.g);
}
if (route.query.v) {
selectedVersions.value = getArrayOrString(route.query.v)
selectedVersions.value = getArrayOrString(route.query.v);
}
if (route.query.l) {
onlyOpenSource.value = route.query.l === 'true'
onlyOpenSource.value = route.query.l === "true";
}
if (route.query.h) {
showSnapshots.value = route.query.h === 'true'
showSnapshots.value = route.query.h === "true";
}
if (route.query.e) {
selectedEnvironments.value = getArrayOrString(route.query.e)
selectedEnvironments.value = getArrayOrString(route.query.e);
}
if (route.query.s) {
sortType.value.name = route.query.s
sortType.value.name = route.query.s;
switch (sortType.value.name) {
case 'relevance':
sortType.value.display = 'Relevance'
break
case 'downloads':
sortType.value.display = 'Downloads'
break
case 'newest':
sortType.value.display = 'Recently published'
break
case 'updated':
sortType.value.display = 'Recently updated'
break
case 'follows':
sortType.value.display = 'Follow count'
break
case "relevance":
sortType.value.display = "Relevance";
break;
case "downloads":
sortType.value.display = "Downloads";
break;
case "newest":
sortType.value.display = "Recently published";
break;
case "updated":
sortType.value.display = "Recently updated";
break;
case "follows":
sortType.value.display = "Follow count";
break;
}
}
if (route.query.m) {
maxResults.value = route.query.m
maxResults.value = route.query.m;
}
if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1;
}
projectType.value = tags.value.projectTypes.find(
(x) => x.id === route.path.substring(1, route.path.length - 1)
)
(x) => x.id === route.path.substring(1, route.path.length - 1),
);
const noLoad = ref(false)
const noLoad = ref(false);
const {
data: rawResults,
refresh: refreshSearch,
pending: searchLoading,
} = useLazyFetch(
() => {
const config = useRuntimeConfig()
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
const config = useRuntimeConfig();
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl;
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`];
if (query.value.length > 0) {
params.push(`query=${encodeURIComponent(query.value)}`)
params.push(`query=${encodeURIComponent(query.value)}`);
}
if (
@@ -480,318 +480,320 @@ const {
selectedEnvironments.value.length > 0 ||
projectType.value
) {
let formattedFacets = []
let formattedFacets = [];
for (const facet of facets.value) {
formattedFacets.push([facet])
formattedFacets.push([facet]);
}
// loaders specifier
if (orFacets.value.length > 0) {
formattedFacets.push(orFacets.value)
} else if (projectType.value.id === 'plugin') {
formattedFacets.push(orFacets.value);
} else if (projectType.value.id === "plugin") {
formattedFacets.push(
tags.value.loaderData.allPluginLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
)
} else if (projectType.value.id === 'mod') {
tags.value.loaderData.allPluginLoaders.map(
(x) => `categories:'${encodeURIComponent(x)}'`,
),
);
} else if (projectType.value.id === "mod") {
formattedFacets.push(
tags.value.loaderData.modLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
)
} else if (projectType.value.id === 'datapack') {
tags.value.loaderData.modLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`),
);
} else if (projectType.value.id === "datapack") {
formattedFacets.push(
tags.value.loaderData.dataPackLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
)
tags.value.loaderData.dataPackLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`),
);
}
if (selectedVersions.value.length > 0) {
const versionFacets = []
const versionFacets = [];
for (const facet of selectedVersions.value) {
versionFacets.push('versions:' + facet)
versionFacets.push("versions:" + facet);
}
formattedFacets.push(versionFacets)
formattedFacets.push(versionFacets);
}
if (onlyOpenSource.value) {
formattedFacets.push(['open_source:true'])
formattedFacets.push(["open_source:true"]);
}
if (selectedEnvironments.value.length > 0) {
let environmentFacets = []
let environmentFacets = [];
const includesClient = selectedEnvironments.value.includes('client')
const includesServer = selectedEnvironments.value.includes('server')
const includesClient = selectedEnvironments.value.includes("client");
const includesServer = selectedEnvironments.value.includes("server");
if (includesClient && includesServer) {
environmentFacets = [['client_side:required'], ['server_side:required']]
environmentFacets = [["client_side:required"], ["server_side:required"]];
} else {
if (includesClient) {
environmentFacets = [
['client_side:optional', 'client_side:required'],
['server_side:optional', 'server_side:unsupported'],
]
["client_side:optional", "client_side:required"],
["server_side:optional", "server_side:unsupported"],
];
}
if (includesServer) {
environmentFacets = [
['client_side:optional', 'client_side:unsupported'],
['server_side:optional', 'server_side:required'],
]
["client_side:optional", "client_side:unsupported"],
["server_side:optional", "server_side:required"],
];
}
}
formattedFacets = [...formattedFacets, ...environmentFacets]
formattedFacets = [...formattedFacets, ...environmentFacets];
}
if (projectType.value) {
formattedFacets.push([`project_type:${projectType.value.actual}`])
formattedFacets.push([`project_type:${projectType.value.actual}`]);
}
params.push(`facets=${encodeURIComponent(JSON.stringify(formattedFacets))}`)
params.push(`facets=${encodeURIComponent(JSON.stringify(formattedFacets))}`);
}
const offset = (currentPage.value - 1) * maxResults.value
const offset = (currentPage.value - 1) * maxResults.value;
if (currentPage.value !== 1) {
params.push(`offset=${offset}`)
params.push(`offset=${offset}`);
}
let url = 'search'
let url = "search";
if (params.length > 0) {
for (let i = 0; i < params.length; i++) {
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
url += i === 0 ? `?${params[i]}` : `&${params[i]}`;
}
}
return `${base}${url}`
return `${base}${url}`;
},
{
transform: (hits) => {
noLoad.value = false
return hits
noLoad.value = false;
return hits;
},
}
)
},
);
const results = shallowRef(toRaw(rawResults))
const results = shallowRef(toRaw(rawResults));
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1
)
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
);
const router = useNativeRouter()
const router = useNativeRouter();
function onSearchChange(newPageNumber) {
noLoad.value = true
noLoad.value = true;
currentPage.value = newPageNumber
currentPage.value = newPageNumber;
if (query.value === null) {
return
return;
}
refreshSearch()
refreshSearch();
if (process.client) {
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
router.replace({ path: route.path, query: obj })
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true);
router.replace({ path: route.path, query: obj });
}
}
function getSearchUrl(offset, useObj) {
const queryItems = []
const obj = {}
const queryItems = [];
const obj = {};
if (query.value) {
queryItems.push(`q=${encodeURIComponent(query.value)}`)
obj.q = query.value
queryItems.push(`q=${encodeURIComponent(query.value)}`);
obj.q = query.value;
}
if (offset > 0) {
queryItems.push(`o=${offset}`)
obj.o = offset
queryItems.push(`o=${offset}`);
obj.o = offset;
}
if (facets.value.length > 0) {
queryItems.push(`f=${encodeURIComponent(facets.value)}`)
obj.f = facets.value
queryItems.push(`f=${encodeURIComponent(facets.value)}`);
obj.f = facets.value;
}
if (orFacets.value.length > 0) {
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`)
obj.g = orFacets.value
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`);
obj.g = orFacets.value;
}
if (selectedVersions.value.length > 0) {
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`)
obj.v = selectedVersions.value
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`);
obj.v = selectedVersions.value;
}
if (onlyOpenSource.value) {
queryItems.push('l=true')
obj.l = true
queryItems.push("l=true");
obj.l = true;
}
if (showSnapshots.value) {
queryItems.push('h=true')
obj.h = true
queryItems.push("h=true");
obj.h = true;
}
if (selectedEnvironments.value.length > 0) {
queryItems.push(`e=${encodeURIComponent(selectedEnvironments.value)}`)
obj.e = selectedEnvironments.value
queryItems.push(`e=${encodeURIComponent(selectedEnvironments.value)}`);
obj.e = selectedEnvironments.value;
}
if (sortType.value.name !== 'relevance') {
queryItems.push(`s=${encodeURIComponent(sortType.value.name)}`)
obj.s = sortType.value.name
if (sortType.value.name !== "relevance") {
queryItems.push(`s=${encodeURIComponent(sortType.value.name)}`);
obj.s = sortType.value.name;
}
if (maxResults.value !== 20) {
queryItems.push(`m=${encodeURIComponent(maxResults.value)}`)
obj.m = maxResults.value
queryItems.push(`m=${encodeURIComponent(maxResults.value)}`);
obj.m = maxResults.value;
}
let url = `${route.path}`
let url = `${route.path}`;
if (queryItems.length > 0) {
url += `?${queryItems[0]}`
url += `?${queryItems[0]}`;
for (let i = 1; i < queryItems.length; i++) {
url += `&${queryItems[i]}`
url += `&${queryItems[i]}`;
}
}
return useObj ? obj : url
return useObj ? obj : url;
}
const categoriesMap = computed(() => {
const categories = {}
const categories = {};
for (const category of data.$sortedCategories()) {
if (categories[category.header]) {
categories[category.header].push(category)
categories[category.header].push(category);
} else {
categories[category.header] = [category]
categories[category.header] = [category];
}
}
return Object.keys(categories).reduce((obj, key) => {
obj[key] = categories[key]
return obj
}, {})
})
obj[key] = categories[key];
return obj;
}, {});
});
function clearFilters() {
for (const facet of [...facets.value]) {
toggleFacet(facet, true)
toggleFacet(facet, true);
}
for (const facet of [...orFacets.value]) {
toggleOrFacet(facet, true)
toggleOrFacet(facet, true);
}
onlyOpenSource.value = false
selectedVersions.value = []
selectedEnvironments.value = []
onSearchChange(1)
onlyOpenSource.value = false;
selectedVersions.value = [];
selectedEnvironments.value = [];
onSearchChange(1);
}
function toggleFacet(elementName, doNotSendRequest = false) {
const index = facets.value.indexOf(elementName)
const index = facets.value.indexOf(elementName);
if (index !== -1) {
facets.value.splice(index, 1)
facets.value.splice(index, 1);
} else {
facets.value.push(elementName)
facets.value.push(elementName);
}
if (!doNotSendRequest) {
onSearchChange(1)
onSearchChange(1);
}
}
function toggleOrFacet(elementName, doNotSendRequest) {
const index = orFacets.value.indexOf(elementName)
const index = orFacets.value.indexOf(elementName);
if (index !== -1) {
orFacets.value.splice(index, 1)
orFacets.value.splice(index, 1);
} else {
if (elementName === 'categories:purpur') {
if (!orFacets.value.includes('categories:paper')) {
orFacets.value.push('categories:paper')
if (elementName === "categories:purpur") {
if (!orFacets.value.includes("categories:paper")) {
orFacets.value.push("categories:paper");
}
if (!orFacets.value.includes('categories:spigot')) {
orFacets.value.push('categories:spigot')
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes('categories:bukkit')) {
orFacets.value.push('categories:bukkit')
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === 'categories:paper') {
if (!orFacets.value.includes('categories:spigot')) {
orFacets.value.push('categories:spigot')
} else if (elementName === "categories:paper") {
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes('categories:bukkit')) {
orFacets.value.push('categories:bukkit')
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === 'categories:spigot') {
if (!orFacets.value.includes('categories:bukkit')) {
orFacets.value.push('categories:bukkit')
} else if (elementName === "categories:spigot") {
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === 'categories:waterfall') {
if (!orFacets.value.includes('categories:bungeecord')) {
orFacets.value.push('categories:bungeecord')
} else if (elementName === "categories:waterfall") {
if (!orFacets.value.includes("categories:bungeecord")) {
orFacets.value.push("categories:bungeecord");
}
}
orFacets.value.push(elementName)
orFacets.value.push(elementName);
}
if (!doNotSendRequest) {
onSearchChange(1)
onSearchChange(1);
}
}
function toggleEnv(environment, sendRequest) {
const index = selectedEnvironments.value.indexOf(environment)
const index = selectedEnvironments.value.indexOf(environment);
if (index !== -1) {
selectedEnvironments.value.splice(index, 1)
selectedEnvironments.value.splice(index, 1);
} else {
selectedEnvironments.value.push(environment)
selectedEnvironments.value.push(environment);
}
if (!sendRequest) {
onSearchChange(1)
onSearchChange(1);
}
}
function onSearchChangeToTop(newPageNumber) {
if (process.client) {
window.scrollTo({ top: 0, behavior: 'smooth' })
window.scrollTo({ top: 0, behavior: "smooth" });
}
onSearchChange(newPageNumber)
onSearchChange(newPageNumber);
}
function cycleSearchDisplayMode() {
cosmetics.value.searchDisplayMode[projectType.value.id] = data.$cycleValue(
cosmetics.value.searchDisplayMode[projectType.value.id],
tags.value.projectViewModes
)
saveCosmetics()
setClosestMaxResults()
tags.value.projectViewModes,
);
saveCosmetics();
setClosestMaxResults();
}
const previousMaxResults = ref(20)
const previousMaxResults = ref(20);
const maxResultsForView = ref({
list: [5, 10, 15, 20, 50, 100],
grid: [6, 12, 18, 24, 48, 96],
gallery: [6, 10, 16, 20, 50, 100],
})
});
function onMaxResultsChange(newPageNumber) {
newPageNumber = Math.max(
1,
Math.min(
Math.floor(newPageNumber / (maxResults.value / previousMaxResults.value)),
pageCount.value
)
)
previousMaxResults.value = maxResults.value
onSearchChange(newPageNumber)
pageCount.value,
),
);
previousMaxResults.value = maxResults.value;
onSearchChange(newPageNumber);
}
function setClosestMaxResults() {
const view = cosmetics.value.searchDisplayMode[projectType.value.id]
const maxResultsOptions = maxResultsForView.value[view] ?? [20]
const currentMax = maxResults.value
const view = cosmetics.value.searchDisplayMode[projectType.value.id];
const maxResultsOptions = maxResultsForView.value[view] ?? [20];
const currentMax = maxResults.value;
if (!maxResultsOptions.includes(currentMax)) {
maxResults.value = maxResultsOptions.reduce(function (prev, curr) {
return Math.abs(curr - currentMax) <= Math.abs(prev - currentMax) ? curr : prev
})
return Math.abs(curr - currentMax) <= Math.abs(prev - currentMax) ? curr : prev;
});
}
}
</script>
@@ -870,7 +872,9 @@ function setClosestMaxResults() {
&.open {
color: var(--color-button-text-active);
background-color: var(--color-brand-highlight);
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
box-shadow:
inset 0 0 0 transparent,
0 0 0 2px var(--color-brand);
}
}

View File

@@ -81,16 +81,16 @@ import {
ShieldIcon,
KeyIcon,
LanguagesIcon,
} from '@modrinth/assets'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import MonitorSmartphoneIcon from '~/assets/images/utils/monitor-smartphone.svg?component'
} from "@modrinth/assets";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import MonitorSmartphoneIcon from "~/assets/images/utils/monitor-smartphone.svg?component";
import { commonMessages, commonSettingsMessages } from '~/utils/common-messages.ts'
import { commonMessages, commonSettingsMessages } from "~/utils/common-messages.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const route = useNativeRoute()
const auth = await useAuth()
const isStaging = useRuntimeConfig().public.siteUrl !== 'https://modrinth.com'
const route = useNativeRoute();
const auth = await useAuth();
const isStaging = useRuntimeConfig().public.siteUrl !== "https://modrinth.com";
</script>

View File

@@ -179,7 +179,7 @@
<qrcode-vue
v-if="twoFactorSecret"
:value="`otpauth://totp/${encodeURIComponent(
auth.user.email
auth.user.email,
)}?secret=${twoFactorSecret}&issuer=Modrinth`"
:size="250"
:margin="2"
@@ -255,15 +255,15 @@
<Modal ref="manageProvidersModal" header="Authentication providers">
<div class="universal-modal">
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">Provider</div>
<div class="table-cell table-text">Actions</div>
<div class="table-head table-row">
<div class="table-text table-cell">Provider</div>
<div class="table-text table-cell">Actions</div>
</div>
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
<div class="table-cell table-text">
<div class="table-text table-cell">
<span><component :is="provider.icon" /> {{ provider.display }}</span>
</div>
<div class="table-cell table-text manage">
<div class="table-text manage table-cell">
<button
v-if="auth.user.auth_providers.includes(provider.id)"
class="btn"
@@ -327,11 +327,11 @@
class="iconified-button"
@click="
() => {
oldPassword = ''
newPassword = ''
confirmNewPassword = ''
removePasswordMode = false
$refs.managePasswordModal.show()
oldPassword = '';
newPassword = '';
confirmNewPassword = '';
removePasswordMode = false;
$refs.managePasswordModal.show();
}
"
>
@@ -401,218 +401,218 @@ import {
RightArrowIcon,
CheckIcon,
ExternalIcon,
} from '@modrinth/assets'
import QrcodeVue from 'qrcode.vue'
import GitHubIcon from 'assets/icons/auth/sso-github.svg'
import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
import GoogleIcon from 'assets/icons/auth/sso-google.svg'
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
import DiscordIcon from 'assets/icons/auth/sso-discord.svg'
import KeyIcon from 'assets/icons/auth/key.svg'
import GitLabIcon from 'assets/icons/auth/sso-gitlab.svg'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import Modal from '~/components/ui/Modal.vue'
} from "@modrinth/assets";
import QrcodeVue from "qrcode.vue";
import GitHubIcon from "assets/icons/auth/sso-github.svg";
import MicrosoftIcon from "assets/icons/auth/sso-microsoft.svg";
import GoogleIcon from "assets/icons/auth/sso-google.svg";
import SteamIcon from "assets/icons/auth/sso-steam.svg";
import DiscordIcon from "assets/icons/auth/sso-discord.svg";
import KeyIcon from "assets/icons/auth/key.svg";
import GitLabIcon from "assets/icons/auth/sso-gitlab.svg";
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import Modal from "~/components/ui/Modal.vue";
useHead({
title: 'Account settings - Modrinth',
})
title: "Account settings - Modrinth",
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const data = useNuxtApp()
const auth = await useAuth()
const data = useNuxtApp();
const auth = await useAuth();
const changeEmailModal = ref()
const email = ref(auth.value.user.email)
const changeEmailModal = ref();
const email = ref(auth.value.user.email);
async function saveEmail() {
if (!email.value) {
return
return;
}
startLoading()
startLoading();
try {
await useBaseFetch(`auth/email`, {
method: 'PATCH',
method: "PATCH",
body: {
email: email.value,
},
})
changeEmailModal.value.hide()
await useAuth(auth.value.token)
});
changeEmailModal.value.hide();
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 managePasswordModal = ref()
const removePasswordMode = ref(false)
const oldPassword = ref('')
const newPassword = ref('')
const confirmNewPassword = ref('')
const managePasswordModal = ref();
const removePasswordMode = ref(false);
const oldPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
async function savePassword() {
if (newPassword.value !== confirmNewPassword.value) {
return
return;
}
startLoading()
startLoading();
try {
await useBaseFetch(`auth/password`, {
method: 'PATCH',
method: "PATCH",
body: {
old_password: auth.value.user.has_password ? oldPassword.value : null,
new_password: removePasswordMode.value ? null : newPassword.value,
},
})
managePasswordModal.value.hide()
await useAuth(auth.value.token)
});
managePasswordModal.value.hide();
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 manageTwoFactorModal = ref()
const twoFactorSecret = ref(null)
const twoFactorFlow = ref(null)
const twoFactorStep = ref(0)
const manageTwoFactorModal = ref();
const twoFactorSecret = ref(null);
const twoFactorFlow = ref(null);
const twoFactorStep = ref(0);
async function showTwoFactorModal() {
twoFactorStep.value = 0
twoFactorCode.value = null
twoFactorIncorrect.value = false
twoFactorStep.value = 0;
twoFactorCode.value = null;
twoFactorIncorrect.value = false;
if (auth.value.user.has_totp) {
manageTwoFactorModal.value.show()
return
manageTwoFactorModal.value.show();
return;
}
twoFactorSecret.value = null
twoFactorFlow.value = null
backupCodes.value = []
manageTwoFactorModal.value.show()
twoFactorSecret.value = null;
twoFactorFlow.value = null;
backupCodes.value = [];
manageTwoFactorModal.value.show();
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/2fa/get_secret', {
method: 'POST',
})
const res = await useBaseFetch("auth/2fa/get_secret", {
method: "POST",
});
twoFactorSecret.value = res.secret
twoFactorFlow.value = res.flow
twoFactorSecret.value = res.secret;
twoFactorFlow.value = res.flow;
} 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 twoFactorIncorrect = ref(false)
const twoFactorCode = ref(null)
const backupCodes = ref([])
const twoFactorIncorrect = ref(false);
const twoFactorCode = ref(null);
const backupCodes = ref([]);
async function verifyTwoFactorCode() {
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/2fa', {
method: 'POST',
const res = await useBaseFetch("auth/2fa", {
method: "POST",
body: {
code: twoFactorCode.value ? twoFactorCode.value : '',
code: twoFactorCode.value ? twoFactorCode.value : "",
flow: twoFactorFlow.value,
},
})
});
backupCodes.value = res.backup_codes
twoFactorStep.value = 2
await useAuth(auth.value.token)
backupCodes.value = res.backup_codes;
twoFactorStep.value = 2;
await useAuth(auth.value.token);
} catch (err) {
twoFactorIncorrect.value = true
twoFactorIncorrect.value = true;
}
stopLoading()
stopLoading();
}
async function removeTwoFactor() {
startLoading()
startLoading();
try {
await useBaseFetch('auth/2fa', {
method: 'DELETE',
await useBaseFetch("auth/2fa", {
method: "DELETE",
body: {
code: twoFactorCode.value ? twoFactorCode.value.toString() : '',
code: twoFactorCode.value ? twoFactorCode.value.toString() : "",
},
})
manageTwoFactorModal.value.hide()
await useAuth(auth.value.token)
});
manageTwoFactorModal.value.hide();
await useAuth(auth.value.token);
} catch (err) {
twoFactorIncorrect.value = true
twoFactorIncorrect.value = true;
}
stopLoading()
stopLoading();
}
const authProviders = [
{
id: 'github',
display: 'GitHub',
id: "github",
display: "GitHub",
icon: GitHubIcon,
},
{
id: 'gitlab',
display: 'GitLab',
id: "gitlab",
display: "GitLab",
icon: GitLabIcon,
},
{
id: 'steam',
display: 'Steam',
id: "steam",
display: "Steam",
icon: SteamIcon,
},
{
id: 'discord',
display: 'Discord',
id: "discord",
display: "Discord",
icon: DiscordIcon,
},
{
id: 'microsoft',
display: 'Microsoft',
id: "microsoft",
display: "Microsoft",
icon: MicrosoftIcon,
},
{
id: 'google',
display: 'Google',
id: "google",
display: "Google",
icon: GoogleIcon,
},
]
];
async function deleteAccount() {
startLoading()
startLoading();
try {
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'DELETE',
})
method: "DELETE",
});
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
useCookie('auth-token').value = null
window.location.href = '/'
useCookie("auth-token").value = null;
window.location.href = "/";
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -134,13 +134,13 @@
class="btn btn-primary"
@click="
() => {
name = null
icon = null
scopesVal = 0
redirectUris = ['']
editingId = null
expires = null
$refs.appModal.show()
name = null;
icon = null;
scopesVal = 0;
redirectUris = [''];
editingId = null;
expires = null;
$refs.appModal.show();
}
"
>
@@ -189,8 +189,8 @@
setForm({
...app,
redirect_uris: app.redirect_uris.map((u) => u.uri) || [],
})
$refs.appModal.show()
});
$refs.appModal.show();
}
"
>
@@ -202,8 +202,8 @@
icon-only
@click="
() => {
editingId = app.id
$refs.modal_confirm.show()
editingId = app.id;
$refs.modal_confirm.show();
}
"
>
@@ -215,9 +215,9 @@
</div>
</template>
<script setup>
import { UploadIcon, PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from '@modrinth/assets'
import { CopyCode, ConfirmModal, Button, Checkbox, Avatar, FileInput } from '@modrinth/ui'
import Modal from '~/components/ui/Modal.vue'
import { UploadIcon, PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { CopyCode, ConfirmModal, Button, Checkbox, Avatar, FileInput } from "@modrinth/ui";
import Modal from "~/components/ui/Modal.vue";
import {
scopeList,
@@ -225,132 +225,132 @@ import {
toggleScope,
useScopes,
getScopeValue,
} from '~/composables/auth/scopes.ts'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
} from "~/composables/auth/scopes.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: 'Applications - Modrinth',
})
title: "Applications - Modrinth",
});
const data = useNuxtApp()
const { scopesToLabels } = useScopes()
const data = useNuxtApp();
const { scopesToLabels } = useScopes();
const appModal = ref()
const appModal = ref();
// Any apps created in the current state will be stored here
// Users can copy Client Secrets and such before the page reloads
const createdApps = ref([])
const createdApps = ref([]);
const editingId = ref(null)
const name = ref(null)
const icon = ref(null)
const scopesVal = ref(BigInt(0))
const redirectUris = ref([''])
const url = ref(null)
const description = ref(null)
const editingId = ref(null);
const name = ref(null);
const icon = ref(null);
const scopesVal = ref(BigInt(0));
const redirectUris = ref([""]);
const url = ref(null);
const description = ref(null);
const loading = ref(false)
const loading = ref(false);
const auth = await useAuth()
const auth = await useAuth();
const { data: usersApps, refresh } = await useAsyncData(
'usersApps',
"usersApps",
() =>
useBaseFetch(`user/${auth.value.user.id}/oauth_apps`, {
apiVersion: 3,
}),
{
watch: [auth],
}
)
},
);
const setForm = (app) => {
if (app?.id) {
editingId.value = app.id
editingId.value = app.id;
} else {
editingId.value = null
editingId.value = null;
}
name.value = app?.name || ''
icon.value = app?.icon_url || ''
scopesVal.value = app?.max_scopes || BigInt(0)
url.value = app?.url || ''
description.value = app?.description || ''
name.value = app?.name || "";
icon.value = app?.icon_url || "";
scopesVal.value = app?.max_scopes || BigInt(0);
url.value = app?.url || "";
description.value = app?.description || "";
if (app?.redirect_uris) {
redirectUris.value = app.redirect_uris.map((uri) => uri?.uri || uri)
redirectUris.value = app.redirect_uris.map((uri) => uri?.uri || uri);
} else {
redirectUris.value = ['']
redirectUris.value = [""];
}
}
};
const canSubmit = computed(() => {
// Make sure name, scopes, and return uri are at least filled in
const filledIn =
name.value && name.value !== '' && name.value?.length > 2 && redirectUris.value.length > 0
name.value && name.value !== "" && name.value?.length > 2 && redirectUris.value.length > 0;
// Make sure the redirect uris are either one empty string or all filled in with valid urls
const oneValid = redirectUris.value.length === 1 && redirectUris.value[0] === ''
let allValid
const oneValid = redirectUris.value.length === 1 && redirectUris.value[0] === "";
let allValid;
try {
allValid = redirectUris.value.every((uri) => {
const url = new URL(uri)
return !!url
})
const url = new URL(uri);
return !!url;
});
} catch (err) {
allValid = false
allValid = false;
}
return filledIn && (oneValid || allValid)
})
return filledIn && (oneValid || allValid);
});
const clientCreatedInState = (id) => {
return createdApps.value.find((app) => app.id === id)
}
return createdApps.value.find((app) => app.id === id);
};
async function onImageSelection(files) {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
if (files.length > 0) {
const file = files[0]
const extFromType = file.type.split('/')[1]
const file = files[0];
const extFromType = file.type.split("/")[1];
await useBaseFetch('oauth/app/' + editingId.value + '/icon', {
method: 'PATCH',
await useBaseFetch("oauth/app/" + editingId.value + "/icon", {
method: "PATCH",
internal: true,
body: file,
query: {
ext: extFromType,
},
})
});
await refresh()
await refresh();
const app = usersApps.value.find((app) => app.id === editingId.value)
const app = usersApps.value.find((app) => app.id === editingId.value);
if (app) {
setForm(app)
setForm(app);
}
data.$notify({
group: 'main',
title: 'Icon updated',
text: 'Your application icon has been updated.',
type: 'success',
})
group: "main",
title: "Icon updated",
text: "Your application icon has been updated.",
type: "success",
});
}
}
async function createApp() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
const createdAppInfo = await useBaseFetch('oauth/app', {
method: 'POST',
const createdAppInfo = await useBaseFetch("oauth/app", {
method: "POST",
internal: true,
body: {
name: name.value,
@@ -358,38 +358,38 @@ async function createApp() {
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
redirect_uris: redirectUris.value,
},
})
});
createdApps.value.push(createdAppInfo)
createdApps.value.push(createdAppInfo);
setForm(null)
appModal.value.hide()
setForm(null);
appModal.value.hide();
await refresh()
await refresh();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function editApp() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
// check if there's any difference between the current app and the one in the state
const app = usersApps.value.find((app) => app.id === editingId.value)
const app = usersApps.value.find((app) => app.id === editingId.value);
if (!app) {
throw new Error('No app found')
throw new Error("No app found");
}
if (
@@ -400,74 +400,74 @@ async function editApp() {
app.url === url.value &&
app.description === description.value
) {
setForm(null)
editingId.value = null
appModal.value.hide()
throw new Error('No changes detected')
setForm(null);
editingId.value = null;
appModal.value.hide();
throw new Error("No changes detected");
}
const body = {
name: name.value,
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
redirect_uris: redirectUris.value,
}
};
if (url.value && url.value?.length > 0) {
body.url = url.value
body.url = url.value;
}
if (description.value && description.value?.length > 0) {
body.description = description.value
body.description = description.value;
}
if (icon.value && icon.value?.length > 0) {
body.icon_url = icon.value
body.icon_url = icon.value;
}
await useBaseFetch('oauth/app/' + editingId.value, {
method: 'PATCH',
await useBaseFetch("oauth/app/" + editingId.value, {
method: "PATCH",
internal: true,
body,
})
});
await refresh()
setForm(null)
editingId.value = null
await refresh();
setForm(null);
editingId.value = null;
appModal.value.hide()
appModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function removeApp() {
startLoading()
startLoading();
try {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
await useBaseFetch(`oauth/app/${editingId.value}`, {
internal: true,
method: 'DELETE',
})
await refresh()
editingId.value = null
method: "DELETE",
});
await refresh();
editingId.value = null;
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -75,8 +75,8 @@
icon-only
@click="
() => {
revokingId = authorization.app_id
$refs.modal_confirm.show()
revokingId = authorization.app_id;
$refs.modal_confirm.show();
}
"
>
@@ -88,88 +88,88 @@
</div>
</template>
<script setup>
import { Button, ConfirmModal, Avatar } from '@modrinth/ui'
import { TrashIcon, CheckIcon } from '@modrinth/assets'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { useScopes } from '~/composables/auth/scopes.ts'
import { Button, ConfirmModal, Avatar } from "@modrinth/ui";
import { TrashIcon, CheckIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import { useScopes } from "~/composables/auth/scopes.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const { scopesToDefinitions } = useScopes()
const { scopesToDefinitions } = useScopes();
const revokingId = ref(null)
const revokingId = ref(null);
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: 'Authorizations - Modrinth',
})
title: "Authorizations - Modrinth",
});
const { data: usersApps, refresh } = await useAsyncData('userAuthorizations', () =>
const { data: usersApps, refresh } = await useAsyncData("userAuthorizations", () =>
useBaseFetch(`oauth/authorizations`, {
internal: true,
})
)
}),
);
const { data: appInformation } = await useAsyncData(
'appInfo',
"appInfo",
() =>
useBaseFetch('oauth/apps', {
useBaseFetch("oauth/apps", {
internal: true,
query: {
ids: usersApps.value.map((c) => c.app_id).join(','),
ids: usersApps.value.map((c) => c.app_id).join(","),
},
}),
{
watch: usersApps,
}
)
},
);
const { data: appCreatorsInformation } = await useAsyncData(
'appCreatorsInfo',
"appCreatorsInfo",
() =>
useBaseFetch('users', {
useBaseFetch("users", {
query: {
ids: JSON.stringify(appInformation.value.map((c) => c.created_by)),
},
}),
{
watch: appInformation,
}
)
},
);
const appInfoLookup = computed(() => {
return usersApps.value.map((app) => {
const info = appInformation.value.find((c) => c.id === app.app_id)
const owner = appCreatorsInformation.value.find((c) => c.id === info.created_by)
const info = appInformation.value.find((c) => c.id === app.app_id);
const owner = appCreatorsInformation.value.find((c) => c.id === info.created_by);
return {
...app,
app: info || null,
owner: owner || null,
}
})
})
};
});
});
async function revokeApp(id) {
try {
await useBaseFetch(`oauth/authorizations`, {
internal: true,
method: 'DELETE',
method: "DELETE",
query: {
client_id: id,
},
})
revokingId.value = null
await refresh()
});
revokingId.value = null;
await refresh();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
}
</script>

View File

@@ -229,234 +229,234 @@
</template>
<script setup>
import { CodeIcon, RadioButtonIcon, RadioButtonChecked, SunIcon, MoonIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { formatProjectType } from '~/plugins/shorthands.js'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import { DARK_THEMES } from '~/composables/theme.js'
import { CodeIcon, RadioButtonIcon, RadioButtonChecked, SunIcon, MoonIcon } from "@modrinth/assets";
import { Button } from "@modrinth/ui";
import { formatProjectType } from "~/plugins/shorthands.js";
import MessageBanner from "~/components/ui/MessageBanner.vue";
import { DARK_THEMES } from "~/composables/theme.js";
useHead({
title: 'Display settings - Modrinth',
})
title: "Display settings - Modrinth",
});
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const developerModeBanner = defineMessages({
description: {
id: 'settings.display.banner.developer-mode.description',
id: "settings.display.banner.developer-mode.description",
defaultMessage:
"<strong>Developer mode</strong> is active. This will allow you to view the internal IDs of various things throughout Modrinth that may be helpful if you're a developer using the Modrinth API. Click on the Modrinth logo at the bottom of the page 5 times to toggle developer mode.",
},
deactivate: {
id: 'settings.display.banner.developer-mode.button',
defaultMessage: 'Deactivate developer mode',
id: "settings.display.banner.developer-mode.button",
defaultMessage: "Deactivate developer mode",
},
})
});
const colorTheme = defineMessages({
title: {
id: 'settings.display.theme.title',
defaultMessage: 'Color theme',
id: "settings.display.theme.title",
defaultMessage: "Color theme",
},
description: {
id: 'settings.display.theme.description',
defaultMessage: 'Select your preferred color theme for Modrinth on this device.',
id: "settings.display.theme.description",
defaultMessage: "Select your preferred color theme for Modrinth on this device.",
},
system: {
id: 'settings.display.theme.system',
defaultMessage: 'Sync with system',
id: "settings.display.theme.system",
defaultMessage: "Sync with system",
},
light: {
id: 'settings.display.theme.light',
defaultMessage: 'Light',
id: "settings.display.theme.light",
defaultMessage: "Light",
},
dark: {
id: 'settings.display.theme.dark',
defaultMessage: 'Dark',
id: "settings.display.theme.dark",
defaultMessage: "Dark",
},
oled: {
id: 'settings.display.theme.oled',
defaultMessage: 'OLED',
id: "settings.display.theme.oled",
defaultMessage: "OLED",
},
retro: {
id: 'settings.display.theme.retro',
defaultMessage: 'Retro',
id: "settings.display.theme.retro",
defaultMessage: "Retro",
},
preferredLight: {
id: 'settings.display.theme.preferred-light-theme',
defaultMessage: 'Preferred light theme',
id: "settings.display.theme.preferred-light-theme",
defaultMessage: "Preferred light theme",
},
preferredDark: {
id: 'settings.display.theme.preferred-dark-theme',
defaultMessage: 'Preferred dark theme',
id: "settings.display.theme.preferred-dark-theme",
defaultMessage: "Preferred dark theme",
},
})
});
const projectListLayouts = defineMessages({
title: {
id: 'settings.display.project-list-layouts.title',
defaultMessage: 'Project list layouts',
id: "settings.display.project-list-layouts.title",
defaultMessage: "Project list layouts",
},
description: {
id: 'settings.display.project-list-layouts.description',
id: "settings.display.project-list-layouts.description",
defaultMessage:
'Select your preferred layout for each page that displays project lists on this device.',
"Select your preferred layout for each page that displays project lists on this device.",
},
mod: {
id: 'settings.display.project-list-layouts.mod',
defaultMessage: 'Mods page',
id: "settings.display.project-list-layouts.mod",
defaultMessage: "Mods page",
},
plugin: {
id: 'settings.display.project-list-layouts.plugin',
defaultMessage: 'Plugins page',
id: "settings.display.project-list-layouts.plugin",
defaultMessage: "Plugins page",
},
datapack: {
id: 'settings.display.project-list-layouts.datapack',
defaultMessage: 'Data Packs page',
id: "settings.display.project-list-layouts.datapack",
defaultMessage: "Data Packs page",
},
shader: {
id: 'settings.display.project-list-layouts.shader',
defaultMessage: 'Shaders page',
id: "settings.display.project-list-layouts.shader",
defaultMessage: "Shaders page",
},
resourcepack: {
id: 'settings.display.project-list-layouts.resourcepack',
defaultMessage: 'Resource Packs page',
id: "settings.display.project-list-layouts.resourcepack",
defaultMessage: "Resource Packs page",
},
modpack: {
id: 'settings.display.project-list-layouts.modpack',
defaultMessage: 'Modpacks page',
id: "settings.display.project-list-layouts.modpack",
defaultMessage: "Modpacks page",
},
user: {
id: 'settings.display.project-list-layouts.user',
defaultMessage: 'User profile pages',
id: "settings.display.project-list-layouts.user",
defaultMessage: "User profile pages",
},
})
});
const toggleFeatures = defineMessages({
title: {
id: 'settings.display.flags.title',
defaultMessage: 'Toggle features',
id: "settings.display.flags.title",
defaultMessage: "Toggle features",
},
description: {
id: 'settings.display.flags.description',
defaultMessage: 'Enable or disable certain features on this device.',
id: "settings.display.flags.description",
defaultMessage: "Enable or disable certain features on this device.",
},
advancedRenderingTitle: {
id: 'settings.display.sidebar.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
id: "settings.display.sidebar.advanced-rendering.title",
defaultMessage: "Advanced rendering",
},
advancedRenderingDescription: {
id: 'settings.display.sidebar.advanced-rendering.description',
id: "settings.display.sidebar.advanced-rendering.description",
defaultMessage:
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
"Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.",
},
externalLinksNewTabTitle: {
id: 'settings.display.sidebar.external-links-new-tab.title',
defaultMessage: 'Open external links in new tab',
id: "settings.display.sidebar.external-links-new-tab.title",
defaultMessage: "Open external links in new tab",
},
externalLinksNewTabDescription: {
id: 'settings.display.sidebar.external-links-new-tab.description',
id: "settings.display.sidebar.external-links-new-tab.description",
defaultMessage:
'Make links which go outside of Modrinth open in a new tab. No matter this setting, links on the same domain and in Markdown descriptions will open in the same tab, and links on ads and edit pages will open in a new tab.',
"Make links which go outside of Modrinth open in a new tab. No matter this setting, links on the same domain and in Markdown descriptions will open in the same tab, and links on ads and edit pages will open in a new tab.",
},
hideModrinthAppPromosTitle: {
id: 'settings.display.sidebar.hide-app-promos.title',
defaultMessage: 'Hide Modrinth App promotions',
id: "settings.display.sidebar.hide-app-promos.title",
defaultMessage: "Hide Modrinth App promotions",
},
hideModrinthAppPromosDescription: {
id: 'settings.display.sidebar.hide-app-promos.description',
id: "settings.display.sidebar.hide-app-promos.description",
defaultMessage:
'Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer.',
},
rightAlignedSearchSidebarTitle: {
id: 'settings.display.sidebar.right-aligned-search-sidebar.title',
defaultMessage: 'Right-aligned search sidebar',
id: "settings.display.sidebar.right-aligned-search-sidebar.title",
defaultMessage: "Right-aligned search sidebar",
},
rightAlignedSearchSidebarDescription: {
id: 'settings.display.sidebar.right-aligned-search-sidebar.description',
defaultMessage: 'Aligns the search filters sidebar to the right of the search results.',
id: "settings.display.sidebar.right-aligned-search-sidebar.description",
defaultMessage: "Aligns the search filters sidebar to the right of the search results.",
},
rightAlignedProjectSidebarTitle: {
id: 'settings.display.sidebar.right-aligned-project-sidebar.title',
defaultMessage: 'Right-aligned project sidebar',
id: "settings.display.sidebar.right-aligned-project-sidebar.title",
defaultMessage: "Right-aligned project sidebar",
},
rightAlignedProjectSidebarDescription: {
id: 'settings.display.sidebar.right-aligned-project-sidebar.description',
id: "settings.display.sidebar.right-aligned-project-sidebar.description",
defaultMessage: "Aligns the project details sidebar to the right of the page's content.",
},
})
});
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useTags()
const cosmetics = useCosmetics();
const flags = useFeatureFlags();
const tags = useTags();
const systemTheme = ref('light')
const systemTheme = ref("light");
const theme = useTheme()
const theme = useTheme();
const themeOptions = computed(() => {
const options = ['system', 'light', 'dark', 'oled']
if (flags.value.developerMode || theme.value.preference === 'retro') {
options.push('retro')
const options = ["system", "light", "dark", "oled"];
if (flags.value.developerMode || theme.value.preference === "retro") {
options.push("retro");
}
return options
})
return options;
});
onMounted(() => {
updateSystemTheme()
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (event) => {
setSystemTheme(event.matches)
})
})
updateSystemTheme();
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", (event) => {
setSystemTheme(event.matches);
});
});
function updateSystemTheme() {
const query = window.matchMedia('(prefers-color-scheme: light)')
setSystemTheme(query.matches)
const query = window.matchMedia("(prefers-color-scheme: light)");
setSystemTheme(query.matches);
}
function setSystemTheme(light) {
if (light) {
systemTheme.value = 'light'
systemTheme.value = "light";
} else {
systemTheme.value = cosmetics.value.preferredDarkTheme ?? 'dark'
systemTheme.value = cosmetics.value.preferredDarkTheme ?? "dark";
}
}
function updateColorTheme(value) {
if (DARK_THEMES.includes(value)) {
cosmetics.value.preferredDarkTheme = value
saveCosmetics()
updateSystemTheme()
cosmetics.value.preferredDarkTheme = value;
saveCosmetics();
updateSystemTheme();
}
updateTheme(value, true)
updateTheme(value, true);
}
function disableDeveloperMode() {
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
flags.value.developerMode = !flags.value.developerMode;
saveFeatureFlags();
addNotification({
group: 'main',
title: 'Developer mode deactivated',
text: 'Developer mode has been disabled',
type: 'success',
})
group: "main",
title: "Developer mode deactivated",
text: "Developer mode has been disabled",
type: "success",
});
}
const listTypes = computed(() => {
const types = tags.value.projectTypes.map((type) => {
return {
id: type.id,
name: formatProjectType(type.id) + 's',
display: 'the ' + formatProjectType(type.id).toLowerCase() + 's search page',
}
})
name: formatProjectType(type.id) + "s",
display: "the " + formatProjectType(type.id).toLowerCase() + "s search page",
};
});
types.push({
id: 'user',
name: 'User profiles',
display: 'user pages',
})
return types
})
id: "user",
name: "User profiles",
display: "user pages",
});
return types;
});
</script>
<style scoped lang="scss">
.preview-radio {
@@ -525,7 +525,7 @@ const listTypes = computed(() => {
margin: 0;
padding: 1rem;
display: grid;
grid-template: 'icon text1' 'icon text2';
grid-template: "icon text1" "icon text2";
grid-template-columns: auto 1fr;
gap: 0.5rem;
outline: 2px solid transparent;

View File

@@ -1,151 +1,151 @@
<script setup lang="ts">
import Fuse from 'fuse.js/dist/fuse.basic'
import RadioButtonIcon from '~/assets/images/utils/radio-button.svg?component'
import RadioButtonCheckedIcon from '~/assets/images/utils/radio-button-checked.svg?component'
import WarningIcon from '~/assets/images/utils/issues.svg?component'
import { isModifierKeyDown } from '~/helpers/events.ts'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import Fuse from "fuse.js/dist/fuse.basic";
import RadioButtonIcon from "~/assets/images/utils/radio-button.svg?component";
import RadioButtonCheckedIcon from "~/assets/images/utils/radio-button-checked.svg?component";
import WarningIcon from "~/assets/images/utils/issues.svg?component";
import { isModifierKeyDown } from "~/helpers/events.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
const messages = defineMessages({
languagesDescription: {
id: 'settings.language.description',
id: "settings.language.description",
defaultMessage:
'Choose your preferred language for the site. Translations are contributed by volunteers <crowdin-link>on Crowdin</crowdin-link>.',
"Choose your preferred language for the site. Translations are contributed by volunteers <crowdin-link>on Crowdin</crowdin-link>.",
},
automaticLocale: {
id: 'settings.language.languages.automatic',
defaultMessage: 'Sync with the system language',
id: "settings.language.languages.automatic",
defaultMessage: "Sync with the system language",
},
noResults: {
id: 'settings.language.languages.search.no-results',
defaultMessage: 'No languages match your search.',
id: "settings.language.languages.search.no-results",
defaultMessage: "No languages match your search.",
},
searchFieldDescription: {
id: 'settings.language.languages.search-field.description',
defaultMessage: 'Submit to focus the first search result',
id: "settings.language.languages.search-field.description",
defaultMessage: "Submit to focus the first search result",
},
searchFieldPlaceholder: {
id: 'settings.language.languages.search-field.placeholder',
defaultMessage: 'Search for a language...',
id: "settings.language.languages.search-field.placeholder",
defaultMessage: "Search for a language...",
},
searchResultsAnnouncement: {
id: 'settings.language.languages.search-results-announcement',
id: "settings.language.languages.search-results-announcement",
defaultMessage:
'{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.',
"{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.",
},
loadFailed: {
id: 'settings.language.languages.load-failed',
defaultMessage: 'Cannot load this language. Try again in a bit.',
id: "settings.language.languages.load-failed",
defaultMessage: "Cannot load this language. Try again in a bit.",
},
languageLabelApplying: {
id: 'settings.language.languages.language-label-applying',
defaultMessage: '{label}. Applying...',
id: "settings.language.languages.language-label-applying",
defaultMessage: "{label}. Applying...",
},
languageLabelError: {
id: 'settings.language.languages.language-label-error',
defaultMessage: '{label}. Error',
id: "settings.language.languages.language-label-error",
defaultMessage: "{label}. Error",
},
})
});
const categoryNames = defineMessages({
auto: {
id: 'settings.language.categories.auto',
defaultMessage: 'Automatic',
id: "settings.language.categories.auto",
defaultMessage: "Automatic",
},
default: {
id: 'settings.language.categories.default',
defaultMessage: 'Standard languages',
id: "settings.language.categories.default",
defaultMessage: "Standard languages",
},
fun: {
id: 'settings.language.categories.fun',
defaultMessage: 'Fun languages',
id: "settings.language.categories.fun",
defaultMessage: "Fun languages",
},
experimental: {
id: 'settings.language.categories.experimental',
defaultMessage: 'Experimental languages',
id: "settings.language.categories.experimental",
defaultMessage: "Experimental languages",
},
searchResult: {
id: 'settings.language.categories.search-result',
defaultMessage: 'Search results',
id: "settings.language.categories.search-result",
defaultMessage: "Search results",
},
})
});
type Category = keyof typeof categoryNames
type Category = keyof typeof categoryNames;
const categoryOrder: Category[] = ['auto', 'default', 'fun', 'experimental']
const categoryOrder: Category[] = ["auto", "default", "fun", "experimental"];
function normalizeCategoryName(name?: string): keyof typeof categoryNames {
switch (name) {
case 'auto':
case 'fun':
case 'experimental':
return name
case "auto":
case "fun":
case "experimental":
return name;
default:
return 'default'
return "default";
}
}
type LocaleBase = {
category: Category
tag: string
searchTerms?: string[]
}
category: Category;
tag: string;
searchTerms?: string[];
};
type AutomaticLocale = LocaleBase & {
auto: true
}
auto: true;
};
type CommonLocale = LocaleBase & {
auto?: never
displayName: string
defaultName: string
translatedName: string
}
auto?: never;
displayName: string;
defaultName: string;
translatedName: string;
};
type Locale = AutomaticLocale | CommonLocale
type Locale = AutomaticLocale | CommonLocale;
const $defaultNames = useDisplayNames(() => vintl.defaultLocale)
const $defaultNames = useDisplayNames(() => vintl.defaultLocale);
const $translatedNames = useDisplayNames(() => vintl.locale)
const $translatedNames = useDisplayNames(() => vintl.locale);
const $locales = computed(() => {
const locales: Locale[] = []
const locales: Locale[] = [];
locales.push({
auto: true,
tag: 'auto',
category: 'auto',
tag: "auto",
category: "auto",
searchTerms: [
'automatic',
'Sync with the system language',
"automatic",
"Sync with the system language",
formatMessage(messages.automaticLocale),
],
})
});
for (const locale of vintl.availableLocales) {
let displayName = locale.meta?.displayName
let displayName = locale.meta?.displayName;
if (displayName == null) {
displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag
displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag;
}
let defaultName = vintl.defaultResources['languages.json']?.[locale.tag]
let defaultName = vintl.defaultResources["languages.json"]?.[locale.tag];
if (defaultName == null) {
defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag
defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag;
}
let translatedName = vintl.resources['languages.json']?.[locale.tag]
let translatedName = vintl.resources["languages.json"]?.[locale.tag];
if (translatedName == null) {
translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag
translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag;
}
let searchTerms = locale.meta?.searchTerms
if (searchTerms === '-') searchTerms = undefined
let searchTerms = locale.meta?.searchTerms;
if (searchTerms === "-") searchTerms = undefined;
locales.push({
tag: locale.tag,
@@ -153,132 +153,132 @@ const $locales = computed(() => {
displayName,
defaultName,
translatedName,
searchTerms: searchTerms?.split('\n'),
})
searchTerms: searchTerms?.split("\n"),
});
}
return locales
})
return locales;
});
const $query = ref('')
const $query = ref("");
const isQueryEmpty = () => $query.value.trim().length === 0
const isQueryEmpty = () => $query.value.trim().length === 0;
const fuse = new Fuse<Locale>([], {
keys: ['tag', 'displayName', 'translatedName', 'englishName', 'searchTerms'],
keys: ["tag", "displayName", "translatedName", "englishName", "searchTerms"],
threshold: 0.4,
distance: 100,
})
});
watchSyncEffect(() => fuse.setCollection($locales.value))
watchSyncEffect(() => fuse.setCollection($locales.value));
const $categories = computed(() => {
const categories = new Map<Category, Locale[]>()
const categories = new Map<Category, Locale[]>();
for (const category of categoryOrder) categories.set(category, [])
for (const category of categoryOrder) categories.set(category, []);
for (const locale of $locales.value) {
let categoryLocales = categories.get(locale.category)
let categoryLocales = categories.get(locale.category);
if (categoryLocales == null) {
categoryLocales = []
categories.set(locale.category, categoryLocales)
categoryLocales = [];
categories.set(locale.category, categoryLocales);
}
categoryLocales.push(locale)
categoryLocales.push(locale);
}
for (const categoryKey of [...categories.keys()]) {
if (categories.get(categoryKey)?.length === 0) {
categories.delete(categoryKey)
categories.delete(categoryKey);
}
}
return categories
})
return categories;
});
const $searchResults = computed(() => {
return new Map<Category, Locale[]>([
['searchResult', isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
])
})
["searchResult", isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
]);
});
const $displayCategories = computed(() =>
isQueryEmpty() ? $categories.value : $searchResults.value
)
isQueryEmpty() ? $categories.value : $searchResults.value,
);
const $changingTo = ref<string | undefined>()
const $changingTo = ref<string | undefined>();
const isChanging = () => $changingTo.value != null
const isChanging = () => $changingTo.value != null;
const $failedLocale = ref<string>()
const $failedLocale = ref<string>();
const $activeLocale = computed(() => {
if ($changingTo.value != null) return $changingTo.value
return vintl.automatic ? 'auto' : vintl.locale
})
if ($changingTo.value != null) return $changingTo.value;
return vintl.automatic ? "auto" : vintl.locale;
});
async function changeLocale(value: string) {
if ($activeLocale.value === value) return
if ($activeLocale.value === value) return;
$changingTo.value = value
$changingTo.value = value;
try {
await vintl.changeLocale(value)
$failedLocale.value = undefined
await vintl.changeLocale(value);
$failedLocale.value = undefined;
} catch (err) {
$failedLocale.value = value
$failedLocale.value = value;
} finally {
$changingTo.value = undefined
$changingTo.value = undefined;
}
}
const $languagesList = ref<HTMLDivElement | undefined>()
const $languagesList = ref<HTMLDivElement | undefined>();
function onSearchKeydown(e: KeyboardEvent) {
if (e.key !== 'Enter' || isModifierKeyDown(e)) return
if (e.key !== "Enter" || isModifierKeyDown(e)) return;
const focusableTarget = $languagesList.value?.querySelector(
'input, [tabindex]:not([tabindex="-1"])'
) as HTMLElement | undefined
'input, [tabindex]:not([tabindex="-1"])',
) as HTMLElement | undefined;
focusableTarget?.focus()
focusableTarget?.focus();
}
function onItemKeydown(e: KeyboardEvent, locale: Locale) {
switch (e.key) {
case 'Enter':
case ' ':
break
case "Enter":
case " ":
break;
default:
return
return;
}
if (isModifierKeyDown(e) || isChanging()) return
if (isModifierKeyDown(e) || isChanging()) return;
changeLocale(locale.tag)
changeLocale(locale.tag);
}
function onItemClick(e: MouseEvent, locale: Locale) {
if (isModifierKeyDown(e) || isChanging()) return
if (isModifierKeyDown(e) || isChanging()) return;
changeLocale(locale.tag)
changeLocale(locale.tag);
}
function getItemLabel(locale: Locale) {
const label = locale.auto
? formatMessage(messages.automaticLocale)
: `${locale.translatedName}. ${locale.displayName}`
: `${locale.translatedName}. ${locale.displayName}`;
if ($changingTo.value === locale.tag) {
return formatMessage(messages.languageLabelApplying, { label })
return formatMessage(messages.languageLabelApplying, { label });
}
if ($failedLocale.value === locale.tag) {
return formatMessage(messages.languageLabelError, { label })
return formatMessage(messages.languageLabelError, { label });
}
return label
return label;
}
</script>
@@ -317,9 +317,9 @@ function getItemLabel(locale: Locale) {
<div id="language-search-results-announcements" class="visually-hidden" aria-live="polite">
{{
isQueryEmpty()
? ''
? ""
: formatMessage(messages.searchResultsAnnouncement, {
matches: $searchResults.get('searchResult')?.length ?? 0,
matches: $searchResults.get("searchResult")?.length ?? 0,
})
}}
</div>
@@ -404,7 +404,7 @@ function getItemLabel(locale: Locale) {
position: relative;
overflow: hidden;
&:not([aria-disabled='true']):hover {
&:not([aria-disabled="true"]):hover {
border-color: var(--color-button-bg-hover);
}
@@ -422,7 +422,7 @@ function getItemLabel(locale: Locale) {
}
&.pending::after {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@@ -465,7 +465,7 @@ function getItemLabel(locale: Locale) {
}
}
&[aria-disabled='true']:not(.pending) {
&[aria-disabled="true"]:not(.pending) {
opacity: 0.8;
pointer-events: none;
cursor: default;

View File

@@ -80,11 +80,11 @@
class="btn btn-primary"
@click="
() => {
name = null
scopesVal = 0
expires = null
editPatIndex = null
$refs.patModal.show()
name = null;
scopesVal = 0;
expires = null;
editPatIndex = null;
$refs.patModal.show();
}
"
>
@@ -176,11 +176,11 @@
class="iconified-button raised-button"
@click="
() => {
editPatIndex = index
name = pat.name
scopesVal = pat.scopes
expires = $dayjs(pat.expires).format('YYYY-MM-DD')
$refs.patModal.show()
editPatIndex = index;
name = pat.name;
scopesVal = pat.scopes;
expires = $dayjs(pat.expires).format('YYYY-MM-DD');
$refs.patModal.show();
}
"
>
@@ -190,8 +190,8 @@
class="iconified-button raised-button"
@click="
() => {
deletePatIndex = pat.id
$refs.modal_confirm.show()
deletePatIndex = pat.id;
$refs.modal_confirm.show();
}
"
>
@@ -202,199 +202,199 @@
</div>
</template>
<script setup>
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from '@modrinth/assets'
import { Checkbox, ConfirmModal } from '@modrinth/ui'
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { Checkbox, ConfirmModal } from "@modrinth/ui";
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import {
hasScope,
scopeList,
toggleScope,
useScopes,
getScopeValue,
} from '~/composables/auth/scopes.ts'
} from "~/composables/auth/scopes.ts";
import CopyCode from '~/components/ui/CopyCode.vue'
import Modal from '~/components/ui/Modal.vue'
import CopyCode from "~/components/ui/CopyCode.vue";
import Modal from "~/components/ui/Modal.vue";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime()
const formatRelativeTime = useRelativeTime();
const createModalMessages = defineMessages({
createTitle: {
id: 'settings.pats.modal.create.title',
defaultMessage: 'Create personal access token',
id: "settings.pats.modal.create.title",
defaultMessage: "Create personal access token",
},
editTitle: {
id: 'settings.pats.modal.edit.title',
defaultMessage: 'Edit personal access token',
id: "settings.pats.modal.edit.title",
defaultMessage: "Edit personal access token",
},
nameLabel: {
id: 'settings.pats.modal.create.name.label',
defaultMessage: 'Name',
id: "settings.pats.modal.create.name.label",
defaultMessage: "Name",
},
namePlaceholder: {
id: 'settings.pats.modal.create.name.placeholder',
id: "settings.pats.modal.create.name.placeholder",
defaultMessage: "Enter the PAT's name...",
},
expiresLabel: {
id: 'settings.pats.modal.create.expires.label',
defaultMessage: 'Expires',
id: "settings.pats.modal.create.expires.label",
defaultMessage: "Expires",
},
action: {
id: 'settings.pats.modal.create.action',
defaultMessage: 'Create PAT',
id: "settings.pats.modal.create.action",
defaultMessage: "Create PAT",
},
})
});
const deleteModalMessages = defineMessages({
title: {
id: 'settings.pats.modal.delete.title',
defaultMessage: 'Are you sure you want to delete this token?',
id: "settings.pats.modal.delete.title",
defaultMessage: "Are you sure you want to delete this token?",
},
description: {
id: 'settings.pats.modal.delete.description',
defaultMessage: 'This will remove this token forever (like really forever).',
id: "settings.pats.modal.delete.description",
defaultMessage: "This will remove this token forever (like really forever).",
},
action: {
id: 'settings.pats.modal.delete.action',
defaultMessage: 'Delete this token',
id: "settings.pats.modal.delete.action",
defaultMessage: "Delete this token",
},
})
});
const messages = defineMessages({
description: {
id: 'settings.pats.description',
id: "settings.pats.description",
defaultMessage:
"PATs can be used to access Modrinth's API. For more information, see <doc-link>Modrinth's API documentation</doc-link>. They can be created and revoked at any time.",
},
create: {
id: 'settings.pats.action.create',
defaultMessage: 'Create a PAT',
id: "settings.pats.action.create",
defaultMessage: "Create a PAT",
},
})
});
const tokenMessages = defineMessages({
edit: {
id: 'settings.pats.token.action.edit',
defaultMessage: 'Edit token',
id: "settings.pats.token.action.edit",
defaultMessage: "Edit token",
},
revoke: {
id: 'settings.pats.token.action.revoke',
defaultMessage: 'Revoke token',
id: "settings.pats.token.action.revoke",
defaultMessage: "Revoke token",
},
lastUsed: {
id: 'settings.pats.token.last-used',
defaultMessage: 'Last used {ago}',
id: "settings.pats.token.last-used",
defaultMessage: "Last used {ago}",
},
neverUsed: {
id: 'settings.pats.token.never-used',
defaultMessage: 'Never used',
id: "settings.pats.token.never-used",
defaultMessage: "Never used",
},
expiresIn: {
id: 'settings.pats.token.expires-in',
defaultMessage: 'Expires {inTime}',
id: "settings.pats.token.expires-in",
defaultMessage: "Expires {inTime}",
},
expiredAgo: {
id: 'settings.pats.token.expired-ago',
defaultMessage: 'Expired {ago}',
id: "settings.pats.token.expired-ago",
defaultMessage: "Expired {ago}",
},
})
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: `${formatMessage(commonSettingsMessages.pats)} - Modrinth`,
})
});
const data = useNuxtApp()
const { scopesToLabels } = useScopes()
const patModal = ref()
const data = useNuxtApp();
const { scopesToLabels } = useScopes();
const patModal = ref();
const editPatIndex = ref(null)
const editPatIndex = ref(null);
const name = ref(null)
const scopesVal = ref(BigInt(0))
const expires = ref(null)
const name = ref(null);
const scopesVal = ref(BigInt(0));
const expires = ref(null);
const deletePatIndex = ref(null)
const deletePatIndex = ref(null);
const loading = ref(false)
const loading = ref(false);
const { data: pats, refresh } = await useAsyncData('pat', () => useBaseFetch('pat'))
const { data: pats, refresh } = await useAsyncData("pat", () => useBaseFetch("pat"));
async function createPat() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
const res = await useBaseFetch('pat', {
method: 'POST',
const res = await useBaseFetch("pat", {
method: "POST",
body: {
name: name.value,
scopes: Number(scopesVal.value),
expires: data.$dayjs(expires.value).toISOString(),
},
})
pats.value.push(res)
patModal.value.hide()
});
pats.value.push(res);
patModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function editPat() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
await useBaseFetch(`pat/${pats.value[editPatIndex.value].id}`, {
method: 'PATCH',
method: "PATCH",
body: {
name: name.value,
scopes: Number(scopesVal.value),
expires: data.$dayjs(expires.value).toISOString(),
},
})
await refresh()
patModal.value.hide()
});
await refresh();
patModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function removePat(id) {
startLoading()
startLoading();
try {
pats.value = pats.value.filter((x) => x.id !== id)
pats.value = pats.value.filter((x) => x.id !== id);
await useBaseFetch(`pat/${id}`, {
method: 'DELETE',
})
await refresh()
method: "DELETE",
});
await refresh();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -36,8 +36,8 @@
v-if="previewImage"
:action="
() => {
icon = null
previewImage = null
icon = null;
previewImage = null;
}
"
>
@@ -86,137 +86,137 @@
</template>
<script setup>
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from '@modrinth/assets'
import { Avatar, FileInput, Button } from '@modrinth/ui'
import { commonMessages } from '~/utils/common-messages.ts'
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from "@modrinth/assets";
import { Avatar, FileInput, Button } from "@modrinth/ui";
import { commonMessages } from "~/utils/common-messages.ts";
useHead({
title: 'Profile settings - Modrinth',
})
title: "Profile settings - Modrinth",
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
title: {
id: 'settings.profile.profile-info',
defaultMessage: 'Profile information',
id: "settings.profile.profile-info",
defaultMessage: "Profile information",
},
description: {
id: 'settings.profile.description',
id: "settings.profile.description",
defaultMessage:
'Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.',
"Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.",
},
profilePicture: {
id: 'settings.profile.profile-picture.title',
defaultMessage: 'Profile picture',
id: "settings.profile.profile-picture.title",
defaultMessage: "Profile picture",
},
profilePictureReset: {
id: 'settings.profile.profile-picture.reset',
defaultMessage: 'Reset',
id: "settings.profile.profile-picture.reset",
defaultMessage: "Reset",
},
usernameTitle: {
id: 'settings.profile.username.title',
defaultMessage: 'Username',
id: "settings.profile.username.title",
defaultMessage: "Username",
},
usernameDescription: {
id: 'settings.profile.username.description',
defaultMessage: 'A unique case-insensitive name to identify your profile.',
id: "settings.profile.username.description",
defaultMessage: "A unique case-insensitive name to identify your profile.",
},
bioTitle: {
id: 'settings.profile.bio.title',
defaultMessage: 'Bio',
id: "settings.profile.bio.title",
defaultMessage: "Bio",
},
bioDescription: {
id: 'settings.profile.bio.description',
defaultMessage: 'A short description to tell everyone a little bit about you.',
id: "settings.profile.bio.description",
defaultMessage: "A short description to tell everyone a little bit about you.",
},
})
});
const auth = await useAuth()
const auth = await useAuth();
const username = ref(auth.value.user.username)
const bio = ref(auth.value.user.bio)
const avatarUrl = ref(auth.value.user.avatar_url)
const icon = shallowRef(null)
const previewImage = shallowRef(null)
const saved = ref(false)
const username = ref(auth.value.user.username);
const bio = ref(auth.value.user.bio);
const avatarUrl = ref(auth.value.user.avatar_url);
const icon = shallowRef(null);
const previewImage = shallowRef(null);
const saved = ref(false);
const hasUnsavedChanges = computed(
() =>
username.value !== auth.value.user.username ||
bio.value !== auth.value.user.bio ||
previewImage.value
)
previewImage.value,
);
function showPreviewImage(files) {
const reader = new FileReader()
icon.value = files[0]
reader.readAsDataURL(icon.value)
const reader = new FileReader();
icon.value = files[0];
reader.readAsDataURL(icon.value);
reader.onload = (event) => {
previewImage.value = event.target.result
}
previewImage.value = event.target.result;
};
}
function cancel() {
icon.value = null
previewImage.value = null
username.value = auth.value.user.username
bio.value = auth.value.user.bio
icon.value = null;
previewImage.value = null;
username.value = auth.value.user.username;
bio.value = auth.value.user.bio;
}
async function saveChanges() {
startLoading()
startLoading();
try {
if (icon.value) {
await useBaseFetch(
`user/${auth.value.user.id}/icon?ext=${
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
icon.value.type.split("/")[icon.value.type.split("/").length - 1]
}`,
{
method: 'PATCH',
method: "PATCH",
body: icon.value,
}
)
icon.value = null
previewImage.value = null
},
);
icon.value = null;
previewImage.value = null;
}
const body = {}
const body = {};
if (auth.value.user.username !== username.value) {
body.username = username.value
body.username = username.value;
}
if (auth.value.user.bio !== bio.value) {
body.bio = bio.value
body.bio = bio.value;
}
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'PATCH',
method: "PATCH",
body,
})
await useAuth(auth.value.token)
avatarUrl.value = auth.value.user.avatar_url
saved.value = true
});
await useAuth(auth.value.token);
avatarUrl.value = auth.value.user.avatar_url;
saved.value = true;
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err
? err.data
? err.data.description
? err.data.description
: err.data
: err
: 'aaaaahhh',
type: 'error',
})
: "aaaaahhh",
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -56,74 +56,74 @@
</div>
</template>
<script setup>
import { XIcon } from '@modrinth/assets'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { XIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime();
const messages = defineMessages({
currentSessionLabel: {
id: 'settings.sessions.current-session',
defaultMessage: 'Current session',
id: "settings.sessions.current-session",
defaultMessage: "Current session",
},
revokeSessionButton: {
id: 'settings.sessions.action.revoke-session',
defaultMessage: 'Revoke session',
id: "settings.sessions.action.revoke-session",
defaultMessage: "Revoke session",
},
createdAgoLabel: {
id: 'settings.sessions.created-ago',
defaultMessage: 'Created {ago}',
id: "settings.sessions.created-ago",
defaultMessage: "Created {ago}",
},
sessionsDescription: {
id: 'settings.sessions.description',
id: "settings.sessions.description",
defaultMessage:
"Here are all the devices that are currently logged in with your Modrinth account. You can log out of each one individually.\n\nIf you see an entry you don't recognize, log out of that device and change your Modrinth account password immediately.",
},
lastAccessedAgoLabel: {
id: 'settings.sessions.last-accessed-ago',
defaultMessage: 'Last accessed {ago}',
id: "settings.sessions.last-accessed-ago",
defaultMessage: "Last accessed {ago}",
},
unknownOsLabel: {
id: 'settings.sessions.unknown-os',
defaultMessage: 'Unknown OS',
id: "settings.sessions.unknown-os",
defaultMessage: "Unknown OS",
},
unknownPlatformLabel: {
id: 'settings.sessions.unknown-platform',
defaultMessage: 'Unknown platform',
id: "settings.sessions.unknown-platform",
defaultMessage: "Unknown platform",
},
})
});
useHead({
title: () => `${formatMessage(commonSettingsMessages.sessions)} - Modrinth`,
})
});
const data = useNuxtApp()
const { data: sessions, refresh } = await useAsyncData('session/list', () =>
useBaseFetch('session/list')
)
const data = useNuxtApp();
const { data: sessions, refresh } = await useAsyncData("session/list", () =>
useBaseFetch("session/list"),
);
async function revokeSession(id) {
startLoading()
startLoading();
try {
sessions.value = sessions.value.filter((x) => x.id !== id)
sessions.value = sessions.value.filter((x) => x.id !== id);
await useBaseFetch(`session/${id}`, {
method: 'DELETE',
})
await refresh()
method: "DELETE",
});
await refresh();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -133,7 +133,7 @@
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/user/${user.username}/${x}s`,
}
};
}),
]"
/>
@@ -173,7 +173,7 @@
? projects.filter(
(x) =>
x.project_type ===
route.params.projectType.substr(0, route.params.projectType.length - 1)
route.params.projectType.substr(0, route.params.projectType.length - 1),
)
: projects
)
@@ -280,109 +280,109 @@
</div>
</template>
<script setup>
import { LibraryIcon, BoxIcon, LinkIcon, LockIcon, XIcon } from '@modrinth/assets'
import { Promotion } from '@modrinth/ui'
import ProjectCard from '~/components/ui/ProjectCard.vue'
import Badge from '~/components/ui/Badge.vue'
import { reportUser } from '~/utils/report-helpers.ts'
import { LibraryIcon, BoxIcon, LinkIcon, LockIcon, XIcon } from "@modrinth/assets";
import { Promotion } from "@modrinth/ui";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import Badge from "~/components/ui/Badge.vue";
import { reportUser } from "~/utils/report-helpers.ts";
import ReportIcon from '~/assets/images/utils/report.svg?component'
import SunriseIcon from '~/assets/images/utils/sunrise.svg?component'
import DownloadIcon from '~/assets/images/utils/download.svg?component'
import SettingsIcon from '~/assets/images/utils/settings.svg?component'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import UserIcon from '~/assets/images/utils/user.svg?component'
import EditIcon from '~/assets/images/utils/edit.svg?component'
import HeartIcon from '~/assets/images/utils/heart.svg?component'
import GridIcon from '~/assets/images/utils/grid.svg?component'
import ListIcon from '~/assets/images/utils/list.svg?component'
import ImageIcon from '~/assets/images/utils/image.svg?component'
import WorldIcon from '~/assets/images/utils/world.svg?component'
import ModalCreation from '~/components/ui/ModalCreation.vue'
import NavRow from '~/components/ui/NavRow.vue'
import CopyCode from '~/components/ui/CopyCode.vue'
import Avatar from '~/components/ui/Avatar.vue'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import ReportIcon from "~/assets/images/utils/report.svg?component";
import SunriseIcon from "~/assets/images/utils/sunrise.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component";
import SettingsIcon from "~/assets/images/utils/settings.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import UserIcon from "~/assets/images/utils/user.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component";
import HeartIcon from "~/assets/images/utils/heart.svg?component";
import GridIcon from "~/assets/images/utils/grid.svg?component";
import ListIcon from "~/assets/images/utils/list.svg?component";
import ImageIcon from "~/assets/images/utils/image.svg?component";
import WorldIcon from "~/assets/images/utils/world.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import NavRow from "~/components/ui/NavRow.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Avatar from "~/components/ui/Avatar.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
const data = useNuxtApp()
const route = useNativeRoute()
const auth = await useAuth()
const cosmetics = useCosmetics()
const tags = useTags()
const data = useNuxtApp();
const route = useNativeRoute();
const auth = await useAuth();
const cosmetics = useCosmetics();
const tags = useTags();
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
const formatCompactNumber = useCompactNumber()
const formatCompactNumber = useCompactNumber();
const formatRelativeTime = useRelativeTime()
const formatRelativeTime = useRelativeTime();
const messages = defineMessages({
profileDownloadsStats: {
id: 'profile.stats.downloads',
id: "profile.stats.downloads",
defaultMessage:
'{count, plural, one {<stat>{count}</stat> download} other {<stat>{count}</stat> downloads}}',
"{count, plural, one {<stat>{count}</stat> download} other {<stat>{count}</stat> downloads}}",
},
profileProjectsFollowersStats: {
id: 'profile.stats.projects-followers',
id: "profile.stats.projects-followers",
defaultMessage:
'{count, plural, one {<stat>{count}</stat> follower} other {<stat>{count}</stat> followers}} of projects',
"{count, plural, one {<stat>{count}</stat> follower} other {<stat>{count}</stat> followers}} of projects",
},
profileJoinedAt: {
id: 'profile.joined-at',
defaultMessage: 'Joined {ago}',
id: "profile.joined-at",
defaultMessage: "Joined {ago}",
},
profileUserId: {
id: 'profile.user-id',
defaultMessage: 'User ID: {id}',
id: "profile.user-id",
defaultMessage: "User ID: {id}",
},
profileOrganizations: {
id: 'profile.label.organizations',
defaultMessage: 'Organizations',
id: "profile.label.organizations",
defaultMessage: "Organizations",
},
profileManageProjectsButton: {
id: 'profile.button.manage-projects',
defaultMessage: 'Manage projects',
id: "profile.button.manage-projects",
defaultMessage: "Manage projects",
},
profileMetaDescription: {
id: 'profile.meta.description',
id: "profile.meta.description",
defaultMessage: "Download {username}'s projects on Modrinth",
},
profileMetaDescriptionWithBio: {
id: 'profile.meta.description-with-bio',
id: "profile.meta.description-with-bio",
defaultMessage: "{bio} - Download {username}'s projects on Modrinth",
},
profileReportButton: {
id: 'profile.button.report',
defaultMessage: 'Report',
id: "profile.button.report",
defaultMessage: "Report",
},
profileNoProjectsLabel: {
id: 'profile.label.no-projects',
defaultMessage: 'This user has no projects!',
id: "profile.label.no-projects",
defaultMessage: "This user has no projects!",
},
profileNoProjectsAuthLabel: {
id: 'profile.label.no-projects-auth',
id: "profile.label.no-projects-auth",
defaultMessage:
"You don't have any projects.\nWould you like to <create-link>create one</create-link>?",
},
profileNoCollectionsLabel: {
id: 'profile.label.no-collections',
defaultMessage: 'This user has no collections!',
id: "profile.label.no-collections",
defaultMessage: "This user has no collections!",
},
profileNoCollectionsAuthLabel: {
id: 'profile.label.no-collections-auth',
id: "profile.label.no-collections-auth",
defaultMessage:
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
},
userNotFoundError: {
id: 'profile.error.not-found',
defaultMessage: 'User not found',
id: "profile.error.not-found",
defaultMessage: "User not found",
},
})
});
let user, projects, organizations, collections
let user, projects, organizations, collections;
try {
;[{ data: user }, { data: projects }, { data: organizations }, { data: collections }] =
[{ data: user }, { data: projects }, { data: organizations }, { data: collections }] =
await Promise.all([
useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
useAsyncData(
@@ -391,33 +391,33 @@ try {
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.categories = project.categories.concat(project.loaders);
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories,
tags.value
)
tags.value,
);
}
return projects
return projects;
},
}
},
),
useAsyncData(`user/${route.params.id}/organizations`, () =>
useBaseFetch(`user/${route.params.id}/organizations`, {
apiVersion: 3,
})
}),
),
useAsyncData(`user/${route.params.id}/collections`, () =>
useBaseFetch(`user/${route.params.id}/collections`, { apiVersion: 3 })
useBaseFetch(`user/${route.params.id}/collections`, { apiVersion: 3 }),
),
])
]);
} catch {
throw createError({
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
});
}
if (!user.value) {
@@ -425,77 +425,77 @@ if (!user.value) {
fatal: true,
statusCode: 404,
message: formatMessage(messages.userNotFoundError),
})
});
}
if (user.value.username !== route.params.id) {
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 });
}
const title = computed(() => `${user.value.username} - Modrinth`)
const title = computed(() => `${user.value.username} - Modrinth`);
const description = computed(() =>
user.value.bio
? formatMessage(messages.profileMetaDescriptionWithBio, {
bio: user.value.bio,
username: user.value.username,
})
: formatMessage(messages.profileMetaDescription, { username: user.value.username })
)
: formatMessage(messages.profileMetaDescription, { username: user.value.username }),
);
useSeoMeta({
title: () => title.value,
description: () => description.value,
ogTitle: () => title.value,
ogDescription: () => description.value,
ogImage: () => user.value.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
})
ogImage: () => user.value.avatar_url ?? "https://cdn.modrinth.com/placeholder.png",
});
const projectTypes = computed(() => {
const obj = {}
const obj = {};
if (collections.value.length > 0) {
obj.collection = true
obj.collection = true;
}
for (const project of projects.value) {
obj[project.project_type] = true
obj[project.project_type] = true;
}
delete obj.project
delete obj.project;
return Object.keys(obj)
})
return Object.keys(obj);
});
const sumDownloads = computed(() => {
let sum = 0
let sum = 0;
for (const project of projects.value) {
sum += project.downloads
sum += project.downloads;
}
return sum
})
return sum;
});
const sumFollows = computed(() => {
let sum = 0
let sum = 0;
for (const project of projects.value) {
sum += project.followers
sum += project.followers;
}
return sum
})
return sum;
});
function cycleSearchDisplayMode() {
cosmetics.value.searchDisplayMode.user = data.$cycleValue(
cosmetics.value.searchDisplayMode.user,
tags.value.projectViewModes
)
saveCosmetics()
tags.value.projectViewModes,
);
saveCosmetics();
}
</script>
<script>
export default defineNuxtComponent({
methods: {},
})
});
</script>
<style lang="scss" scoped>