You've already forked AstralRinth
forked from didirus/AstralRinth
Add TailwindCSS (#1252)
* Setup TailwindCSS * Fully setup configuration * Refactor some tailwind variables
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
</template>
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
middleware: "auth",
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user