Add TailwindCSS (#1252)

* Setup TailwindCSS

* Fully setup configuration

* Refactor some tailwind variables
This commit is contained in:
Evan Song 2024-07-06 20:57:32 -07:00 committed by GitHub
parent 0f2ddb452c
commit abec2e48d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
176 changed files with 7905 additions and 7433 deletions

View File

@ -1,4 +0,0 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.turbo

View File

@ -1,6 +0,0 @@
{
"printWidth": 100,
"semi": false,
"singleQuote": true,
"endOfLine": "auto"
}

View File

@ -1,6 +1,10 @@
{ {
"extends": ["@nuxt/eslint-config"], "extends": ["@nuxt/eslint-config", "plugin:prettier/recommended", "prettier"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"], "env": {
"browser": true,
"node": true
},
"ignorePatterns": [".nuxt/**", ".output/**", "node_modules"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],

View File

@ -0,0 +1,3 @@
**/.nuxt
**/dist
**/node_modules

View File

@ -0,0 +1,4 @@
{
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -12,7 +12,7 @@ This includes, but may not be limited to, the following files:
- assets/images/404.svg - assets/images/404.svg
- assets/images/logo.svg - assets/images/logo.svg
- components/brand/* - components/brand/\*
- static/favicon.ico - static/favicon.ico
- static/favicon-light.ico - static/favicon-light.ico
@ -20,4 +20,4 @@ This includes, but may not be limited to, the following files:
The following files are owned by their respective copyright holders and must be used within each of their Brand Guidelines: The following files are owned by their respective copyright holders and must be used within each of their Brand Guidelines:
- assets/images/external/* - assets/images/external/\*

View File

@ -1,6 +1,6 @@
project_id: 518556 project_id: 518556
preserve_hierarchy: true preserve_hierarchy: true
commit_message: '[ci skip]' commit_message: "[ci skip]"
files: files:
- source: /locales/en-US/* - source: /locales/en-US/*

View File

@ -1,27 +1,28 @@
import { promises as fs } from 'fs' /* eslint-disable no-extra-semi */
import { pathToFileURL } from 'node:url' import { promises as fs } from "fs";
import svgLoader from 'vite-svg-loader' import { pathToFileURL } from "node:url";
import { resolve, basename, relative } from 'pathe' import svgLoader from "vite-svg-loader";
import { defineNuxtConfig } from 'nuxt/config' import { resolve, basename, relative } from "pathe";
import { $fetch } from 'ofetch' import { defineNuxtConfig } from "nuxt/config";
import { globIterate } from 'glob' import { $fetch } from "ofetch";
import { match as matchLocale } from '@formatjs/intl-localematcher' import { globIterate } from "glob";
import { consola } from 'consola' import { match as matchLocale } from "@formatjs/intl-localematcher";
import { consola } from "consola";
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/' const STAGING_API_URL = "https://staging-api.modrinth.com/v2/";
const preloadedFonts = [ const preloadedFonts = [
'inter/Inter-Regular.woff2', "inter/Inter-Regular.woff2",
'inter/Inter-Medium.woff2', "inter/Inter-Medium.woff2",
'inter/Inter-SemiBold.woff2', "inter/Inter-SemiBold.woff2",
'inter/Inter-Bold.woff2', "inter/Inter-Bold.woff2",
] ];
const favicons = { const favicons = {
'(prefers-color-scheme:no-preference)': '/favicon-light.ico', "(prefers-color-scheme:no-preference)": "/favicon-light.ico",
'(prefers-color-scheme:light)': '/favicon-light.ico', "(prefers-color-scheme:light)": "/favicon-light.ico",
'(prefers-color-scheme:dark)': '/favicon.ico', "(prefers-color-scheme:dark)": "/favicon.ico",
} };
/** /**
* Tags of locales that are auto-discovered besides the default locale. * Tags of locales that are auto-discovered besides the default locale.
@ -29,67 +30,67 @@ const favicons = {
* Preferably only the locales that reach a certain threshold of complete * Preferably only the locales that reach a certain threshold of complete
* translations would be included in this array. * translations would be included in this array.
*/ */
const enabledLocales: string[] = [] const enabledLocales: string[] = [];
/** /**
* Overrides for the categories of the certain locales. * Overrides for the categories of the certain locales.
*/ */
const localesCategoriesOverrides: Partial<Record<string, 'fun' | 'experimental'>> = { const localesCategoriesOverrides: Partial<Record<string, "fun" | "experimental">> = {
'en-x-pirate': 'fun', "en-x-pirate": "fun",
'en-x-updown': 'fun', "en-x-updown": "fun",
'en-x-lolcat': 'fun', "en-x-lolcat": "fun",
'en-x-uwu': 'fun', "en-x-uwu": "fun",
'ru-x-bandit': 'fun', "ru-x-bandit": "fun",
ar: 'experimental', ar: "experimental",
he: 'experimental', he: "experimental",
pes: 'experimental', pes: "experimental",
} };
export default defineNuxtConfig({ export default defineNuxtConfig({
srcDir: 'src/', srcDir: "src/",
app: { app: {
head: { head: {
htmlAttrs: { htmlAttrs: {
lang: 'en', lang: "en",
}, },
title: 'Modrinth', title: "Modrinth",
link: [ link: [
// The type is necessary because the linter can't always compare this very nested/complex type on itself // The type is necessary because the linter can't always compare this very nested/complex type on itself
...preloadedFonts.map((font): object => { ...preloadedFonts.map((font): object => {
return { return {
rel: 'preload', rel: "preload",
href: `https://cdn-raw.modrinth.com/fonts/${font}?v=3.19`, href: `https://cdn-raw.modrinth.com/fonts/${font}?v=3.19`,
as: 'font', as: "font",
type: 'font/woff2', type: "font/woff2",
crossorigin: 'anonymous', crossorigin: "anonymous",
} };
}), }),
...Object.entries(favicons).map(([media, href]): object => { ...Object.entries(favicons).map(([media, href]): object => {
return { rel: 'icon', type: 'image/x-icon', href, media } return { rel: "icon", type: "image/x-icon", href, media };
}), }),
...Object.entries(favicons).map(([media, href]): object => { ...Object.entries(favicons).map(([media, href]): object => {
return { rel: 'apple-touch-icon', type: 'image/x-icon', href, media, sizes: '64x64' } return { rel: "apple-touch-icon", type: "image/x-icon", href, media, sizes: "64x64" };
}), }),
{ {
rel: 'search', rel: "search",
type: 'application/opensearchdescription+xml', type: "application/opensearchdescription+xml",
href: '/opensearch.xml', href: "/opensearch.xml",
title: 'Modrinth mods', title: "Modrinth mods",
}, },
], ],
}, },
}, },
vite: { vite: {
cacheDir: '../../node_modules/.vite/apps/knossos', cacheDir: "../../node_modules/.vite/apps/knossos",
resolve: { resolve: {
dedupe: ['vue'], dedupe: ["vue"],
}, },
plugins: [ plugins: [
svgLoader({ svgLoader({
svgoConfig: { svgoConfig: {
plugins: [ plugins: [
{ {
name: 'preset-default', name: "preset-default",
params: { params: {
overrides: { overrides: {
removeViewBox: false, removeViewBox: false,
@ -102,28 +103,28 @@ export default defineNuxtConfig({
], ],
}, },
hooks: { hooks: {
async 'build:before'() { async "build:before"() {
// 30 minutes // 30 minutes
const TTL = 30 * 60 * 1000 const TTL = 30 * 60 * 1000;
let state: { let state: {
lastGenerated?: string lastGenerated?: string;
apiUrl?: string apiUrl?: string;
categories?: any[] categories?: any[];
loaders?: any[] loaders?: any[];
gameVersions?: any[] gameVersions?: any[];
donationPlatforms?: any[] donationPlatforms?: any[];
reportTypes?: any[] reportTypes?: any[];
} = {} } = {};
try { try {
state = JSON.parse(await fs.readFile('./src/generated/state.json', 'utf8')) state = JSON.parse(await fs.readFile("./src/generated/state.json", "utf8"));
} catch { } catch {
// File doesn't exist, create folder // File doesn't exist, create folder
await fs.mkdir('./src/generated', { recursive: true }) await fs.mkdir("./src/generated", { recursive: true });
} }
const API_URL = getApiUrl() const API_URL = getApiUrl();
if ( if (
// Skip regeneration if within TTL... // Skip regeneration if within TTL...
@ -132,18 +133,18 @@ export default defineNuxtConfig({
// ...but only if the API URL is the same // ...but only if the API URL is the same
state.apiUrl === API_URL state.apiUrl === API_URL
) { ) {
return return;
} }
state.lastGenerated = new Date().toISOString() state.lastGenerated = new Date().toISOString();
state.apiUrl = API_URL state.apiUrl = API_URL;
const headers = { const headers = {
headers: { headers: {
'user-agent': 'Knossos generator (support@modrinth.com)', "user-agent": "Knossos generator (support@modrinth.com)",
}, },
} };
const [categories, loaders, gameVersions, donationPlatforms, reportTypes] = await Promise.all( const [categories, loaders, gameVersions, donationPlatforms, reportTypes] = await Promise.all(
[ [
@ -152,137 +153,137 @@ export default defineNuxtConfig({
$fetch(`${API_URL}tag/game_version`, headers), $fetch(`${API_URL}tag/game_version`, headers),
$fetch(`${API_URL}tag/donation_platform`, headers), $fetch(`${API_URL}tag/donation_platform`, headers),
$fetch(`${API_URL}tag/report_type`, headers), $fetch(`${API_URL}tag/report_type`, headers),
] ],
) );
state.categories = categories state.categories = categories;
state.loaders = loaders state.loaders = loaders;
state.gameVersions = gameVersions state.gameVersions = gameVersions;
state.donationPlatforms = donationPlatforms state.donationPlatforms = donationPlatforms;
state.reportTypes = reportTypes state.reportTypes = reportTypes;
await fs.writeFile('./src/generated/state.json', JSON.stringify(state)) await fs.writeFile("./src/generated/state.json", JSON.stringify(state));
console.log('Tags generated!') console.log("Tags generated!");
}, },
'pages:extend'(routes) { "pages:extend"(routes) {
routes.splice( routes.splice(
routes.findIndex((x) => x.name === 'search-searchProjectType'), routes.findIndex((x) => x.name === "search-searchProjectType"),
1 1,
) );
const types = ['mods', 'modpacks', 'plugins', 'resourcepacks', 'shaders', 'datapacks'] const types = ["mods", "modpacks", "plugins", "resourcepacks", "shaders", "datapacks"];
types.forEach((type) => types.forEach((type) =>
routes.push({ routes.push({
name: `search-${type}`, name: `search-${type}`,
path: `/${type}`, path: `/${type}`,
file: resolve(__dirname, 'src/pages/search/[searchProjectType].vue'), file: resolve(__dirname, "src/pages/search/[searchProjectType].vue"),
children: [], children: [],
}) }),
) );
}, },
async 'vintl:extendOptions'(opts) { async "vintl:extendOptions"(opts) {
opts.locales ??= [] opts.locales ??= [];
const isProduction = getDomain() === 'https://modrinth.com' const isProduction = getDomain() === "https://modrinth.com";
const resolveCompactNumberDataImport = await (async () => { const resolveCompactNumberDataImport = await (async () => {
const compactNumberLocales: string[] = [] const compactNumberLocales: string[] = [];
for await (const localeFile of globIterate( for await (const localeFile of globIterate(
'node_modules/@vintl/compact-number/dist/locale-data/*.mjs', "node_modules/@vintl/compact-number/dist/locale-data/*.mjs",
{ ignore: '**/*.data.mjs' } { ignore: "**/*.data.mjs" },
)) { )) {
const tag = basename(localeFile, '.mjs') const tag = basename(localeFile, ".mjs");
compactNumberLocales.push(tag) compactNumberLocales.push(tag);
} }
function resolveImport(tag: string) { function resolveImport(tag: string) {
const matchedTag = matchLocale([tag], compactNumberLocales, 'en-x-placeholder') const matchedTag = matchLocale([tag], compactNumberLocales, "en-x-placeholder");
return matchedTag === 'en-x-placeholder' return matchedTag === "en-x-placeholder"
? undefined ? undefined
: `@vintl/compact-number/locale-data/${matchedTag}` : `@vintl/compact-number/locale-data/${matchedTag}`;
} }
return resolveImport return resolveImport;
})() })();
const resolveOmorphiaLocaleImport = await (async () => { const resolveOmorphiaLocaleImport = await (async () => {
const omorphiaLocales: string[] = [] const omorphiaLocales: string[] = [];
const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>() const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>();
for await (const localeDir of globIterate('node_modules/omorphia/locales/*', { for await (const localeDir of globIterate("node_modules/omorphia/locales/*", {
posix: true, posix: true,
})) { })) {
const tag = basename(localeDir) const tag = basename(localeDir);
omorphiaLocales.push(tag) omorphiaLocales.push(tag);
const localeFiles: { from: string; format?: string }[] = [] const localeFiles: { from: string; format?: string }[] = [];
omorphiaLocaleSets.set(tag, { files: localeFiles }) omorphiaLocaleSets.set(tag, { files: localeFiles });
for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) { for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) {
localeFiles.push({ localeFiles.push({
from: pathToFileURL(localeFile).toString(), from: pathToFileURL(localeFile).toString(),
format: 'default', format: "default",
}) });
} }
} }
return function resolveLocaleImport(tag: string) { return function resolveLocaleImport(tag: string) {
return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, 'en-x-placeholder')) return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, "en-x-placeholder"));
} };
})() })();
for await (const localeDir of globIterate('src/locales/*/', { posix: true })) { for await (const localeDir of globIterate("src/locales/*/", { posix: true })) {
const tag = basename(localeDir) const tag = basename(localeDir);
if (isProduction && !enabledLocales.includes(tag) && opts.defaultLocale !== tag) continue if (isProduction && !enabledLocales.includes(tag) && opts.defaultLocale !== tag) continue;
const locale = const locale =
opts.locales.find((locale) => locale.tag === tag) ?? opts.locales.find((locale) => locale.tag === tag) ??
opts.locales[opts.locales.push({ tag }) - 1]! opts.locales[opts.locales.push({ tag }) - 1]!;
const localeFiles = (locale.files ??= []) const localeFiles = (locale.files ??= []);
for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) { for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) {
const fileName = basename(localeFile) const fileName = basename(localeFile);
if (fileName === 'index.json') { if (fileName === "index.json") {
localeFiles.push({ localeFiles.push({
from: `./${relative('./src', localeFile)}`, from: `./${relative("./src", localeFile)}`,
format: 'crowdin', format: "crowdin",
}) });
} else if (fileName === 'meta.json') { } else if (fileName === "meta.json") {
const meta: Record<string, { message: string }> = await fs const meta: Record<string, { message: string }> = await fs
.readFile(localeFile, 'utf8') .readFile(localeFile, "utf8")
.then((date) => JSON.parse(date)) .then((date) => JSON.parse(date));
const localeMeta = (locale.meta ??= {}) const localeMeta = (locale.meta ??= {});
for (const key in meta) { for (const key in meta) {
const value = meta[key] const value = meta[key];
if (value === undefined) continue if (value === undefined) continue;
localeMeta[key] = value.message localeMeta[key] = value.message;
} }
} else { } else {
;(locale.resources ??= {})[fileName] = `./${relative('./src', localeFile)}` (locale.resources ??= {})[fileName] = `./${relative("./src", localeFile)}`;
} }
} }
const categoryOverride = localesCategoriesOverrides[tag] const categoryOverride = localesCategoriesOverrides[tag];
if (categoryOverride != null) { if (categoryOverride != null) {
;(locale.meta ??= {}).category = categoryOverride (locale.meta ??= {}).category = categoryOverride;
} }
const omorphiaLocaleData = resolveOmorphiaLocaleImport(tag) const omorphiaLocaleData = resolveOmorphiaLocaleImport(tag);
if (omorphiaLocaleData != null) { if (omorphiaLocaleData != null) {
localeFiles.push(...omorphiaLocaleData.files) localeFiles.push(...omorphiaLocaleData.files);
} }
const cnDataImport = resolveCompactNumberDataImport(tag) const cnDataImport = resolveCompactNumberDataImport(tag);
if (cnDataImport != null) { if (cnDataImport != null) {
;(locale.additionalImports ??= []).push({ (locale.additionalImports ??= []).push({
from: cnDataImport, from: cnDataImport,
resolve: false, resolve: false,
}) });
} }
} }
}, },
@ -298,22 +299,22 @@ export default defineNuxtConfig({
production: isProduction(), production: isProduction(),
featureFlagOverrides: getFeatureFlagOverrides(), featureFlagOverrides: getFeatureFlagOverrides(),
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth', owner: process.env.VERCEL_GIT_REPO_OWNER || "modrinth",
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos', slug: process.env.VERCEL_GIT_REPO_SLUG || "knossos",
branch: branch:
process.env.VERCEL_GIT_COMMIT_REF || process.env.VERCEL_GIT_COMMIT_REF ||
process.env.CF_PAGES_BRANCH || process.env.CF_PAGES_BRANCH ||
// @ts-ignore // @ts-ignore
globalThis.CF_PAGES_BRANCH || globalThis.CF_PAGES_BRANCH ||
'master', "master",
hash: hash:
process.env.VERCEL_GIT_COMMIT_SHA || process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.CF_PAGES_COMMIT_SHA || process.env.CF_PAGES_COMMIT_SHA ||
// @ts-ignore // @ts-ignore
globalThis.CF_PAGES_COMMIT_SHA || globalThis.CF_PAGES_COMMIT_SHA ||
'unknown', "unknown",
turnstile: { siteKey: '0x4AAAAAAAW3guHM6Eunbgwu' }, turnstile: { siteKey: "0x4AAAAAAAW3guHM6Eunbgwu" },
}, },
}, },
typescript: { typescript: {
@ -322,99 +323,106 @@ export default defineNuxtConfig({
typeCheck: false, typeCheck: false,
tsConfig: { tsConfig: {
compilerOptions: { compilerOptions: {
moduleResolution: 'bundler', moduleResolution: "bundler",
allowImportingTsExtensions: true, allowImportingTsExtensions: true,
}, },
}, },
}, },
modules: ['@vintl/nuxt', '@nuxtjs/turnstile'], modules: ["@vintl/nuxt", "@nuxtjs/turnstile"],
vintl: { vintl: {
defaultLocale: 'en-US', defaultLocale: "en-US",
locales: [ locales: [
{ {
tag: 'en-US', tag: "en-US",
meta: { meta: {
static: { static: {
iso: 'en', iso: "en",
}, },
}, },
}, },
], ],
storage: 'cookie', storage: "cookie",
parserless: 'only-prod', parserless: "only-prod",
seo: { seo: {
defaultLocaleHasParameter: false, defaultLocaleHasParameter: false,
}, },
onParseError({ error, message, messageId, moduleId, parseMessage, parserOptions }) { onParseError({ error, message, messageId, moduleId, parseMessage, parserOptions }) {
const errorMessage = String(error) const errorMessage = String(error);
const modulePath = relative(__dirname, moduleId) const modulePath = relative(__dirname, moduleId);
try { try {
const fallback = parseMessage(message, { ...parserOptions, ignoreTag: true }) const fallback = parseMessage(message, { ...parserOptions, ignoreTag: true });
consola.warn( consola.warn(
`[i18n] ${messageId} in ${modulePath} cannot be parsed normally due to ${errorMessage}. The tags will will not be parsed.` `[i18n] ${messageId} in ${modulePath} cannot be parsed normally due to ${errorMessage}. The tags will will not be parsed.`,
) );
return fallback return fallback;
} catch (err) { } catch (err) {
const secondaryErrorMessage = String(err) const secondaryErrorMessage = String(err);
const reason = const reason =
errorMessage === secondaryErrorMessage errorMessage === secondaryErrorMessage
? errorMessage ? errorMessage
: `${errorMessage} and ${secondaryErrorMessage}` : `${errorMessage} and ${secondaryErrorMessage}`;
consola.warn( consola.warn(
`[i18n] ${messageId} in ${modulePath} cannot be parsed due to ${reason}. It will be skipped.` `[i18n] ${messageId} in ${modulePath} cannot be parsed due to ${reason}. It will be skipped.`,
) );
} }
}, },
}, },
nitro: { nitro: {
moduleSideEffects: ['@vintl/compact-number/locale-data'], moduleSideEffects: ["@vintl/compact-number/locale-data"],
output: { output: {
dir: '../../dist/apps/knossos/.output', dir: "../../dist/apps/knossos/.output",
}, },
}, },
devtools: { devtools: {
enabled: true, enabled: true,
}, },
compatibilityDate: '2024-07-03', css: ["~/assets/styles/tailwind.css"],
}) postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
compatibilityDate: "2024-07-03",
});
function getApiUrl() { function getApiUrl() {
// @ts-ignore // @ts-ignore
return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL;
} }
function isProduction() { function isProduction() {
return process.env.NODE_ENV === 'production' return process.env.NODE_ENV === "production";
} }
function getFeatureFlagOverrides() { function getFeatureFlagOverrides() {
return JSON.parse(process.env.FLAG_OVERRIDES ?? '{}') return JSON.parse(process.env.FLAG_OVERRIDES ?? "{}");
} }
function getDomain() { function getDomain() {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === "production") {
if (process.env.SITE_URL) { if (process.env.SITE_URL) {
return process.env.SITE_URL return process.env.SITE_URL;
} }
// @ts-ignore // @ts-ignore
else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) { else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
// @ts-ignore // @ts-ignore
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL;
} else if (process.env.HEROKU_APP_NAME) { } else if (process.env.HEROKU_APP_NAME) {
return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com` return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`;
} else if (process.env.VERCEL_URL) { } else if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}` return `https://${process.env.VERCEL_URL}`;
} else if (getApiUrl() === STAGING_API_URL) { } else if (getApiUrl() === STAGING_API_URL) {
return 'https://staging.modrinth.com' return "https://staging.modrinth.com";
} else { } else {
return 'https://modrinth.com' return "https://modrinth.com";
} }
} else { } else {
return 'http://localhost:3000' return "http://localhost:3000";
} }
} }

View File

@ -1,6 +1,7 @@
{ {
"name": "@modrinth/frontend", "name": "@modrinth/frontend",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"build": "nuxi build", "build": "nuxi build",
"dev": "nuxi dev", "dev": "nuxi dev",
@ -9,31 +10,36 @@
"postinstall": "nuxi prepare", "postinstall": "nuxi prepare",
"lint:js": "eslint ./src --ext .js,.vue,.ts", "lint:js": "eslint ./src --ext .js,.vue,.ts",
"lint": "npm run lint:js && prettier --check .", "lint": "npm run lint:js && prettier --check .",
"fix": "eslint . --fix --ext .js,.vue,.ts && prettier --write .", "fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file locales/en-US/index.json --format crowdin --preserve-whitespace" "intl:extract": "formatjs extract \"{,components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file locales/en-US/index.json --format crowdin --preserve-whitespace"
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^6.1.2", "@formatjs/cli": "^6.1.2",
"@nuxt/devtools": "^1.3.3", "@nuxt/devtools": "^1.3.3",
"@nuxt/eslint-config": "^0.3.13", "@nuxt/eslint-config": "^0.3.13",
"@nuxtjs/eslint-config-typescript": "^12.0.0", "@nuxtjs/eslint-config-typescript": "^12.1.0",
"@nuxtjs/turnstile": "^0.8.0", "@nuxtjs/turnstile": "^0.8.0",
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^5.59.8", "@typescript-eslint/parser": "^7.15.0",
"@vintl/compact-number": "^2.0.5", "@vintl/compact-number": "^2.0.5",
"@vintl/how-ago": "^3.0.1", "@vintl/how-ago": "^3.0.1",
"@vintl/nuxt": "^1.8.0", "@vintl/nuxt": "^1.8.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-custom": "workspace:*",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.27.0",
"glob": "^10.2.7", "glob": "^10.2.7",
"nuxt": "^3.12.3", "nuxt": "^3.12.3",
"prettier": "^2.8.8", "postcss": "^8.4.39",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"sass": "^1.58.0", "sass": "^1.58.0",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",

View File

@ -6,6 +6,6 @@
</NuxtLayout> </NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts' import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
import Notifications from '~/components/ui/Notifications.vue' import Notifications from "~/components/ui/Notifications.vue";
</script> </script>

View File

@ -456,7 +456,7 @@
} }
&:disabled, &:disabled,
&[disabled='true'] { &[disabled="true"] {
cursor: not-allowed; cursor: not-allowed;
filter: grayscale(50%); filter: grayscale(50%);
opacity: 0.5; opacity: 0.5;
@ -502,7 +502,7 @@ tr.button-transparent {
} }
&:disabled > *, &:disabled > *,
&[disabled='true'] > * { &[disabled="true"] > * {
cursor: not-allowed; cursor: not-allowed;
filter: grayscale(50%); filter: grayscale(50%);
opacity: 0.5; opacity: 0.5;
@ -511,7 +511,10 @@ tr.button-transparent {
} }
.button-within { .button-within {
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline 0.2s ease-in-out; outline 0.2s ease-in-out;
&:focus-visible:not(&.disabled), &:focus-visible:not(&.disabled),
@ -530,7 +533,7 @@ tr.button-transparent {
box-shadow: none; box-shadow: none;
&disabled, &disabled,
&[disabled='true'] { &[disabled="true"] {
cursor: not-allowed; cursor: not-allowed;
box-shadow: none; box-shadow: none;
} }
@ -544,7 +547,9 @@ tr.button-transparent {
color: var(--text-color); color: var(--text-color);
background-color: var(--background-color); background-color: var(--background-color);
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
border-radius: var(--size-rounded-sm); border-radius: var(--size-rounded-sm);
} }
@ -560,7 +565,10 @@ tr.button-transparent {
cursor: pointer; cursor: pointer;
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, scale 0.05s ease-in-out, transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
scale 0.05s ease-in-out,
outline 0.2s ease-in-out; outline 0.2s ease-in-out;
text-decoration: none; text-decoration: none;
@ -603,7 +611,9 @@ tr.button-transparent {
border-radius: var(--size-rounded-sm); border-radius: var(--size-rounded-sm);
color: var(--text-color); color: var(--text-color);
background-color: var(--background-color); background-color: var(--background-color);
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
svg { svg {
min-width: 1.25rem; min-width: 1.25rem;
@ -826,7 +836,7 @@ tr.button-transparent {
background: var(--color-button-bg); background: var(--color-button-bg);
&:after { &:after {
content: ''; content: "";
position: absolute; position: absolute;
top: 7px; top: 7px;
left: 7px; left: 7px;
@ -1065,7 +1075,9 @@ button {
background: var(--color-button-bg); background: var(--color-button-bg);
width: fit-content; width: fit-content;
border-radius: var(--size-rounded-sm); border-radius: var(--size-rounded-sm);
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
transition: box-shadow 0.1s ease-in-out; transition: box-shadow 0.1s ease-in-out;
overflow: hidden; overflow: hidden;
max-width: 100%; max-width: 100%;
@ -1099,7 +1111,9 @@ button {
&:focus, &:focus,
&:focus-visible, &:focus-visible,
&:focus-within { &:focus-within {
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); color: var(--color-button-text-active);
} }
} }
@ -1436,15 +1450,15 @@ svg.inline-svg {
height: var(--_size, var(--icon-16)) !important; height: var(--_size, var(--icon-16)) !important;
border: 1px solid var(--color-button-border); border: 1px solid var(--color-button-border);
&[data-size='32'] { &[data-size="32"] {
--_size: var(--icon-32); --_size: var(--icon-32);
} }
&[data-shape='circle'] { &[data-shape="circle"] {
border-radius: var(--radius-max) !important; border-radius: var(--radius-max) !important;
} }
&[data-shape='square'] { &[data-shape="square"] {
border-radius: calc(2.25 * (var(--_size) / 16)) !important; border-radius: calc(2.25 * (var(--_size) / 16)) !important;
} }
} }

View File

@ -214,8 +214,8 @@ html {
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px; --shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp'); --landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing-light.webp");
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp'); --landing-maze-gradient-bg: url("https://cdn.modrinth.com/landing-new/landing-lower-light.webp");
--landing-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%); --landing-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
--landing-color-heading: #000; --landing-color-heading: #000;
@ -341,9 +341,9 @@ html {
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px; --shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing.webp'); --landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing.webp");
--landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%), --landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
url('https://cdn.modrinth.com/landing-new/landing-lower.webp'); url("https://cdn.modrinth.com/landing-new/landing-lower.webp");
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%); --landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
--landing-color-heading: #fff; --landing-color-heading: #fff;
@ -537,18 +537,22 @@ textarea {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
transition: box-shadow 0.1s ease-in-out; transition: box-shadow 0.1s ease-in-out;
min-height: 40px; min-height: 40px;
&:focus, &:focus,
&:focus-visible { &:focus-visible {
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); color: var(--color-button-text-active);
} }
&:disabled, &:disabled,
&[disabled='true'] { &[disabled="true"] {
opacity: 0.6; opacity: 0.6;
pointer-events: none; pointer-events: none;
cursor: not-allowed; cursor: not-allowed;
@ -565,7 +569,7 @@ textarea {
} }
button, button,
input[type='button'] { input[type="button"] {
cursor: pointer; cursor: pointer;
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
@ -581,13 +585,13 @@ kbd {
font-size: 0.85em !important; font-size: 0.85em !important;
} }
@import '~/assets/styles/layout.scss'; @import "~/assets/styles/layout.scss";
@import '~/assets/styles/utils.scss'; @import "~/assets/styles/utils.scss";
@import '~/assets/styles/components.scss'; @import "~/assets/styles/components.scss";
button:focus-visible, button:focus-visible,
a:focus-visible, a:focus-visible,
[tabindex='0']:focus-visible { [tabindex="0"]:focus-visible {
outline: 0.25rem solid #ea80ff; outline: 0.25rem solid #ea80ff;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@ -602,7 +606,10 @@ input {
} }
.button-animation { .button-animation {
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline-width 0.2s ease-in-out; outline-width 0.2s ease-in-out;
} }

View File

@ -42,9 +42,9 @@
padding: 0 0.75rem; padding: 0 0.75rem;
grid-template: grid-template:
'sidebar' "sidebar"
'content' "content"
'info' "info"
/ 100%; / 100%;
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
@ -81,25 +81,25 @@
column-gap: 0.75rem; column-gap: 0.75rem;
grid-template: grid-template:
'sidebar content' auto "sidebar content" auto
'info content' auto "info content" auto
'dummy content' 1fr "dummy content" 1fr
/ 20rem 1fr; / 20rem 1fr;
&.alt-layout { &.alt-layout {
grid-template: grid-template:
'content sidebar' auto "content sidebar" auto
'content info' auto "content info" auto
'content dummy' 1fr "content dummy" 1fr
/ 1fr 20rem; / 1fr 20rem;
} }
&.no-sidebar { &.no-sidebar {
grid-template: grid-template:
'header header' auto "header header" auto
'content content' auto "content content" auto
'info info' auto "info info" auto
'dummy dummy' 1fr "dummy dummy" 1fr
/ 1fr 1fr; / 1fr 1fr;
.normal-page__content { .normal-page__content {

View File

@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
h1,
h2,
h3 {
@apply font-bold;
}

View File

@ -43,32 +43,32 @@
/> />
<path <path
d="M51.28 316.13c10.59 125 115.54 223.3 243.27 223.3 96.51 0 180.02-56.12 219.63-137.46l48.61 16.83c-46.78 101.34-149.35 171.75-268.24 171.75C138.6 590.55 10.71 469.38 0 316.13h51.28ZM.78 265.24C15.86 116.36 141.73 0 294.56 0c162.97 0 295.28 132.31 295.28 295.28 0 26.14-3.4 51.49-9.8 75.63l-48.48-16.78a244.28 244.28 0 0 0 7.15-58.85c0-134.75-109.4-244.15-244.15-244.15-124.58 0-227.49 93.5-242.32 214.11H.8Z" d="M51.28 316.13c10.59 125 115.54 223.3 243.27 223.3 96.51 0 180.02-56.12 219.63-137.46l48.61 16.83c-46.78 101.34-149.35 171.75-268.24 171.75C138.6 590.55 10.71 469.38 0 316.13h51.28ZM.78 265.24C15.86 116.36 141.73 0 294.56 0c162.97 0 295.28 132.31 295.28 295.28 0 26.14-3.4 51.49-9.8 75.63l-48.48-16.78a244.28 244.28 0 0 0 7.15-58.85c0-134.75-109.4-244.15-244.15-244.15-124.58 0-227.49 93.5-242.32 214.11H.8Z"
class="ring ring--large" class="ring--large ring"
/> />
<path <path
d="M293.77 153.17c-78.49.07-142.2 63.83-142.2 142.34 0 78.56 63.79 142.34 142.35 142.34 3.98 0 7.93-.16 11.83-.49l14.22 49.76a194.65 194.65 0 0 1-26.05 1.74c-106.72 0-193.36-86.64-193.36-193.35 0-106.72 86.64-193.35 193.36-193.35 2.64 0 5.28.05 7.9.16l-8.05 50.85Zm58.2-42.13c78.39 24.67 135.3 97.98 135.3 184.47 0 80.07-48.77 148.83-118.2 178.18l-14.17-49.55c48.08-22.85 81.36-71.89 81.36-128.63 0-60.99-38.44-113.07-92.39-133.32l8.1-51.15Z" d="M293.77 153.17c-78.49.07-142.2 63.83-142.2 142.34 0 78.56 63.79 142.34 142.35 142.34 3.98 0 7.93-.16 11.83-.49l14.22 49.76a194.65 194.65 0 0 1-26.05 1.74c-106.72 0-193.36-86.64-193.36-193.35 0-106.72 86.64-193.35 193.36-193.35 2.64 0 5.28.05 7.9.16l-8.05 50.85Zm58.2-42.13c78.39 24.67 135.3 97.98 135.3 184.47 0 80.07-48.77 148.83-118.2 178.18l-14.17-49.55c48.08-22.85 81.36-71.89 81.36-128.63 0-60.99-38.44-113.07-92.39-133.32l8.1-51.15Z"
class="ring ring--small" class="ring--small ring"
/> />
</g> </g>
</svg> </svg>
</template> </template>
<script setup> <script setup>
const loading = useLoading() const loading = useLoading();
const config = useRuntimeConfig() const config = useRuntimeConfig();
const api = computed(() => { const api = computed(() => {
const apiUrl = config.public.apiBaseUrl const apiUrl = config.public.apiBaseUrl;
if (apiUrl.startsWith('https://api.modrinth.com')) { if (apiUrl.startsWith("https://api.modrinth.com")) {
return 'prod' return "prod";
} else if (apiUrl.startsWith('https://staging-api.modrinth.com')) { } else if (apiUrl.startsWith("https://staging-api.modrinth.com")) {
return 'staging' return "staging";
} else if (apiUrl.startsWith('localhost') || apiUrl.startsWith('127.0.0.1')) { } else if (apiUrl.startsWith("localhost") || apiUrl.startsWith("127.0.0.1")) {
return 'localhost' return "localhost";
} }
return 'foreign' return "foreign";
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -35,8 +35,8 @@
</template> </template>
<script setup> <script setup>
const pixelated = ref(false) const pixelated = ref(false);
const img = ref(null) const img = ref(null);
defineProps({ defineProps({
src: { src: {
@ -45,13 +45,13 @@ defineProps({
}, },
alt: { alt: {
type: String, type: String,
default: '', default: "",
}, },
size: { size: {
type: String, type: String,
default: 'sm', default: "sm",
validator(value) { validator(value) {
return ['xxs', 'xs', 'sm', 'md', 'lg'].includes(value) return ["xxs", "xs", "sm", "md", "lg"].includes(value);
}, },
}, },
circle: { circle: {
@ -64,19 +64,19 @@ defineProps({
}, },
loading: { loading: {
type: String, type: String,
default: 'eager', default: "eager",
}, },
raised: { raised: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) });
function updatePixelated() { function updatePixelated() {
if (img.value && img.value.naturalWidth && img.value.naturalWidth <= 96) { if (img.value && img.value.naturalWidth && img.value.naturalWidth <= 96) {
pixelated.value = true pixelated.value = true;
} else { } else {
pixelated.value = false pixelated.value = false;
} }
} }
</script> </script>
@ -85,8 +85,10 @@ function updatePixelated() {
.avatar { .avatar {
border-radius: var(--size-rounded-icon); border-radius: var(--size-rounded-icon);
box-shadow: var(--shadow-inset-lg), var(--shadow-card); box-shadow: var(--shadow-inset-lg), var(--shadow-card);
height: var(--size); min-height: var(--size);
width: var(--size); min-width: var(--size);
max-height: var(--size);
max-width: var(--size);
background-color: var(--color-button-bg); background-color: var(--color-button-bg);
object-fit: contain; object-fit: contain;

View File

@ -35,19 +35,19 @@
</template> </template>
<script setup> <script setup>
import ModrinthIcon from '~/assets/images/logo.svg?component' import ModrinthIcon from "~/assets/images/logo.svg?component";
import ModeratorIcon from '~/assets/images/sidebar/admin.svg?component' import ModeratorIcon from "~/assets/images/sidebar/admin.svg?component";
import CreatorIcon from '~/assets/images/utils/box.svg?component' import CreatorIcon from "~/assets/images/utils/box.svg?component";
import ListIcon from '~/assets/images/utils/list.svg?component' import ListIcon from "~/assets/images/utils/list.svg?component";
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?component' import EyeOffIcon from "~/assets/images/utils/eye-off.svg?component";
import DraftIcon from '~/assets/images/utils/file-text.svg?component' import DraftIcon from "~/assets/images/utils/file-text.svg?component";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import ArchiveIcon from '~/assets/images/utils/archive.svg?component' import ArchiveIcon from "~/assets/images/utils/archive.svg?component";
import ProcessingIcon from '~/assets/images/utils/updated.svg?component' import ProcessingIcon from "~/assets/images/utils/updated.svg?component";
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import LockIcon from '~/assets/images/utils/lock.svg?component' import LockIcon from "~/assets/images/utils/lock.svg?component";
import CalendarIcon from '~/assets/images/utils/calendar.svg?component' import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import CloseIcon from '~/assets/images/utils/check-circle.svg?component' import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
defineProps({ defineProps({
type: { type: {
@ -56,9 +56,9 @@ defineProps({
}, },
color: { color: {
type: String, type: String,
default: '', default: "",
}, },
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -15,7 +15,7 @@
</template> </template>
<script setup> <script setup>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component' import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
defineProps({ defineProps({
linkStack: { linkStack: {
@ -26,7 +26,7 @@ defineProps({
type: String, type: String,
required: true, required: true,
}, },
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -25,8 +25,8 @@
</template> </template>
<script> <script>
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component' import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
export default { export default {
components: { components: {
@ -36,7 +36,7 @@ export default {
props: { props: {
label: { label: {
type: String, type: String,
default: '', default: "",
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
@ -56,15 +56,15 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['update:modelValue'], emits: ["update:modelValue"],
methods: { methods: {
toggle() { toggle() {
if (!this.disabled) { if (!this.disabled) {
this.$emit('update:modelValue', !this.modelValue) this.$emit("update:modelValue", !this.modelValue);
} }
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -105,7 +105,9 @@ export default {
color: var(--color-button-text); color: var(--color-button-text);
background-color: var(--color-button-bg); background-color: var(--color-button-bg);
border-radius: var(--size-rounded-control); border-radius: var(--size-rounded-control);
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
&.checked { &.checked {
background-color: var(--color-brand); background-color: var(--color-brand);

View File

@ -14,7 +14,7 @@
</template> </template>
<script> <script>
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
export default { export default {
components: { components: {
@ -42,32 +42,32 @@ export default {
default: true, default: true,
}, },
}, },
emits: ['update:modelValue'], emits: ["update:modelValue"],
computed: { computed: {
selected: { selected: {
get() { get() {
return this.modelValue return this.modelValue;
}, },
set(value) { set(value) {
this.$emit('update:modelValue', value) this.$emit("update:modelValue", value);
}, },
}, },
}, },
created() { created() {
if (this.items.length > 0 && this.neverEmpty) { if (this.items.length > 0 && this.neverEmpty) {
this.selected = this.items[0] this.selected = this.items[0];
} }
}, },
methods: { methods: {
toggleItem(item) { toggleItem(item) {
if (this.selected === item && !this.neverEmpty) { if (this.selected === item && !this.neverEmpty) {
this.selected = null this.selected = null;
} else { } else {
this.selected = item this.selected = item;
} }
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -95,7 +95,9 @@ export default {
.selected { .selected {
color: var(--color-button-text-active); color: var(--color-button-text-active);
background-color: var(--color-brand-highlight); 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);
} }
} }
</style> </style>

View File

@ -4,8 +4,8 @@
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
Your new collection will be created as a public collection with Your new collection will be created as a public collection with
{{ projectIds.length > 0 ? projectIds.length : 'no' }} {{ projectIds.length > 0 ? projectIds.length : "no" }}
{{ projectIds.length !== 1 ? 'projects' : 'project' }}. {{ projectIds.length !== 1 ? "projects" : "project" }}.
</p> </p>
</div> </div>
<label for="name"> <label for="name">
@ -40,61 +40,61 @@
</Modal> </Modal>
</template> </template>
<script setup> <script setup>
import { XIcon as CrossIcon, CheckIcon } from '@modrinth/assets' import { XIcon as CrossIcon, CheckIcon } from "@modrinth/assets";
import { Modal, Button } from '@modrinth/ui' import { Modal, Button } from "@modrinth/ui";
const router = useNativeRouter() const router = useNativeRouter();
const name = ref('') const name = ref("");
const description = ref('') const description = ref("");
const modal = ref() const modal = ref();
const props = defineProps({ const props = defineProps({
projectIds: { projectIds: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
}) });
async function create() { async function create() {
startLoading() startLoading();
try { try {
const result = await useBaseFetch('collection', { const result = await useBaseFetch("collection", {
method: 'POST', method: "POST",
body: { body: {
name: name.value.trim(), name: name.value.trim(),
description: description.value.trim(), description: description.value.trim(),
projects: props.projectIds, projects: props.projectIds,
}, },
apiVersion: 3, apiVersion: 3,
}) });
await initUserCollections() await initUserCollections();
modal.value.hide() modal.value.hide();
await router.push(`/collection/${result.id}`) await router.push(`/collection/${result.id}`);
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err?.data?.description || err?.message || err, text: err?.data?.description || err?.message || err,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
} }
function show() { function show() {
name.value = '' name.value = "";
description.value = '' description.value = "";
modal.value.show() modal.value.show();
} }
defineExpose({ defineExpose({
show, show,
}) });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -17,5 +17,5 @@ defineProps({
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}) });
</script> </script>

View File

@ -7,8 +7,8 @@
</template> </template>
<script> <script>
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?component' import ClipboardCopyIcon from "~/assets/images/utils/clipboard-copy.svg?component";
export default { export default {
components: { components: {
@ -24,15 +24,15 @@ export default {
data() { data() {
return { return {
copied: false, copied: false,
} };
}, },
methods: { methods: {
async copyText() { async copyText() {
await navigator.clipboard.writeText(this.text) await navigator.clipboard.writeText(this.text);
this.copied = true this.copied = true;
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -48,7 +48,10 @@ export default {
width: min-content; width: min-content;
border-radius: 10px; border-radius: 10px;
user-select: text; user-select: text;
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline 0.2s ease-in-out; outline 0.2s ease-in-out;
span { span {

View File

@ -4,10 +4,10 @@
class="drop-area" class="drop-area"
@drop.stop.prevent=" @drop.stop.prevent="
(event) => { (event) => {
$refs.drop_area.style.visibility = 'hidden' $refs.drop_area.style.visibility = 'hidden';
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) { if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
$emit('change', event.dataTransfer.files) $emit('change', event.dataTransfer.files);
} }
} }
" "
@ -22,45 +22,45 @@ export default {
props: { props: {
accept: { accept: {
type: String, type: String,
default: '', default: "",
}, },
}, },
emits: ['change'], emits: ["change"],
data() { data() {
return { return {
fileAllowed: false, fileAllowed: false,
} };
}, },
mounted() { mounted() {
document.addEventListener('dragenter', this.allowDrag) document.addEventListener("dragenter", this.allowDrag);
}, },
methods: { methods: {
allowDrag(event) { allowDrag(event) {
const file = event.dataTransfer?.items[0] const file = event.dataTransfer?.items[0];
if ( if (
file && file &&
this.accept this.accept
.split(',') .split(",")
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false) .reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === "*", false)
) { ) {
this.fileAllowed = true this.fileAllowed = true;
event.dataTransfer.dropEffect = 'copy' event.dataTransfer.dropEffect = "copy";
event.preventDefault() event.preventDefault();
if (this.$refs.drop_area) { if (this.$refs.drop_area) {
this.$refs.drop_area.style.visibility = 'visible' this.$refs.drop_area.style.visibility = "visible";
} }
} else { } else {
this.fileAllowed = false this.fileAllowed = false;
if (this.$refs.drop_area) { if (this.$refs.drop_area) {
this.$refs.drop_area.style.visibility = 'hidden' this.$refs.drop_area.style.visibility = "hidden";
} }
} }
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -73,13 +73,15 @@ export default {
z-index: 10; z-index: 10;
visibility: hidden; visibility: hidden;
background-color: hsla(0, 0%, 0%, 0.5); background-color: hsla(0, 0%, 0%, 0.5);
transition: visibility 0.2s ease-in-out, background-color 0.1s ease-in-out; transition:
visibility 0.2s ease-in-out,
background-color 0.1s ease-in-out;
display: flex; display: flex;
&::before { &::before {
--indent: 4rem; --indent: 4rem;
content: ' '; content: " ";
position: relative; position: relative;
top: var(--indent); top: var(--indent);
left: var(--indent); left: var(--indent);

View File

@ -48,25 +48,25 @@
</span> </span>
</template> </template>
<script setup> <script setup>
import InfoIcon from '~/assets/images/utils/info.svg?component' import InfoIcon from "~/assets/images/utils/info.svg?component";
import ClientIcon from '~/assets/images/utils/client.svg?component' import ClientIcon from "~/assets/images/utils/client.svg?component";
import GlobeIcon from '~/assets/images/utils/globe.svg?component' import GlobeIcon from "~/assets/images/utils/globe.svg?component";
import ServerIcon from '~/assets/images/utils/server.svg?component' import ServerIcon from "~/assets/images/utils/server.svg?component";
defineProps({ defineProps({
type: { type: {
type: String, type: String,
default: 'mod', default: "mod",
}, },
serverSide: { serverSide: {
type: String, type: String,
required: false, required: false,
default: '', default: "",
}, },
clientSide: { clientSide: {
type: String, type: String,
required: false, required: false,
default: '', default: "",
}, },
typeOnly: { typeOnly: {
type: Boolean, type: Boolean,
@ -87,12 +87,12 @@ defineProps({
type: Array, type: Array,
required: false, required: false,
default() { default() {
return [] return [];
}, },
}, },
}) });
const tags = useTags() const tags = useTags();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.environment { .environment {

View File

@ -18,14 +18,14 @@
</template> </template>
<script> <script>
import { fileIsValid } from '~/helpers/fileUtils.js' import { fileIsValid } from "~/helpers/fileUtils.js";
export default { export default {
components: {}, components: {},
props: { props: {
prompt: { prompt: {
type: String, type: String,
default: 'Select file', default: "Select file",
}, },
multiple: { multiple: {
type: Boolean, type: Boolean,
@ -59,33 +59,33 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['change'], emits: ["change"],
data() { data() {
return { return {
files: [], files: [],
} };
}, },
methods: { methods: {
addFiles(files, shouldNotReset) { addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) { if (!shouldNotReset || this.shouldAlwaysReset) {
this.files = files this.files = files;
} }
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true } const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true };
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions)) this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions));
if (this.files.length > 0) { if (this.files.length > 0) {
this.$emit('change', this.files) this.$emit("change", this.files);
} }
}, },
handleDrop(e) { handleDrop(e) {
this.addFiles(e.dataTransfer.files) this.addFiles(e.dataTransfer.files);
}, },
handleChange(e) { handleChange(e) {
this.addFiles(e.target.files) this.addFiles(e.target.files);
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,12 +7,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
type MessageType = 'information' | 'warning' type MessageType = "information" | "warning";
const props = withDefaults(defineProps<{ messageType?: MessageType }>(), { const props = withDefaults(defineProps<{ messageType?: MessageType }>(), {
messageType: 'information', messageType: "information",
}) });
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`) const cardClassByType = computed(() => `message-banner__content_${props.messageType}`);
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`) const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`);
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@ -26,7 +26,7 @@
</template> </template>
<script> <script>
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
export default { export default {
components: { components: {
@ -39,31 +39,31 @@ export default {
}, },
}, },
setup() { setup() {
const cosmetics = useCosmetics() const cosmetics = useCosmetics();
return { cosmetics } return { cosmetics };
}, },
data() { data() {
return { return {
shown: false, shown: false,
actuallyShown: false, actuallyShown: false,
} };
}, },
methods: { methods: {
show() { show() {
this.shown = true this.shown = true;
setTimeout(() => { setTimeout(() => {
this.actuallyShown = true this.actuallyShown = true;
}, 50) }, 50);
}, },
hide() { hide() {
this.actuallyShown = false this.actuallyShown = false;
setTimeout(() => { setTimeout(() => {
this.shown = false this.shown = false;
}, 300) }, 300);
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -34,10 +34,10 @@
</template> </template>
<script> <script>
import { renderString } from '@modrinth/utils' import { renderString } from "@modrinth/utils";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import TrashIcon from '~/assets/images/utils/trash.svg?component' import TrashIcon from "~/assets/images/utils/trash.svg?component";
import Modal from '~/components/ui/Modal.vue' import Modal from "~/components/ui/Modal.vue";
export default { export default {
components: { components: {
@ -48,7 +48,7 @@ export default {
props: { props: {
confirmationText: { confirmationText: {
type: String, type: String,
default: '', default: "",
}, },
hasToType: { hasToType: {
type: Boolean, type: Boolean,
@ -56,46 +56,46 @@ export default {
}, },
title: { title: {
type: String, type: String,
default: 'No title defined', default: "No title defined",
required: true, required: true,
}, },
description: { description: {
type: String, type: String,
default: 'No description defined', default: "No description defined",
required: true, required: true,
}, },
proceedLabel: { proceedLabel: {
type: String, type: String,
default: 'Proceed', default: "Proceed",
}, },
}, },
emits: ['proceed'], emits: ["proceed"],
data() { data() {
return { return {
action_disabled: this.hasToType, action_disabled: this.hasToType,
confirmation_typed: '', confirmation_typed: "",
} };
}, },
methods: { methods: {
renderString, renderString,
cancel() { cancel() {
this.$refs.modal.hide() this.$refs.modal.hide();
}, },
proceed() { proceed() {
this.$refs.modal.hide() this.$refs.modal.hide();
this.$emit('proceed') this.$emit("proceed");
}, },
type() { type() {
if (this.hasToType) { if (this.hasToType) {
this.action_disabled = this.action_disabled =
this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase() this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase();
} }
}, },
show() { show() {
this.$refs.modal.show() this.$refs.modal.show();
}, },
}, },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -73,10 +73,10 @@
</template> </template>
<script> <script>
import { Multiselect } from 'vue-multiselect' import { Multiselect } from "vue-multiselect";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import CheckIcon from '~/assets/images/utils/right-arrow.svg?component' import CheckIcon from "~/assets/images/utils/right-arrow.svg?component";
import Modal from '~/components/ui/Modal.vue' import Modal from "~/components/ui/Modal.vue";
export default { export default {
components: { components: {
@ -93,122 +93,122 @@ export default {
}, },
}, },
setup() { setup() {
const tags = useTags() const tags = useTags();
return { tags } return { tags };
}, },
data() { data() {
return { return {
name: '', name: "",
slug: '', slug: "",
description: '', description: "",
manualSlug: false, manualSlug: false,
visibilities: [ visibilities: [
{ {
actual: 'approved', actual: "approved",
display: 'Public', display: "Public",
}, },
{ {
actual: 'private', actual: "private",
display: 'Private', display: "Private",
}, },
{ {
actual: 'unlisted', actual: "unlisted",
display: 'Unlisted', display: "Unlisted",
}, },
], ],
visibility: { visibility: {
actual: 'approved', actual: "approved",
display: 'Public', display: "Public",
}, },
} };
}, },
methods: { methods: {
cancel() { cancel() {
this.$refs.modal.hide() this.$refs.modal.hide();
}, },
async createProject() { async createProject() {
startLoading() startLoading();
const formData = new FormData() const formData = new FormData();
const auth = await useAuth() const auth = await useAuth();
const projectData = { const projectData = {
title: this.name.trim(), title: this.name.trim(),
project_type: 'mod', project_type: "mod",
slug: this.slug, slug: this.slug,
description: this.description.trim(), description: this.description.trim(),
body: '', body: "",
requested_status: this.visibility.actual, requested_status: this.visibility.actual,
initial_versions: [], initial_versions: [],
team_members: [ team_members: [
{ {
user_id: auth.value.user.id, user_id: auth.value.user.id,
name: auth.value.user.username, name: auth.value.user.username,
role: 'Owner', role: "Owner",
}, },
], ],
categories: [], categories: [],
client_side: 'required', client_side: "required",
server_side: 'required', server_side: "required",
license_id: 'LicenseRef-Unknown', license_id: "LicenseRef-Unknown",
is_draft: true, is_draft: true,
} };
if (this.organizationId) { if (this.organizationId) {
projectData.organization_id = this.organizationId projectData.organization_id = this.organizationId;
} }
formData.append('data', JSON.stringify(projectData)) formData.append("data", JSON.stringify(projectData));
try { try {
await useBaseFetch('project', { await useBaseFetch("project", {
method: 'POST', method: "POST",
body: formData, body: formData,
headers: { headers: {
'Content-Disposition': formData, "Content-Disposition": formData,
}, },
}) });
this.$refs.modal.hide() this.$refs.modal.hide();
await this.$router.push({ await this.$router.push({
name: 'type-id', name: "type-id",
params: { params: {
type: 'project', type: "project",
id: this.slug, id: this.slug,
}, },
}) });
} catch (err) { } catch (err) {
this.$notify({ this.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data.description, text: err.data.description,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
}, },
show() { show() {
this.projectType = this.tags.projectTypes[0].display this.projectType = this.tags.projectTypes[0].display;
this.name = '' this.name = "";
this.slug = '' this.slug = "";
this.description = '' this.description = "";
this.manualSlug = false this.manualSlug = false;
this.$refs.modal.show() this.$refs.modal.show();
}, },
updatedName() { updatedName() {
if (!this.manualSlug) { if (!this.manualSlug) {
this.slug = this.name this.slug = this.name
.trim() .trim()
.toLowerCase() .toLowerCase()
.replaceAll(' ', '-') .replaceAll(" ", "-")
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '') .replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
.replaceAll(/--+/gm, '-') .replaceAll(/--+/gm, "-");
} }
}, },
}, },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -253,7 +253,7 @@
> >
<div <div
v-for="(option, index) in selectedOptions[steps[currentStepIndex].id].filter( v-for="(option, index) in selectedOptions[steps[currentStepIndex].id].filter(
(x) => x.fillers && x.fillers.length > 0 (x) => x.fillers && x.fillers.length > 0,
)" )"
:key="index" :key="index"
> >
@ -339,9 +339,9 @@ import {
XIcon as CrossIcon, XIcon as CrossIcon,
EyeOffIcon, EyeOffIcon,
ExitIcon, ExitIcon,
} from '@modrinth/assets' } from "@modrinth/assets";
import { MarkdownEditor, OverflowMenu } from '@modrinth/ui' import { MarkdownEditor, OverflowMenu } from "@modrinth/ui";
import Categories from '~/components/ui/search/Categories.vue' import Categories from "~/components/ui/search/Categories.vue";
const props = defineProps({ const props = defineProps({
project: { project: {
@ -357,72 +357,72 @@ const props = defineProps({
required: true, required: true,
default: () => {}, default: () => {},
}, },
}) });
const steps = computed(() => const steps = computed(() =>
[ [
{ {
id: 'title', id: "title",
question: 'Is this title free of useless information?', question: "Is this title free of useless information?",
shown: true, shown: true,
rules: [ rules: [
'No unnecessary data (mod loaders, game versions, etc)', "No unnecessary data (mod loaders, game versions, etc)",
'No emojis / useless text decorators', "No emojis / useless text decorators",
], ],
examples: [ examples: [
'✅ NoobMod [1.8+] • Kill all noobs in your world!', "✅ NoobMod [1.8+] • Kill all noobs in your world!",
'[FABRIC] My Optimization Pack', "[FABRIC] My Optimization Pack",
'[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)', "[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)",
], ],
exceptions: [ exceptions: [
'Loaders and/or game versions allowed if this project is a port of another mod. (ex: Gravestones for 1.20)', "Loaders and/or game versions allowed if this project is a port of another mod. (ex: Gravestones for 1.20)",
'Loaders allowed if they choose to separate their project into Forge and Fabric variants (discouraged)', "Loaders allowed if they choose to separate their project into Forge and Fabric variants (discouraged)",
], ],
options: [ options: [
{ {
name: 'Contains useless info', name: "Contains useless info",
resultingMessage: `## Misuse of Title resultingMessage: `## Misuse of Title
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) we ask that you limit the title to just the name of your project. Additional information, such as themes, tags, supported versions or loaders, etc. should be saved for the Summary or Description. When changing your project title, remember to also ensure that your project slug (URL) matches and accurately represents your project.`, Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) we ask that you limit the title to just the name of your project. Additional information, such as themes, tags, supported versions or loaders, etc. should be saved for the Summary or Description. When changing your project title, remember to also ensure that your project slug (URL) matches and accurately represents your project.`,
}, },
], ],
}, },
{ {
id: 'slug', id: "slug",
question: 'Is the slug accurate and appropriate?', question: "Is the slug accurate and appropriate?",
shown: true, shown: true,
rules: ['Matches title / not misleading (acronyms are OK)'], rules: ["Matches title / not misleading (acronyms are OK)"],
options: [ options: [
{ {
name: 'Misused', name: "Misused",
resultingMessage: `## Misuse of Slug resultingMessage: `## Misuse of Slug
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your project slug (URL) must accurately represent your project. `, Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your project slug (URL) must accurately represent your project. `,
}, },
], ],
}, },
{ {
id: 'summary', id: "summary",
question: `Is the project's summary sufficient?`, question: `Is the project's summary sufficient?`,
shown: true, shown: true,
rules: [ rules: [
'The summary should provide a brief overview of your project that informs and entices users.', "The summary should provide a brief overview of your project that informs and entices users.",
`Should not be the exact same as the project's title`, `Should not be the exact same as the project's title`,
'Should not include any markdown formatting.', "Should not include any markdown formatting.",
], ],
options: [ options: [
{ {
name: 'Insufficient', name: "Insufficient",
resultingMessage: `## Insufficient Summary resultingMessage: `## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) Your project summary should provide a brief overview of your project that informs and entices users. Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) Your project summary should provide a brief overview of your project that informs and entices users.
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`, This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
}, },
{ {
name: 'Repeat of title', name: "Repeat of title",
resultingMessage: `## Insufficient Summary resultingMessage: `## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not be the same as your project's Title. Your project summary should provide a brief overview of your project that informs and entices users. Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not be the same as your project's Title. Your project summary should provide a brief overview of your project that informs and entices users.
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`, This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
}, },
{ {
name: 'Formatting', name: "Formatting",
resultingMessage: `## Insufficient Summary resultingMessage: `## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not include any extra formatting such as lists, or links. Your project summary should provide a brief overview of your project that informs and entices users. Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not include any extra formatting such as lists, or links. Your project summary should provide a brief overview of your project that informs and entices users.
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`, This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
@ -430,33 +430,33 @@ This is the first thing most people will see about your mod other than the Logo,
], ],
}, },
{ {
id: 'description', id: "description",
question: `Is the project's description sufficient?`, question: `Is the project's description sufficient?`,
navigate: `/${props.project.project_type}/${props.project.slug}`, navigate: `/${props.project.project_type}/${props.project.slug}`,
shown: true, shown: true,
rules: [ rules: [
'Should answer what the project specifically does or adds ', "Should answer what the project specifically does or adds ",
'Should answer why someone should want to download the project ', "Should answer why someone should want to download the project ",
'Should indicate any other critical information the user must know before downloading', "Should indicate any other critical information the user must know before downloading",
'Should be accessible (no fancy characters / non-standard text, no image-only descriptions, must have English component, etc)', "Should be accessible (no fancy characters / non-standard text, no image-only descriptions, must have English component, etc)",
], ],
options: [ options: [
{ {
name: 'Insufficient', name: "Insufficient",
resultingMessage: `## Insufficient Description resultingMessage: `## Insufficient Description
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project. Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
Currently, it looks like there are some missing details. Currently, it looks like there are some missing details.
%EXPLAINER%`, %EXPLAINER%`,
fillers: [ fillers: [
{ {
id: 'EXPLAINER', id: "EXPLAINER",
question: 'Please elaborate on how the author can improve their description.', question: "Please elaborate on how the author can improve their description.",
large: true, large: true,
}, },
], ],
}, },
{ {
name: 'Insufficient (default packs)', name: "Insufficient (default packs)",
resultingMessage: `## Insufficient Description resultingMessage: `## Insufficient Description
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project. Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
Currently, it looks like there are some missing details. Currently, it looks like there are some missing details.
@ -465,7 +465,7 @@ See descriptions like [Simply Optimized](https://modrinth.com/modpack/sop) or [A
`, `,
}, },
{ {
name: 'Insufficient (default projects)', name: "Insufficient (default projects)",
resultingMessage: `## Insufficient Description resultingMessage: `## Insufficient Description
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project. Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
Currently, it looks like there are some missing details. Currently, it looks like there are some missing details.
@ -474,35 +474,35 @@ See descriptions like [Sodium](https://modrinth.com/mod/sodium) or [LambDynamicL
`, `,
}, },
{ {
name: 'Non-english', name: "Non-english",
resultingMessage: `## No English Description resultingMessage: `## No English Description
Per section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#accessibility) a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations. You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your Description page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).`, Per section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#accessibility) a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations. You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your Description page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).`,
}, },
{ {
name: 'Unfinished', name: "Unfinished",
resultingMessage: `## Unfinished Description resultingMessage: `## Unfinished Description
It looks like your project Description is still a WIP seeing as %REASON%. Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations), if you have any questions on this feel free to reach out!`, It looks like your project Description is still a WIP seeing as %REASON%. Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations), if you have any questions on this feel free to reach out!`,
}, },
{ {
name: 'Headers as body text', name: "Headers as body text",
resultingMessage: `## Description Accessibility resultingMessage: `## Description Accessibility
In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we request that \`# header\`s not be used as body text. Headers are interpreted differently by screen-readers and thus should generally only be used for things like separating sections of your Description. If you would like to emphasize a particular sentence or paragraph, instead consider using \`**bold**\` text using the **B** button above the text editor.`, In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we request that \`# header\`s not be used as body text. Headers are interpreted differently by screen-readers and thus should generally only be used for things like separating sections of your Description. If you would like to emphasize a particular sentence or paragraph, instead consider using \`**bold**\` text using the **B** button above the text editor.`,
}, },
{ {
name: 'Image-only', name: "Image-only",
resultingMessage: `## Image Descriptions resultingMessage: `## Image Descriptions
In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we ask that you provide a text alternative to your current Description. It is important that your Description contains enough detail about your project that a user can have a full understanding of it from text alone. A text-based transcription allows for those using screen readers, and users with slow internet connections unable to load images to be able to access the contents of your Description. This also acts as a backup in case the image in your Description ever goes offline for some reason. In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we ask that you provide a text alternative to your current Description. It is important that your Description contains enough detail about your project that a user can have a full understanding of it from text alone. A text-based transcription allows for those using screen readers, and users with slow internet connections unable to load images to be able to access the contents of your Description. This also acts as a backup in case the image in your Description ever goes offline for some reason.
We appreciate how much effort you put into your Description, but accessibility is important to us at Modrinth, if you would like you could put the transcription of your Description entirely in a \`details\` tag, so as to not spoil the visuals of your Description.`, We appreciate how much effort you put into your Description, but accessibility is important to us at Modrinth, if you would like you could put the transcription of your Description entirely in a \`details\` tag, so as to not spoil the visuals of your Description.`,
}, },
{ {
name: 'Non-standard text', name: "Non-standard text",
resultingMessage: `## Description Accessibility resultingMessage: `## Description Accessibility
Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#clear-and-honest-function) your description must be plainly readable and accessible. Using non-standard text characters like Zalgo or "fancy text" in place of text anywhere in your project, including the Description, Summary, or Title can make your project pages inaccessible. This is important for users who rely on Screen Readers and for search engines in order to provide relevant results to users. Please remove any instances of this type of text.`, Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#clear-and-honest-function) your description must be plainly readable and accessible. Using non-standard text characters like Zalgo or "fancy text" in place of text anywhere in your project, including the Description, Summary, or Title can make your project pages inaccessible. This is important for users who rely on Screen Readers and for search engines in order to provide relevant results to users. Please remove any instances of this type of text.`,
}, },
], ],
}, },
{ {
id: 'links', id: "links",
question: `Are the project's links accessible and not misleading?`, question: `Are the project's links accessible and not misleading?`,
shown: shown:
props.project.issues_url || props.project.issues_url ||
@ -516,49 +516,49 @@ Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#cle
], ],
options: [ options: [
{ {
name: 'Links are misused', name: "Links are misused",
resultingMessage: `## Misuse of External Resources resultingMessage: `## Misuse of External Resources
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.`, Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.`,
}, },
{ {
name: 'Not accessible (source)', name: "Not accessible (source)",
resultingMessage: `## Unreachable Links resultingMessage: `## Unreachable Links
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project. Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
Currently, your Source link directs to a Page Not Found error, likely because your repository is private, make sure to make your repository public before resubmitting your project!`, Currently, your Source link directs to a Page Not Found error, likely because your repository is private, make sure to make your repository public before resubmitting your project!`,
}, },
{ {
name: 'Not accessible (other)', name: "Not accessible (other)",
resultingMessage: `## Unreachable Links resultingMessage: `## Unreachable Links
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project. Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
Currently, your %LINK% link is inaccessible!`, Currently, your %LINK% link is inaccessible!`,
fillers: [ fillers: [
{ {
id: 'LINK', id: "LINK",
question: 'Please specify the link type that is inaccessible.', question: "Please specify the link type that is inaccessible.",
}, },
], ],
}, },
], ],
}, },
{ {
id: 'categories', id: "categories",
question: `Are the project's tags/categories accurate?`, question: `Are the project's tags/categories accurate?`,
shown: props.project.categories.length > 0 || props.project.additional_categories.length > 0, shown: props.project.categories.length > 0 || props.project.additional_categories.length > 0,
options: [ options: [
{ {
name: 'Inaccurate', name: "Inaccurate",
resultingMessage: `## Misuse of Tags resultingMessage: `## Misuse of Tags
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.`, Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.`,
}, },
], ],
}, },
{ {
id: 'side-types', id: "side-types",
question: `Is the project's environment information accurate?`, question: `Is the project's environment information accurate?`,
shown: ['mod', 'modpack'].includes(props.project.project_type), shown: ["mod", "modpack"].includes(props.project.project_type),
options: [ options: [
{ {
name: 'Inaccurate (modpack)', name: "Inaccurate (modpack)",
resultingMessage: `## Incorrect Environment Information resultingMessage: `## Incorrect Environment Information
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side. Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
For a brief rundown of how this works: For a brief rundown of how this works:
@ -567,7 +567,7 @@ Most other modpacks that change how the game is played are going to be required
When in doubt, test for yourself or check the requirements of the mods in your pack.`, When in doubt, test for yourself or check the requirements of the mods in your pack.`,
}, },
{ {
name: 'Inaccurate (mod)', name: "Inaccurate (mod)",
resultingMessage: `## Environment Information resultingMessage: `## Environment Information
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side. Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
For a brief rundown of how this works: For a brief rundown of how this works:
@ -578,48 +578,48 @@ A mod that adds features, entities, or new blocks and items, generally will be r
], ],
}, },
{ {
id: 'gallery', id: "gallery",
navigate: `/${props.project.project_type}/${props.project.slug}/gallery`, navigate: `/${props.project.project_type}/${props.project.slug}/gallery`,
question: `Are the project's gallery images relevant?`, question: `Are the project's gallery images relevant?`,
shown: props.project.gallery.length > 0, shown: props.project.gallery.length > 0,
options: [ options: [
{ {
name: 'Not relevant', name: "Not relevant",
resultingMessage: `## Unrelated Gallery Images resultingMessage: `## Unrelated Gallery Images
Per section 5.5 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) any images in your project's Gallery must be relevant to the project and also include a Title.`, Per section 5.5 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) any images in your project's Gallery must be relevant to the project and also include a Title.`,
}, },
], ],
}, },
{ {
id: 'versions', id: "versions",
navigate: `/${props.project.project_type}/${props.project.slug}/versions`, navigate: `/${props.project.project_type}/${props.project.slug}/versions`,
question: `Are these project's files correct?`, question: `Are these project's files correct?`,
shown: !['modpack'].includes(props.project.project_type), shown: !["modpack"].includes(props.project.project_type),
rules: [ rules: [
'A multi-loader project should not use additional files for more loaders', "A multi-loader project should not use additional files for more loaders",
'Modpacks must be uploaded as MRPACK files. Be sure to check the project type is modpack (if not their file is malformed)', "Modpacks must be uploaded as MRPACK files. Be sure to check the project type is modpack (if not their file is malformed)",
], ],
options: [ options: [
{ {
name: 'Incorrect additional files', name: "Incorrect additional files",
resultingMessage: `## Incorrect Use of Additional Files resultingMessage: `## Incorrect Use of Additional Files
It looks like you've uploaded multiple \`mod.jar\` files to one Version as Additional Files. Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) each Version of your project must include only one \`mod.jar\` that corresponds to its respective Minecraft and loader versions. This allows users to easily find and download the file they need for the version they're on with ease. The Additional Files feature can be used for things like a \`Sources.jar\`. It looks like you've uploaded multiple \`mod.jar\` files to one Version as Additional Files. Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) each Version of your project must include only one \`mod.jar\` that corresponds to its respective Minecraft and loader versions. This allows users to easily find and download the file they need for the version they're on with ease. The Additional Files feature can be used for things like a \`Sources.jar\`.
Please upload each version of your mod separately, thank you.`, Please upload each version of your mod separately, thank you.`,
}, },
{ {
name: 'Invalid file type (modpacks)', name: "Invalid file type (modpacks)",
resultingMessage: `## Modpacks on Modrinth resultingMessage: `## Modpacks on Modrinth
It looks like you've uploaded your Modpack as a \`.zip\`, unfortunately, this is invalid and is why your project type is "Mod". I recommend taking a look at our support page about [Modrinth Modpacks](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth), and once you're ready feel free to resubmit your project as a \`.mrpack\`. Don't forget to delete the old files from your Versions!`, It looks like you've uploaded your Modpack as a \`.zip\`, unfortunately, this is invalid and is why your project type is "Mod". I recommend taking a look at our support page about [Modrinth Modpacks](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth), and once you're ready feel free to resubmit your project as a \`.mrpack\`. Don't forget to delete the old files from your Versions!`,
}, },
{ {
name: 'Invalid file type (resourcepacks)', name: "Invalid file type (resourcepacks)",
resultingMessage: `## Resource Packs on Modrinth resultingMessage: `## Resource Packs on Modrinth
It looks like you've selected loaders for your Resource Pack that are causing it to be marked as a different project type. Resource Packs must only be uploaded with the "Resource Pack" loader selected. Please re-upload all versions of your resource pack and make sure to only select "Resource Pack" as the loader.`, It looks like you've selected loaders for your Resource Pack that are causing it to be marked as a different project type. Resource Packs must only be uploaded with the "Resource Pack" loader selected. Please re-upload all versions of your resource pack and make sure to only select "Resource Pack" as the loader.`,
}, },
], ],
}, },
{ {
id: 'copyright', id: "copyright",
question: `Does the author have proper permissions to post this project?`, question: `Does the author have proper permissions to post this project?`,
shown: true, shown: true,
rules: [ rules: [
@ -628,20 +628,20 @@ It looks like you've selected loaders for your Resource Pack that are causing it
], ],
options: [ options: [
{ {
name: 'Re-upload', name: "Re-upload",
resultingMessage: `## Reuploads are forbidden resultingMessage: `## Reuploads are forbidden
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%. This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
Per section 4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) this is strictly forbidden. Per section 4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) this is strictly forbidden.
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.`, If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.`,
fillers: [ fillers: [
{ {
id: 'ORIGINAL_PROJECT', id: "ORIGINAL_PROJECT",
question: 'What is the title of the original project?', question: "What is the title of the original project?",
required: true, required: true,
}, },
{ {
id: 'ORIGINAL_AUTHOR', id: "ORIGINAL_AUTHOR",
question: 'What is the author of the original project?', question: "What is the author of the original project?",
required: true, required: true,
}, },
], ],
@ -649,25 +649,25 @@ If you believe this is an error, or you can verify you are the creator and right
], ],
}, },
{ {
id: 'rule-following', id: "rule-following",
question: `Does this project follow our content rules?`, question: `Does this project follow our content rules?`,
navigate: `/${props.project.project_type}/${props.project.slug}`, navigate: `/${props.project.project_type}/${props.project.slug}`,
shown: true, shown: true,
rules: [ rules: [
'Should not be a cheat/hack (without a server-side opt-out)', "Should not be a cheat/hack (without a server-side opt-out)",
'Should not contain sexually explicit / inappropriate content', "Should not contain sexually explicit / inappropriate content",
'Should not be excessively profane', "Should not be excessively profane",
'Should not promote any illegal activity (including illicit drugs + substances)', "Should not promote any illegal activity (including illicit drugs + substances)",
'Anything else infringing of our content rules (see 1.1-12, 3.1-3)', "Anything else infringing of our content rules (see 1.1-12, 3.1-3)",
], ],
options: [ options: [
{ {
name: 'No', name: "No",
resultingMessage: `%MESSAGE%`, resultingMessage: `%MESSAGE%`,
fillers: [ fillers: [
{ {
id: 'MESSAGE', id: "MESSAGE",
question: 'Please explain to the user how it infringes on our content rules.', question: "Please explain to the user how it infringes on our content rules.",
large: true, large: true,
}, },
], ],
@ -675,88 +675,88 @@ If you believe this is an error, or you can verify you are the creator and right
], ],
}, },
{ {
id: 'modpack-permissions', id: "modpack-permissions",
question: 'Modpack permissions', question: "Modpack permissions",
shown: ['modpack'].includes(props.project.project_type), shown: ["modpack"].includes(props.project.project_type),
options: [], options: [],
}, },
{ {
id: 'private-server', id: "private-server",
question: `Is this pack for a private server?`, question: `Is this pack for a private server?`,
shown: ['modpack'].includes(props.project.project_type), shown: ["modpack"].includes(props.project.project_type),
rules: [ rules: [
'Select this if you are withholding this pack since it is for a private server (for circumstances you would normally reject for).', "Select this if you are withholding this pack since it is for a private server (for circumstances you would normally reject for).",
], ],
options: [ options: [
{ {
name: 'Private server (withhold)', name: "Private server (withhold)",
resultingMessage: `## Private Server resultingMessage: `## Private Server
Under normal circumstances, your project would be rejected due to the issues listed above. However, since your project is intended for a specific server and not for general use, these requirements will be waived and your project will be withheld. This means it will be unlisted and accessible only through a direct link, without appearing in public search results. If you're fine with this, no further action is needed. Otherwise, feel free to resubmit once all issues have been addressed. `, Under normal circumstances, your project would be rejected due to the issues listed above. However, since your project is intended for a specific server and not for general use, these requirements will be waived and your project will be withheld. This means it will be unlisted and accessible only through a direct link, without appearing in public search results. If you're fine with this, no further action is needed. Otherwise, feel free to resubmit once all issues have been addressed. `,
}, },
], ],
}, },
].filter((x) => x.shown) ].filter((x) => x.shown),
) );
const currentStepIndex = ref(0) const currentStepIndex = ref(0);
const selectedOptions = ref({}) const selectedOptions = ref({});
function toggleOption(stepId, option) { function toggleOption(stepId, option) {
if (!selectedOptions.value[stepId]) { if (!selectedOptions.value[stepId]) {
selectedOptions.value[stepId] = [] selectedOptions.value[stepId] = [];
} }
const index = selectedOptions.value[stepId].findIndex((x) => x.name === option.name) const index = selectedOptions.value[stepId].findIndex((x) => x.name === option.name);
if (index === -1) { if (index === -1) {
selectedOptions.value[stepId].push(option) selectedOptions.value[stepId].push(option);
} else { } else {
selectedOptions.value[stepId].splice(index, 1) selectedOptions.value[stepId].splice(index, 1);
} }
const instance = getCurrentInstance() const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate() instance?.proxy?.$forceUpdate();
} }
function previousPage() { function previousPage() {
currentStepIndex.value -= 1 currentStepIndex.value -= 1;
generatedMessage.value = false generatedMessage.value = false;
if (steps.value[currentStepIndex.value].navigate) { if (steps.value[currentStepIndex.value].navigate) {
navigateTo(steps.value[currentStepIndex.value].navigate) navigateTo(steps.value[currentStepIndex.value].navigate);
} }
} }
async function nextPage() { async function nextPage() {
currentStepIndex.value += 1 currentStepIndex.value += 1;
if (steps.value[currentStepIndex.value].navigate) { if (steps.value[currentStepIndex.value].navigate) {
navigateTo(steps.value[currentStepIndex.value].navigate) navigateTo(steps.value[currentStepIndex.value].navigate);
} }
if (steps.value[currentStepIndex.value].id === 'modpack-permissions') { if (steps.value[currentStepIndex.value].id === "modpack-permissions") {
await initializeModPackData() await initializeModPackData();
} }
} }
async function initializeModPackData() { async function initializeModPackData() {
startLoading() startLoading();
try { try {
const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true }) const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true });
const projects = [] const projects = [];
for (const [hash, fileName] of Object.entries(raw.unknown_files)) { for (const [hash, fileName] of Object.entries(raw.unknown_files)) {
projects.push({ projects.push({
type: 'unknown', type: "unknown",
hash, hash,
file_name: fileName, file_name: fileName,
status: null, status: null,
approved: null, approved: null,
}) });
} }
for (const [hash, file] of Object.entries(raw.flame_files)) { for (const [hash, file] of Object.entries(raw.flame_files)) {
projects.push({ projects.push({
type: 'flame', type: "flame",
hash, hash,
file_name: file.file_name, file_name: file.file_name,
status: null, status: null,
@ -764,130 +764,130 @@ async function initializeModPackData() {
id: file.id, id: file.id,
url: file.url, url: file.url,
approved: null, approved: null,
}) });
} }
for (const [hash, file] of Object.entries(raw.identified)) { for (const [hash, file] of Object.entries(raw.identified)) {
if (file.status !== 'yes' && file.status !== 'with-attribution-and-source') { if (file.status !== "yes" && file.status !== "with-attribution-and-source") {
projects.push({ projects.push({
type: 'identified', type: "identified",
hash, hash,
file_name: file.file_name, file_name: file.file_name,
status: file.status, status: file.status,
approved: null, approved: null,
}) });
} }
} }
modPackData.value = projects modPackData.value = projects;
} catch (err) { } catch (err) {
const app = useNuxtApp() const app = useNuxtApp();
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
} }
const modPackData = ref(null) const modPackData = ref(null);
const modPackIndex = ref(0) const modPackIndex = ref(0);
const fileApprovalTypes = ref([ const fileApprovalTypes = ref([
{ {
id: 'yes', id: "yes",
name: 'Yes', name: "Yes",
}, },
{ {
id: 'with-attribution-and-source', id: "with-attribution-and-source",
name: 'With attribution and source', name: "With attribution and source",
}, },
{ {
id: 'with-attribution', id: "with-attribution",
name: 'With attribution', name: "With attribution",
}, },
{ {
id: 'no', id: "no",
name: 'No', name: "No",
}, },
{ {
id: 'permanent-no', id: "permanent-no",
name: 'Permanent no', name: "Permanent no",
}, },
{ {
id: 'unidentified', id: "unidentified",
name: 'Unidentified', name: "Unidentified",
}, },
]) ]);
const filePermissionTypes = ref([ const filePermissionTypes = ref([
{ {
id: true, id: true,
name: 'Yes', name: "Yes",
}, },
{ {
id: false, id: false,
name: 'No', name: "No",
}, },
]) ]);
const message = ref('') const message = ref("");
const generatedMessage = ref(false) const generatedMessage = ref(false);
const loadingMessage = ref(false) const loadingMessage = ref(false);
async function generateMessage() { async function generateMessage() {
message.value = '' message.value = "";
loadingMessage.value = true loadingMessage.value = true;
function printMods(mods, msg) { function printMods(mods, msg) {
if (mods.length === 0) { if (mods.length === 0) {
return return;
} }
message.value += msg message.value += msg;
message.value += '\n\n' message.value += "\n\n";
for (const mod of mods) { for (const mod of mods) {
message.value += `- ${mod}\n` message.value += `- ${mod}\n`;
} }
} }
if (modPackData.value && modPackData.value.length > 0) { if (modPackData.value && modPackData.value.length > 0) {
const updateProjects = {} const updateProjects = {};
const attributeMods = [] const attributeMods = [];
const noMods = [] const noMods = [];
const permanentNoMods = [] const permanentNoMods = [];
const unidentifiedMods = [] const unidentifiedMods = [];
for (const project of modPackData.value) { for (const project of modPackData.value) {
if (project.type === 'unknown') { if (project.type === "unknown") {
updateProjects[project.hash] = { updateProjects[project.hash] = {
type: 'unknown', type: "unknown",
status: project.status, status: project.status,
proof: project.proof, proof: project.proof,
title: project.title, title: project.title,
link: project.url, link: project.url,
} };
} }
if (project.type === 'flame') { if (project.type === "flame") {
updateProjects[project.hash] = { updateProjects[project.hash] = {
type: 'flame', type: "flame",
status: project.status, status: project.status,
id: project.id, id: project.id,
link: project.url, link: project.url,
title: project.title, title: project.title,
} };
} }
if (project.status === 'with-attribution' && !project.approved) { if (project.status === "with-attribution" && !project.approved) {
attributeMods.push(project.file_name) attributeMods.push(project.file_name);
} else if (project.status === 'unidentified' && !project.approved) { } else if (project.status === "unidentified" && !project.approved) {
unidentifiedMods.push(project.file_name) unidentifiedMods.push(project.file_name);
} else if (project.status === 'no' && !project.approved) { } else if (project.status === "no" && !project.approved) {
noMods.push(project.file_name) noMods.push(project.file_name);
} else if (project.status === 'permanent-no') { } else if (project.status === "permanent-no") {
permanentNoMods.push(project.file_name) permanentNoMods.push(project.file_name);
} }
} }
@ -895,17 +895,17 @@ async function generateMessage() {
try { try {
await useBaseFetch(`moderation/project`, { await useBaseFetch(`moderation/project`, {
internal: true, internal: true,
method: 'POST', method: "POST",
body: updateProjects, body: updateProjects,
}) });
} catch (err) { } catch (err) {
const app = useNuxtApp() const app = useNuxtApp();
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
} }
@ -915,106 +915,109 @@ async function generateMessage() {
permanentNoMods.length > 0 || permanentNoMods.length > 0 ||
unidentifiedMods.length > 0 unidentifiedMods.length > 0
) { ) {
message.value += '## Copyrighted Content \n' message.value += "## Copyrighted Content \n";
printMods( printMods(
attributeMods, attributeMods,
"The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your modpack description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):" "The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your modpack description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):",
) );
printMods( printMods(
noMods, noMods,
'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:' "The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:",
) );
printMods( printMods(
permanentNoMods, permanentNoMods,
"The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:" "The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:",
) );
printMods( printMods(
unidentifiedMods, unidentifiedMods,
'The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:' "The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:",
) );
message.value += '\n\n' message.value += "\n\n";
} }
} }
for (const options of Object.values(selectedOptions.value)) { for (const options of Object.values(selectedOptions.value)) {
for (const option of options) { for (const option of options) {
let addonMessage = option.resultingMessage let addonMessage = option.resultingMessage;
if (option.fillers && option.fillers.length > 0) { if (option.fillers && option.fillers.length > 0) {
for (const filler of option.fillers) { for (const filler of option.fillers) {
addonMessage = addonMessage.replace(new RegExp(`%${filler.id}%`, 'g'), filler.value ?? '') addonMessage = addonMessage.replace(
new RegExp(`%${filler.id}%`, "g"),
filler.value ?? "",
);
} }
} }
message.value += addonMessage message.value += addonMessage;
message.value += '\n\n' message.value += "\n\n";
} }
} }
generatedMessage.value = true generatedMessage.value = true;
loadingMessage.value = false loadingMessage.value = false;
currentStepIndex.value += 1 currentStepIndex.value += 1;
await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`) await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`);
} }
const done = ref(false) const done = ref(false);
async function sendMessage(status) { async function sendMessage(status) {
startLoading() startLoading();
try { try {
await useBaseFetch(`project/${props.project.id}`, { await useBaseFetch(`project/${props.project.id}`, {
method: 'PATCH', method: "PATCH",
body: { body: {
status, status,
}, },
}) });
if (message.value) { if (message.value) {
await useBaseFetch(`thread/${props.project.thread_id}`, { await useBaseFetch(`thread/${props.project.thread_id}`, {
method: 'POST', method: "POST",
body: { body: {
body: { body: {
type: 'text', type: "text",
body: message.value, body: message.value,
}, },
}, },
}) });
} }
await props.resetProject() await props.resetProject();
done.value = true done.value = true;
} catch (err) { } catch (err) {
const app = useNuxtApp() const app = useNuxtApp();
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
} }
const router = useNativeRouter() const router = useNativeRouter();
async function goToNextProject() { async function goToNextProject() {
const project = props.futureProjects[0] const project = props.futureProjects[0];
if (!project) { if (!project) {
await navigateTo('/moderation/review') await navigateTo("/moderation/review");
} }
await router.push({ await router.push({
name: 'type-id', name: "type-id",
params: { params: {
type: 'project', type: "project",
id: project, id: project,
}, },
state: { state: {
showChecklist: true, showChecklist: true,
projects: props.futureProjects.slice(1), projects: props.futureProjects.slice(1),
}, },
}) });
} }
</script> </script>
@ -1042,7 +1045,9 @@ async function goToNextProject() {
.option-selected { .option-selected {
color: var(--color-contrast); color: var(--color-contrast);
background-color: var(--color-brand-highlight); 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);
} }
} }
</style> </style>

View File

@ -24,7 +24,7 @@
</template> </template>
<script setup> <script setup>
const route = useNativeRoute() const route = useNativeRoute();
const props = defineProps({ const props = defineProps({
links: { links: {
@ -35,59 +35,59 @@ const props = defineProps({
default: null, default: null,
type: String, type: String,
}, },
}) });
const sliderPositionX = ref(0) const sliderPositionX = ref(0);
const sliderPositionY = ref(18) const sliderPositionY = ref(18);
const selectedElementWidth = ref(0) const selectedElementWidth = ref(0);
const activeIndex = ref(-1) const activeIndex = ref(-1);
const oldIndex = ref(-1) const oldIndex = ref(-1);
const filteredLinks = computed(() => const filteredLinks = computed(() =>
props.links.filter((x) => (x.shown === undefined ? true : x.shown)) props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
) );
const positionToMoveX = computed(() => `${sliderPositionX.value}px`) const positionToMoveX = computed(() => `${sliderPositionX.value}px`);
const positionToMoveY = computed(() => `${sliderPositionY.value}px`) const positionToMoveY = computed(() => `${sliderPositionY.value}px`);
const sliderWidth = computed(() => `${selectedElementWidth.value}px`) const sliderWidth = computed(() => `${selectedElementWidth.value}px`);
function pickLink() { function pickLink() {
console.log('link is picking') console.log("link is picking");
activeIndex.value = props.query activeIndex.value = props.query
? filteredLinks.value.findIndex( ? filteredLinks.value.findIndex(
(x) => (x.href === '' ? undefined : x.href) === route.path[props.query] (x) => (x.href === "" ? undefined : x.href) === route.path[props.query],
) )
: filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path)) : filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path));
if (activeIndex.value !== -1) { if (activeIndex.value !== -1) {
startAnimation() startAnimation();
} else { } else {
oldIndex.value = -1 oldIndex.value = -1;
sliderPositionX.value = 0 sliderPositionX.value = 0;
selectedElementWidth.value = 0 selectedElementWidth.value = 0;
} }
} }
const linkElements = ref() const linkElements = ref();
function startAnimation() { function startAnimation() {
const el = linkElements.value[activeIndex.value].$el const el = linkElements.value[activeIndex.value].$el;
sliderPositionX.value = el.offsetLeft sliderPositionX.value = el.offsetLeft;
sliderPositionY.value = el.offsetTop + el.offsetHeight sliderPositionY.value = el.offsetTop + el.offsetHeight;
selectedElementWidth.value = el.offsetWidth selectedElementWidth.value = el.offsetWidth;
} }
onMounted(() => { onMounted(() => {
window.addEventListener('resize', pickLink) window.addEventListener("resize", pickLink);
pickLink() pickLink();
}) });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', pickLink) window.removeEventListener("resize", pickLink);
}) });
watch(route, () => pickLink()) watch(route, () => pickLink());
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,7 +7,7 @@
</template> </template>
<script> <script>
export default {} export default {};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -23,7 +23,7 @@
</template> </template>
<script> <script>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component' import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
export default { export default {
components: { components: {
@ -55,7 +55,7 @@ export default {
type: Boolean, type: Boolean,
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -184,8 +184,8 @@
class="iconified-button square-button brand-button button-transparent" class="iconified-button square-button brand-button button-transparent"
@click=" @click="
() => { () => {
acceptTeamInvite(notification.body.team_id) acceptTeamInvite(notification.body.team_id);
read() read();
} }
" "
> >
@ -196,8 +196,8 @@
class="iconified-button square-button danger-button button-transparent" class="iconified-button square-button danger-button button-transparent"
@click=" @click="
() => { () => {
removeSelfFromTeam(notification.body.team_id) removeSelfFromTeam(notification.body.team_id);
read() read();
} }
" "
> >
@ -222,8 +222,8 @@
class="iconified-button brand-button" class="iconified-button brand-button"
@click=" @click="
() => { () => {
acceptTeamInvite(notification.body.team_id) acceptTeamInvite(notification.body.team_id);
read() read();
} }
" "
> >
@ -233,8 +233,8 @@
class="iconified-button danger-button" class="iconified-button danger-button"
@click=" @click="
() => { () => {
removeSelfFromTeam(notification.body.team_id) removeSelfFromTeam(notification.body.team_id);
read() read();
} }
" "
> >
@ -288,29 +288,29 @@
</template> </template>
<script setup> <script setup>
import { renderString } from '@modrinth/utils' import { renderString } from "@modrinth/utils";
import InvitationIcon from '~/assets/images/utils/user-plus.svg?component' import InvitationIcon from "~/assets/images/utils/user-plus.svg?component";
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component' import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?component' import NotificationIcon from "~/assets/images/sidebar/notifications.svg?component";
import ReadIcon from '~/assets/images/utils/check-circle.svg?component' import ReadIcon from "~/assets/images/utils/check-circle.svg?component";
import CalendarIcon from '~/assets/images/utils/calendar.svg?component' import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import VersionIcon from '~/assets/images/utils/version.svg?component' import VersionIcon from "~/assets/images/utils/version.svg?component";
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import ExternalIcon from '~/assets/images/utils/external.svg?component' import ExternalIcon from "~/assets/images/utils/external.svg?component";
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue' import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { getProjectLink, getVersionLink } from '~/helpers/projects.js' import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
import { getUserLink } from '~/helpers/users.js' import { getUserLink } from "~/helpers/users.js";
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js' import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
import { markAsRead } from '~/helpers/notifications.js' import { markAsRead } from "~/helpers/notifications.js";
import DoubleIcon from '~/components/ui/DoubleIcon.vue' import DoubleIcon from "~/components/ui/DoubleIcon.vue";
import Avatar from '~/components/ui/Avatar.vue' import Avatar from "~/components/ui/Avatar.vue";
import Badge from '~/components/ui/Badge.vue' import Badge from "~/components/ui/Badge.vue";
import CopyCode from '~/components/ui/CopyCode.vue' import CopyCode from "~/components/ui/CopyCode.vue";
import Categories from '~/components/ui/search/Categories.vue' import Categories from "~/components/ui/search/Categories.vue";
const app = useNuxtApp() const app = useNuxtApp();
const emit = defineEmits(['update:notifications']) const emit = defineEmits(["update:notifications"]);
const props = defineProps({ const props = defineProps({
notification: { notification: {
@ -333,34 +333,34 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const flags = useFeatureFlags() const flags = useFeatureFlags();
const tags = useTags() const tags = useTags();
const type = computed(() => const type = computed(() =>
!props.notification.body || props.notification.body.type === 'legacy_markdown' !props.notification.body || props.notification.body.type === "legacy_markdown"
? null ? null
: props.notification.body.type : props.notification.body.type,
) );
const thread = computed(() => props.notification.extra_data.thread) const thread = computed(() => props.notification.extra_data.thread);
const report = computed(() => props.notification.extra_data.report) const report = computed(() => props.notification.extra_data.report);
const project = computed(() => props.notification.extra_data.project) const project = computed(() => props.notification.extra_data.project);
const version = computed(() => props.notification.extra_data.version) const version = computed(() => props.notification.extra_data.version);
const user = computed(() => props.notification.extra_data.user) const user = computed(() => props.notification.extra_data.user);
const organization = computed(() => props.notification.extra_data.organization) const organization = computed(() => props.notification.extra_data.organization);
const invitedBy = computed(() => props.notification.extra_data.invited_by) const invitedBy = computed(() => props.notification.extra_data.invited_by);
const threadLink = computed(() => { const threadLink = computed(() => {
if (report.value) { if (report.value) {
return `/dashboard/report/${report.value.id}` return `/dashboard/report/${report.value.id}`;
} else if (project.value) { } else if (project.value) {
return `${getProjectLink(project.value)}/moderation#messages` return `${getProjectLink(project.value)}/moderation#messages`;
} }
return '#' return "#";
}) });
const hasBody = computed(() => !type.value || thread.value || type.value === 'project_update') const hasBody = computed(() => !type.value || thread.value || type.value === "project_update");
async function read() { async function read() {
try { try {
@ -369,54 +369,54 @@ async function read() {
...(props.notification.grouped_notifs ...(props.notification.grouped_notifs
? props.notification.grouped_notifs.map((notif) => notif.id) ? props.notification.grouped_notifs.map((notif) => notif.id)
: []), : []),
] ];
const updateNotifs = await markAsRead(ids) const updateNotifs = await markAsRead(ids);
const newNotifs = updateNotifs(props.notifications) const newNotifs = updateNotifs(props.notifications);
emit('update:notifications', newNotifs) emit("update:notifications", newNotifs);
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Error marking notification as read', title: "Error marking notification as read",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
} }
async function performAction(notification, actionIndex) { async function performAction(notification, actionIndex) {
startLoading() startLoading();
try { try {
await read() await read();
if (actionIndex !== null) { if (actionIndex !== null) {
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, { await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
method: notification.actions[actionIndex].action_route[0].toUpperCase(), method: notification.actions[actionIndex].action_route[0].toUpperCase(),
}) });
} }
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data.description, text: err.data.description,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
} }
function getMessages() { function getMessages() {
const messages = [] const messages = [];
if (props.notification.body.message_id) { if (props.notification.body.message_id) {
messages.push(props.notification.body.message_id) messages.push(props.notification.body.message_id);
} }
if (props.notification.grouped_notifs) { if (props.notification.grouped_notifs) {
for (const notif of props.notification.grouped_notifs) { for (const notif of props.notification.grouped_notifs) {
if (notif.body.message_id) { if (notif.body.message_id) {
messages.push(notif.body.message_id) messages.push(notif.body.message_id);
} }
} }
} }
return messages return messages;
} }
</script> </script>
@ -424,35 +424,35 @@ function getMessages() {
.notification { .notification {
display: grid; display: grid;
grid-template: grid-template:
'icon title' "icon title"
'actions actions' "actions actions"
'date date'; "date date";
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
grid-template-rows: min-content min-content min-content; grid-template-rows: min-content min-content min-content;
gap: var(--spacing-card-sm); gap: var(--spacing-card-sm);
&.compact { &.compact {
grid-template: grid-template:
'icon title actions' "icon title actions"
'date date date'; "date date date";
grid-template-columns: min-content 1fr auto; grid-template-columns: min-content 1fr auto;
grid-template-rows: auto min-content; grid-template-rows: auto min-content;
} }
&.has-body { &.has-body {
grid-template: grid-template:
'icon title' "icon title"
'body body' "body body"
'actions actions' "actions actions"
'date date'; "date date";
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
grid-template-rows: min-content auto auto min-content; grid-template-rows: min-content auto auto min-content;
&.compact { &.compact {
grid-template: grid-template:
'icon title actions' "icon title actions"
'body body body' "body body body"
'date date date'; "date date date";
grid-template-columns: min-content 1fr auto; grid-template-columns: min-content 1fr auto;
grid-template-rows: min-content auto min-content; grid-template-rows: min-content auto min-content;
} }

View File

@ -18,10 +18,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
const notifications = useNotifications() const notifications = useNotifications();
function stopTimer(notif) { function stopTimer(notif) {
clearTimeout(notif.timer) clearTimeout(notif.timer);
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -54,51 +54,51 @@
</Modal> </Modal>
</template> </template>
<script setup> <script setup>
import { XIcon as CrossIcon, CheckIcon } from '@modrinth/assets' import { XIcon as CrossIcon, CheckIcon } from "@modrinth/assets";
import { Modal, Button } from '@modrinth/ui' import { Modal, Button } from "@modrinth/ui";
const router = useNativeRouter() const router = useNativeRouter();
const name = ref('') const name = ref("");
const slug = ref('') const slug = ref("");
const description = ref('') const description = ref("");
const manualSlug = ref(false) const manualSlug = ref(false);
const modal = ref() const modal = ref();
async function createProject() { async function createProject() {
startLoading() startLoading();
try { try {
const value = { const value = {
name: name.value.trim(), name: name.value.trim(),
description: description.value.trim(), description: description.value.trim(),
slug: slug.value.trim().replace(/ +/g, ''), slug: slug.value.trim().replace(/ +/g, ""),
} };
const result = await useBaseFetch('organization', { const result = await useBaseFetch("organization", {
method: 'POST', method: "POST",
body: JSON.stringify(value), body: JSON.stringify(value),
apiVersion: 3, apiVersion: 3,
}) });
modal.value.hide() modal.value.hide();
await router.push(`/organization/${result.slug}`) await router.push(`/organization/${result.slug}`);
} catch (err) { } catch (err) {
console.error(err) console.error(err);
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data.description, text: err.data.description,
type: 'error', type: "error",
}) });
} }
stopLoading() stopLoading();
} }
function show() { function show() {
name.value = '' name.value = "";
description.value = '' description.value = "";
modal.value.show() modal.value.show();
} }
function updateSlug() { function updateSlug() {
@ -106,15 +106,15 @@ function updateSlug() {
slug.value = name.value slug.value = name.value
.trim() .trim()
.toLowerCase() .toLowerCase()
.replaceAll(' ', '-') .replaceAll(" ", "-")
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '') .replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
.replaceAll(/--+/gm, '-') .replaceAll(/--+/gm, "-");
} }
} }
defineExpose({ defineExpose({
show, show,
}) });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -3,8 +3,8 @@
<Modal ref="modalOpen" header="Transfer Projects"> <Modal ref="modalOpen" header="Transfer Projects">
<div class="universal-modal items"> <div class="universal-modal items">
<div class="table"> <div class="table">
<div class="table-row table-head"> <div class="table-head table-row">
<div class="table-cell check-cell"> <div class="check-cell table-cell">
<Checkbox <Checkbox
:model-value="selectedProjects.length === props.projects.length" :model-value="selectedProjects.length === props.projects.length"
@update:model-value="toggleSelectedProjects()" @update:model-value="toggleSelectedProjects()"
@ -17,7 +17,7 @@
<div class="table-cell" /> <div class="table-cell" />
</div> </div>
<div v-for="project in props.projects" :key="`project-${project.id}`" class="table-row"> <div v-for="project in props.projects" :key="`project-${project.id}`" class="table-row">
<div class="table-cell check-cell"> <div class="check-cell table-cell">
<Checkbox <Checkbox
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS" :disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:model-value="selectedProjects.includes(project)" :model-value="selectedProjects.includes(project)"
@ -59,9 +59,9 @@
<span>{{ <span>{{
$formatProjectType( $formatProjectType(
$getProjectTypeForDisplay( $getProjectTypeForDisplay(
project.project_types?.[0] ?? 'project', project.project_types?.[0] ?? "project",
project.loaders project.loaders,
) ),
) )
}}</span> }}</span>
</div> </div>
@ -88,13 +88,13 @@
<span> <span>
{{ {{
selectedProjects.length === props.projects.length selectedProjects.length === props.projects.length
? 'All' ? "All"
: selectedProjects.length : selectedProjects.length
}} }}
</span> </span>
<span> <span>
{{ ' ' }} {{ " " }}
{{ selectedProjects.length === 1 ? 'project' : 'projects' }} {{ selectedProjects.length === 1 ? "project" : "projects" }}
</span> </span>
</span> </span>
</Button> </Button>
@ -109,39 +109,39 @@
</template> </template>
<script setup> <script setup>
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets' import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
import { Button, Modal, Checkbox, CopyCode, Avatar } from '@modrinth/ui' import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
const modalOpen = ref(null) const modalOpen = ref(null);
const props = defineProps({ const props = defineProps({
projects: { projects: {
type: Array, type: Array,
required: true, required: true,
}, },
}) });
// define emit for submission // define emit for submission
const emit = defineEmits(['submit']) const emit = defineEmits(["submit"]);
const selectedProjects = ref([]) const selectedProjects = ref([]);
const toggleSelectedProjects = () => { const toggleSelectedProjects = () => {
if (selectedProjects.value.length === props.projects.length) { if (selectedProjects.value.length === props.projects.length) {
selectedProjects.value = [] selectedProjects.value = [];
} else { } else {
selectedProjects.value = props.projects selectedProjects.value = props.projects;
}
} }
};
const onSubmitHandler = () => { const onSubmitHandler = () => {
if (selectedProjects.value.length === 0) { if (selectedProjects.value.length === 0) {
return return;
}
emit('submit', selectedProjects.value)
selectedProjects.value = []
modalOpen.value?.hide()
} }
emit("submit", selectedProjects.value);
selectedProjects.value = [];
modalOpen.value?.hide();
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -175,7 +175,7 @@ const onSubmitHandler = () => {
.table-row { .table-row {
display: grid; display: grid;
grid-template: 'checkbox icon name type settings' 'checkbox icon id type settings'; grid-template: "checkbox icon name type settings" "checkbox icon id type settings";
grid-template-columns: grid-template-columns:
min-content min-content minmax(min-content, 2fr) min-content min-content minmax(min-content, 2fr)
minmax(min-content, 1fr) min-content; minmax(min-content, 1fr) min-content;
@ -207,7 +207,7 @@ const onSubmitHandler = () => {
} }
.table-head { .table-head {
grid-template: 'checkbox settings'; grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr); grid-template-columns: min-content minmax(min-content, 1fr);
:nth-child(2), :nth-child(2),
@ -222,7 +222,7 @@ const onSubmitHandler = () => {
@media screen and (max-width: 560px) { @media screen and (max-width: 560px) {
.table-row { .table-row {
display: grid; 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; grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
:nth-child(5) { :nth-child(5) {
@ -231,7 +231,7 @@ const onSubmitHandler = () => {
} }
.table-head { .table-head {
grid-template: 'checkbox settings'; grid-template: "checkbox settings";
grid-template-columns: min-content minmax(min-content, 1fr); grid-template-columns: min-content minmax(min-content, 1fr);
} }
} }

View File

@ -51,9 +51,9 @@
</template> </template>
<script> <script>
import GapIcon from '~/assets/images/utils/gap.svg?component' import GapIcon from "~/assets/images/utils/gap.svg?component";
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?component' import LeftArrowIcon from "~/assets/images/utils/left-arrow.svg?component";
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?component' import RightArrowIcon from "~/assets/images/utils/right-arrow.svg?component";
export default { export default {
components: { components: {
@ -73,47 +73,47 @@ export default {
linkFunction: { linkFunction: {
type: Function, type: Function,
default() { default() {
return () => '/' return () => "/";
}, },
}, },
}, },
emits: ['switch-page'], emits: ["switch-page"],
computed: { computed: {
pages() { pages() {
let pages = [] let pages = [];
if (this.count > 7) { if (this.count > 7) {
if (this.page + 3 >= this.count) { if (this.page + 3 >= this.count) {
pages = [ pages = [
1, 1,
'-', "-",
this.count - 4, this.count - 4,
this.count - 3, this.count - 3,
this.count - 2, this.count - 2,
this.count - 1, this.count - 1,
this.count, this.count,
] ];
} else if (this.page > 5) { } else if (this.page > 5) {
pages = [1, '-', this.page - 1, this.page, this.page + 1, '-', this.count] pages = [1, "-", this.page - 1, this.page, this.page + 1, "-", this.count];
} else { } else {
pages = [1, 2, 3, 4, 5, '-', this.count] pages = [1, 2, 3, 4, 5, "-", this.count];
} }
} else { } else {
pages = Array.from({ length: this.count }, (_, i) => i + 1) pages = Array.from({ length: this.count }, (_, i) => i + 1);
} }
return pages return pages;
}, },
}, },
methods: { methods: {
switchPage(newPage) { switchPage(newPage) {
this.$emit('switch-page', newPage) this.$emit("switch-page", newPage);
if (newPage !== null && newPage !== '' && !isNaN(newPage)) { if (newPage !== null && newPage !== "" && !isNaN(newPage)) {
this.$emit('switch-page', Math.min(Math.max(newPage, 1), this.count)) this.$emit("switch-page", Math.min(Math.max(newPage, 1), this.count));
} }
}, },
}, },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -126,7 +126,10 @@ a {
border-radius: 2rem; border-radius: 2rem;
background: var(--color-raised-bg); background: var(--color-raised-bg);
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline 0.2s ease-in-out; outline 0.2s ease-in-out;
&.page-number.current { &.page-number.current {

View File

@ -90,15 +90,15 @@
</template> </template>
<script> <script>
import Categories from '~/components/ui/search/Categories.vue' import Categories from "~/components/ui/search/Categories.vue";
import Badge from '~/components/ui/Badge.vue' import Badge from "~/components/ui/Badge.vue";
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue' import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import CalendarIcon from '~/assets/images/utils/calendar.svg?component' import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import EditIcon from '~/assets/images/utils/updated.svg?component' import EditIcon from "~/assets/images/utils/updated.svg?component";
import DownloadIcon from '~/assets/images/utils/download.svg?component' import DownloadIcon from "~/assets/images/utils/download.svg?component";
import HeartIcon from '~/assets/images/utils/heart.svg?component' import HeartIcon from "~/assets/images/utils/heart.svg?component";
import Avatar from '~/components/ui/Avatar.vue' import Avatar from "~/components/ui/Avatar.vue";
export default { export default {
components: { components: {
@ -114,15 +114,15 @@ export default {
props: { props: {
id: { id: {
type: String, type: String,
default: 'modrinth-0', default: "modrinth-0",
}, },
type: { type: {
type: String, type: String,
default: 'mod', default: "mod",
}, },
name: { name: {
type: String, type: String,
default: 'Project Name', default: "Project Name",
}, },
author: { author: {
type: String, type: String,
@ -130,11 +130,11 @@ export default {
}, },
description: { description: {
type: String, type: String,
default: 'A _type description', default: "A _type description",
}, },
iconUrl: { iconUrl: {
type: String, type: String,
default: '#', default: "#",
required: false, required: false,
}, },
downloads: { downloads: {
@ -149,7 +149,7 @@ export default {
}, },
createdAt: { createdAt: {
type: String, type: String,
default: '0000-00-00', default: "0000-00-00",
}, },
updatedAt: { updatedAt: {
type: String, type: String,
@ -158,7 +158,7 @@ export default {
categories: { categories: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
status: { status: {
@ -172,12 +172,12 @@ export default {
serverSide: { serverSide: {
type: String, type: String,
required: false, required: false,
default: '', default: "",
}, },
clientSide: { clientSide: {
type: String, type: String,
required: false, required: false,
default: '', default: "",
}, },
moderation: { moderation: {
type: Boolean, type: Boolean,
@ -216,25 +216,25 @@ export default {
}, },
}, },
setup() { setup() {
const tags = useTags() const tags = useTags();
return { tags } return { tags };
}, },
computed: { computed: {
projectTypeDisplay() { projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories) return this.$getProjectTypeForDisplay(this.type, this.categories);
}, },
toColor() { toColor() {
let color = this.color let color = this.color;
color >>>= 0 color >>>= 0;
const b = color & 0xff const b = color & 0xff;
const g = (color & 0xff00) >>> 8 const g = (color & 0xff00) >>> 8;
const r = (color & 0xff0000) >>> 16 const r = (color & 0xff0000) >>> 16;
return 'rgba(' + [r, g, b, 1].join(',') + ')' return "rgba(" + [r, g, b, 1].join(",") + ")";
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -247,9 +247,9 @@ export default {
.display-mode--list .project-card { .display-mode--list .project-card {
grid-template: grid-template:
'icon title stats' "icon title stats"
'icon description stats' "icon description stats"
'icon tags stats'; "icon tags stats";
grid-template-columns: min-content 1fr auto; grid-template-columns: min-content 1fr auto;
grid-template-rows: min-content 1fr min-content; grid-template-rows: min-content 1fr min-content;
column-gap: var(--spacing-card-md); column-gap: var(--spacing-card-md);
@ -258,20 +258,20 @@ export default {
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
grid-template: grid-template:
'icon title' "icon title"
'icon description' "icon description"
'icon tags' "icon tags"
'stats stats'; "stats stats";
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
grid-template-rows: min-content 1fr min-content min-content; grid-template-rows: min-content 1fr min-content min-content;
} }
@media screen and (max-width: 550px) { @media screen and (max-width: 550px) {
grid-template: grid-template:
'icon title' "icon title"
'icon description' "icon description"
'tags tags' "tags tags"
'stats stats'; "stats stats";
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
grid-template-rows: min-content 1fr min-content min-content; grid-template-rows: min-content 1fr min-content min-content;
} }
@ -280,7 +280,7 @@ export default {
.display-mode--gallery .project-card, .display-mode--gallery .project-card,
.display-mode--grid .project-card { .display-mode--grid .project-card {
padding: 0 0 var(--spacing-card-bg) 0; padding: 0 0 var(--spacing-card-bg) 0;
grid-template: 'gallery gallery' 'icon title' 'description description' 'tags tags' 'stats stats'; grid-template: "gallery gallery" "icon title" "description description" "tags tags" "stats stats";
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
grid-template-rows: min-content min-content 1fr min-content min-content; grid-template-rows: min-content min-content 1fr min-content min-content;
row-gap: var(--spacing-card-sm); row-gap: var(--spacing-card-sm);
@ -311,7 +311,9 @@ export default {
img, img,
svg { svg {
border-radius: var(--size-rounded-lg); border-radius: var(--size-rounded-lg);
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);
} }
} }
@ -502,10 +504,10 @@ export default {
.small-mode { .small-mode {
@media screen and (min-width: 750px) { @media screen and (min-width: 750px) {
grid-template: grid-template:
'icon title' "icon title"
'icon description' "icon description"
'icon tags' "icon tags"
'stats stats' !important; "stats stats" !important;
grid-template-columns: min-content auto !important; grid-template-columns: min-content auto !important;
grid-template-rows: min-content 1fr min-content min-content !important; grid-template-rows: min-content 1fr min-content min-content !important;

View File

@ -106,17 +106,17 @@
</template> </template>
<script setup> <script setup>
import { formatProjectType } from '~/plugins/shorthands.js' import { formatProjectType } from "~/plugins/shorthands.js";
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component' import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component' import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import RequiredIcon from '~/assets/images/utils/asterisk.svg?component' import RequiredIcon from "~/assets/images/utils/asterisk.svg?component";
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg?component' import SuggestionIcon from "~/assets/images/utils/lightbulb.svg?component";
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component' import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import SendIcon from '~/assets/images/utils/send.svg?component' import SendIcon from "~/assets/images/utils/send.svg?component";
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js' import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
const props = defineProps({ const props = defineProps({
project: { project: {
@ -126,7 +126,7 @@ const props = defineProps({
versions: { versions: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
currentMember: { currentMember: {
@ -147,7 +147,7 @@ const props = defineProps({
}, },
routeName: { routeName: {
type: String, type: String,
default: '', default: "",
}, },
auth: { auth: {
type: Object, type: Object,
@ -162,12 +162,12 @@ const props = defineProps({
default() { default() {
return () => { return () => {
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: 'setProcessing function not found', text: "setProcessing function not found",
type: 'error', type: "error",
}) });
} };
}, },
}, },
toggleCollapsed: { toggleCollapsed: {
@ -175,12 +175,12 @@ const props = defineProps({
default() { default() {
return () => { return () => {
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: 'toggleCollapsed function not found', text: "toggleCollapsed function not found",
type: 'error', type: "error",
}) });
} };
}, },
}, },
updateMembers: { updateMembers: {
@ -188,81 +188,81 @@ const props = defineProps({
default() { default() {
return () => { return () => {
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: 'updateMembers function not found', text: "updateMembers function not found",
type: 'error', type: "error",
}) });
} };
}, },
}, },
}) });
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured)) const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured));
const nags = computed(() => [ const nags = computed(() => [
{ {
condition: props.versions.length < 1, condition: props.versions.length < 1,
title: 'Upload a version', title: "Upload a version",
id: 'upload-version', id: "upload-version",
description: 'At least one version is required for a project to be submitted for review.', description: "At least one version is required for a project to be submitted for review.",
status: 'required', status: "required",
link: { link: {
path: 'versions', path: "versions",
title: 'Visit versions page', title: "Visit versions page",
hide: props.routeName === 'type-id-versions', hide: props.routeName === "type-id-versions",
}, },
}, },
{ {
condition: condition:
props.project.body === '' || props.project.body.startsWith('# Placeholder description'), props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
title: 'Add a description', title: "Add a description",
id: 'add-description', id: "add-description",
description: description:
"A description that clearly describes the project's purpose and function is required.", "A description that clearly describes the project's purpose and function is required.",
status: 'required', status: "required",
link: { link: {
path: 'settings/description', path: "settings/description",
title: 'Visit description settings', title: "Visit description settings",
hide: props.routeName === 'type-id-settings-description', hide: props.routeName === "type-id-settings-description",
}, },
}, },
{ {
condition: !props.project.icon_url, condition: !props.project.icon_url,
title: 'Add an icon', title: "Add an icon",
id: 'add-icon', id: "add-icon",
description: description:
'Your project should have a nice-looking icon to uniquely identify your project at a glance.', "Your project should have a nice-looking icon to uniquely identify your project at a glance.",
status: 'suggestion', status: "suggestion",
link: { link: {
path: 'settings', path: "settings",
title: 'Visit general settings', title: "Visit general settings",
hide: props.routeName === 'type-id-settings', hide: props.routeName === "type-id-settings",
}, },
}, },
{ {
condition: props.project.gallery.length === 0 || !featuredGalleryImage, condition: props.project.gallery.length === 0 || !featuredGalleryImage,
title: 'Feature a gallery image', title: "Feature a gallery image",
id: 'feature-gallery-image', id: "feature-gallery-image",
description: 'Featured gallery images may be the first impression of many users.', description: "Featured gallery images may be the first impression of many users.",
status: 'suggestion', status: "suggestion",
link: { link: {
path: 'gallery', path: "gallery",
title: 'Visit gallery page', title: "Visit gallery page",
hide: props.routeName === 'type-id-gallery', hide: props.routeName === "type-id-gallery",
}, },
}, },
{ {
hide: props.project.versions.length === 0, hide: props.project.versions.length === 0,
condition: props.project.categories.length < 1, condition: props.project.categories.length < 1,
title: 'Select tags', title: "Select tags",
id: 'select-tags', id: "select-tags",
description: 'Select all tags that apply to your project.', description: "Select all tags that apply to your project.",
status: 'suggestion', status: "suggestion",
link: { link: {
path: 'settings/tags', path: "settings/tags",
title: 'Visit tag settings', title: "Visit tag settings",
hide: props.routeName === 'type-id-settings-tags', hide: props.routeName === "type-id-settings-tags",
}, },
}, },
{ {
@ -273,110 +273,110 @@ const nags = computed(() => [
props.project.discord_url || props.project.discord_url ||
props.project.donation_urls.length > 0 props.project.donation_urls.length > 0
), ),
title: 'Add external links', title: "Add external links",
id: 'add-links', id: "add-links",
description: description:
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.', "Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
status: 'suggestion', status: "suggestion",
link: { link: {
path: 'settings/links', path: "settings/links",
title: 'Visit links settings', title: "Visit links settings",
hide: props.routeName === 'type-id-settings-links', hide: props.routeName === "type-id-settings-links",
}, },
}, },
{ {
hide: hide:
props.project.versions.length === 0 || props.project.versions.length === 0 ||
props.project.project_type === 'resourcepack' || props.project.project_type === "resourcepack" ||
props.project.project_type === 'plugin' || props.project.project_type === "plugin" ||
props.project.project_type === 'shader' || props.project.project_type === "shader" ||
props.project.project_type === 'datapack', props.project.project_type === "datapack",
condition: condition:
props.project.client_side === 'unknown' || props.project.client_side === "unknown" ||
props.project.server_side === 'unknown' || props.project.server_side === "unknown" ||
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'), (props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
title: 'Select supported environments', title: "Select supported environments",
id: 'select-environments', id: "select-environments",
description: `Select if the ${formatProjectType( description: `Select if the ${formatProjectType(
props.project.project_type props.project.project_type,
).toLowerCase()} functions on the client-side and/or server-side.`, ).toLowerCase()} functions on the client-side and/or server-side.`,
status: 'required', status: "required",
link: { link: {
path: 'settings', path: "settings",
title: 'Visit general settings', title: "Visit general settings",
hide: props.routeName === 'type-id-settings', hide: props.routeName === "type-id-settings",
}, },
}, },
{ {
condition: props.project.license.id === 'LicenseRef-Unknown', condition: props.project.license.id === "LicenseRef-Unknown",
title: 'Select license', title: "Select license",
id: 'select-license', id: "select-license",
description: `Select the license your ${formatProjectType( description: `Select the license your ${formatProjectType(
props.project.project_type props.project.project_type,
).toLowerCase()} is distributed under.`, ).toLowerCase()} is distributed under.`,
status: 'required', status: "required",
link: { link: {
path: 'settings/license', path: "settings/license",
title: 'Visit license settings', title: "Visit license settings",
hide: props.routeName === 'type-id-settings-license', hide: props.routeName === "type-id-settings-license",
}, },
}, },
{ {
condition: props.project.status === 'draft', condition: props.project.status === "draft",
title: 'Submit for review', title: "Submit for review",
id: 'submit-for-review', id: "submit-for-review",
description: description:
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.', "Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
status: 'review', status: "review",
link: null, link: null,
action: { action: {
onClick: submitForReview, onClick: submitForReview,
title: 'Submit for review', title: "Submit for review",
disabled: () => nags.value.filter((x) => x.condition && x.status === 'required').length > 0, disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
}, },
}, },
{ {
condition: props.tags.rejectedStatuses.includes(props.project.status), condition: props.tags.rejectedStatuses.includes(props.project.status),
title: 'Resubmit for review', title: "Resubmit for review",
id: 'resubmit-for-review', id: "resubmit-for-review",
description: `Your project has been ${props.project.status} by description: `Your project has been ${props.project.status} by
Modrinth's staff. In most cases, you can resubmit for review after Modrinth's staff. In most cases, you can resubmit for review after
addressing the staff's message.`, addressing the staff's message.`,
status: 'review', status: "review",
link: { link: {
path: 'moderation', path: "moderation",
title: 'Visit moderation page', title: "Visit moderation page",
hide: props.routeName === 'type-id-moderation', hide: props.routeName === "type-id-moderation",
}, },
}, },
]) ]);
const showInvitation = computed(() => { const showInvitation = computed(() => {
if (props.allMembers && props.auth) { if (props.allMembers && props.auth) {
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id) const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
return member && !member.accepted return member && !member.accepted;
} }
return false return false;
}) });
const acceptInvite = () => { const acceptInvite = () => {
acceptTeamInvite(props.project.team) acceptTeamInvite(props.project.team);
props.updateMembers() props.updateMembers();
} };
const declineInvite = () => { const declineInvite = () => {
removeTeamMember(props.project.team, props.auth.user.id) removeTeamMember(props.project.team, props.auth.user.id);
props.updateMembers() props.updateMembers();
} };
const submitForReview = async () => { const submitForReview = async () => {
if ( if (
!props.acknowledgedMessage || !props.acknowledgedMessage ||
nags.value.filter((x) => x.condition && x.status === 'required').length === 0 nags.value.filter((x) => x.condition && x.status === "required").length === 0
) { ) {
await props.setProcessing() await props.setProcessing();
}
} }
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -70,10 +70,10 @@
class="iconified-button" class="iconified-button"
@click=" @click="
() => { () => {
selectedLoaders = [] selectedLoaders = [];
selectedGameVersions = [] selectedGameVersions = [];
selectedVersionTypes = [] selectedVersionTypes = [];
updateQuery() updateQuery();
} }
" "
> >
@ -84,51 +84,51 @@
</template> </template>
<script setup> <script setup>
import { Multiselect } from 'vue-multiselect' import { Multiselect } from "vue-multiselect";
import Checkbox from '~/components/ui/Checkbox.vue' import Checkbox from "~/components/ui/Checkbox.vue";
import ClearIcon from '~/assets/images/utils/clear.svg?component' import ClearIcon from "~/assets/images/utils/clear.svg?component";
const props = defineProps({ const props = defineProps({
versions: { versions: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
}) });
const emit = defineEmits(['switch-page']) const emit = defineEmits(["switch-page"]);
const router = useNativeRouter() const router = useNativeRouter();
const route = useNativeRoute() const route = useNativeRoute();
const tags = useTags() const tags = useTags();
const tempLoaders = new Set() const tempLoaders = new Set();
let tempVersions = new Set() let tempVersions = new Set();
const tempReleaseChannels = new Set() const tempReleaseChannels = new Set();
for (const version of props.versions) { for (const version of props.versions) {
for (const loader of version.loaders) { for (const loader of version.loaders) {
tempLoaders.add(loader) tempLoaders.add(loader);
} }
for (const gameVersion of version.game_versions) { for (const gameVersion of version.game_versions) {
tempVersions.add(gameVersion) tempVersions.add(gameVersion);
} }
tempReleaseChannels.add(version.version_type) tempReleaseChannels.add(version.version_type);
} }
tempVersions = Array.from(tempVersions) tempVersions = Array.from(tempVersions);
const loaderFilters = shallowRef(Array.from(tempLoaders)) const loaderFilters = shallowRef(Array.from(tempLoaders));
const gameVersionFilters = shallowRef( const gameVersionFilters = shallowRef(
tags.value.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version)) tags.value.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version)),
) );
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels)) const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels));
const includeSnapshots = ref(route.query.s === 'true') const includeSnapshots = ref(route.query.s === "true");
const selectedGameVersions = shallowRef(getArrayOrString(route.query.g) ?? []) const selectedGameVersions = shallowRef(getArrayOrString(route.query.g) ?? []);
const selectedLoaders = shallowRef(getArrayOrString(route.query.l) ?? []) const selectedLoaders = shallowRef(getArrayOrString(route.query.l) ?? []);
const selectedVersionTypes = shallowRef(getArrayOrString(route.query.c) ?? []) const selectedVersionTypes = shallowRef(getArrayOrString(route.query.c) ?? []);
async function updateQuery() { async function updateQuery() {
await router.replace({ await router.replace({
@ -139,8 +139,8 @@ async function updateQuery() {
c: selectedVersionTypes.value.length === 0 ? undefined : selectedVersionTypes.value, c: selectedVersionTypes.value.length === 0 ? undefined : selectedVersionTypes.value,
s: includeSnapshots.value ? true : undefined, s: includeSnapshots.value ? true : undefined,
}, },
}) });
emit('switch-page', 1) emit("switch-page", 1);
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import dayjs from 'dayjs' import dayjs from "dayjs";
import { formatNumber, formatMoney } from '@modrinth/utils' import { formatNumber, formatMoney } from "@modrinth/utils";
import VueApexCharts from 'vue3-apexcharts' import VueApexCharts from "vue3-apexcharts";
const props = defineProps({ const props = defineProps({
name: { name: {
@ -18,7 +18,7 @@ const props = defineProps({
}, },
formatLabels: { formatLabels: {
type: Function, type: Function,
default: (label) => dayjs(label).format('MMM D'), default: (label) => dayjs(label).format("MMM D"),
}, },
colors: { colors: {
type: Array, type: Array,
@ -26,11 +26,11 @@ const props = defineProps({
}, },
prefix: { prefix: {
type: String, type: String,
default: '', default: "",
}, },
suffix: { suffix: {
type: String, type: String,
default: '', default: "",
}, },
hideToolbar: { hideToolbar: {
type: Boolean, type: Boolean,
@ -46,7 +46,7 @@ const props = defineProps({
}, },
type: { type: {
type: String, type: String,
default: 'bar', default: "bar",
}, },
hideTotal: { hideTotal: {
type: Boolean, type: Boolean,
@ -58,11 +58,11 @@ const props = defineProps({
}, },
legendPosition: { legendPosition: {
type: String, type: String,
default: 'right', default: "right",
}, },
xAxisType: { xAxisType: {
type: String, type: String,
default: 'datetime', default: "datetime",
}, },
percentStacked: { percentStacked: {
type: Boolean, type: Boolean,
@ -76,14 +76,14 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) });
function formatTooltipValue(value, props) { function formatTooltipValue(value, props) {
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false) return props.isMoney ? formatMoney(value, false) : formatNumber(value, false);
} }
function generateListEntry(value, index, _, w, props) { function generateListEntry(value, index, _, w, props) {
const color = w.globals.colors?.[index] const color = w.globals.colors?.[index];
return `<div class="list-entry"> return `<div class="list-entry">
<span class="circle" style="background-color: ${color}"></span> <span class="circle" style="background-color: ${color}"></span>
@ -93,35 +93,35 @@ function generateListEntry(value, index, _, w, props) {
<div class="value"> <div class="value">
${props.prefix}${formatTooltipValue(value, props)}${props.suffix} ${props.prefix}${formatTooltipValue(value, props)}${props.suffix}
</div> </div>
</div>` </div>`;
} }
function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) { function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
const label = w.globals.lastXAxis.categories?.[dataPointIndex] const label = w.globals.lastXAxis.categories?.[dataPointIndex];
const formattedLabel = props.formatLabels(label) const formattedLabel = props.formatLabels(label);
let tooltip = `<div class="bar-tooltip"> let tooltip = `<div class="bar-tooltip">
<div class="seperated-entry title"> <div class="seperated-entry title">
<div class="label">${formattedLabel}</div>` <div class="label">${formattedLabel}</div>`;
// Logic for total and percent stacked // Logic for total and percent stacked
if (!props.hideTotal) { if (!props.hideTotal) {
if (props.percentStacked) { if (props.percentStacked) {
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0) const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total;
tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${ tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${
props.suffix props.suffix
}</div>` }</div>`;
} else { } else {
const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0) const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
tooltip += `<div class="value">${props.prefix}${formatTooltipValue(totalValue, props)}${ tooltip += `<div class="value">${props.prefix}${formatTooltipValue(totalValue, props)}${
props.suffix props.suffix
}</div>` }</div>`;
} }
} }
tooltip += '</div><hr class="card-divider" />' tooltip += '</div><hr class="card-divider" />';
// Logic for generating list entries // Logic for generating list entries
if (props.percentStacked) { if (props.percentStacked) {
@ -130,10 +130,10 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
seriesIndex, seriesIndex,
seriesIndex, seriesIndex,
w, w,
props props,
) );
} else { } else {
const returnTopN = 5 const returnTopN = 5;
const listEntries = series const listEntries = series
.map((value, index) => [ .map((value, index) => [
@ -144,13 +144,13 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
.sort((a, b) => b[0] - a[0]) .sort((a, b) => b[0] - a[0])
.slice(0, returnTopN) // Return only the top X entries .slice(0, returnTopN) // Return only the top X entries
.map((value) => value[1]) .map((value) => value[1])
.join('') .join("");
tooltip += listEntries tooltip += listEntries;
} }
tooltip += '</div>' tooltip += "</div>";
return tooltip return tooltip;
} }
const chartOptions = computed(() => { const chartOptions = computed(() => {
@ -158,19 +158,19 @@ const chartOptions = computed(() => {
chart: { chart: {
id: props.name, id: props.name,
fontFamily: fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif', "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
foreColor: 'var(--color-base)', foreColor: "var(--color-base)",
selection: { selection: {
enabled: true, enabled: true,
fill: { fill: {
color: 'var(--color-brand)', color: "var(--color-brand)",
}, },
}, },
toolbar: { toolbar: {
show: false, show: false,
}, },
stacked: props.stacked, stacked: props.stacked,
stackType: props.percentStacked ? '100%' : 'normal', stackType: props.percentStacked ? "100%" : "normal",
zoom: { zoom: {
autoScaleYaxis: true, autoScaleYaxis: true,
}, },
@ -183,7 +183,7 @@ const chartOptions = computed(() => {
categories: props.labels, categories: props.labels,
labels: { labels: {
style: { style: {
borderRadius: 'var(--radius-sm)', borderRadius: "var(--radius-sm)",
}, },
}, },
axisTicks: { axisTicks: {
@ -207,8 +207,8 @@ const chartOptions = computed(() => {
}, },
}, },
grid: { grid: {
borderColor: 'var(--color-button-bg)', borderColor: "var(--color-button-bg)",
tickColor: 'var(--color-button-bg)', tickColor: "var(--color-button-bg)",
}, },
legend: { legend: {
show: !props.hideLegend, show: !props.hideLegend,
@ -216,16 +216,16 @@ const chartOptions = computed(() => {
showForZeroSeries: false, showForZeroSeries: false,
showForSingleSeries: false, showForSingleSeries: false,
showForNullSeries: false, showForNullSeries: false,
fontSize: 'var(--font-size-nm)', fontSize: "var(--font-size-nm)",
fontFamily: fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif', "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
onItemClick: { onItemClick: {
toggleDataSeries: true, toggleDataSeries: true,
}, },
}, },
markers: { markers: {
size: 0, size: 0,
strokeColor: 'var(--color-contrast)', strokeColor: "var(--color-contrast)",
strokeWidth: 3, strokeWidth: 3,
strokeOpacity: 1, strokeOpacity: 1,
fillOpacity: 1, fillOpacity: 1,
@ -236,29 +236,29 @@ const chartOptions = computed(() => {
plotOptions: { plotOptions: {
bar: { bar: {
horizontal: props.horizontalBar, horizontal: props.horizontalBar,
columnWidth: '80%', columnWidth: "80%",
endingShape: 'rounded', endingShape: "rounded",
borderRadius: 5, borderRadius: 5,
borderRadiusApplication: 'end', borderRadiusApplication: "end",
borderRadiusWhenStacked: 'last', borderRadiusWhenStacked: "last",
}, },
}, },
stroke: { stroke: {
curve: 'smooth', curve: "smooth",
width: 2, width: 2,
}, },
tooltip: { tooltip: {
custom: (d) => generateTooltip(d, props), custom: (d) => generateTooltip(d, props),
}, },
fill: fill:
props.type === 'area' props.type === "area"
? { ? {
colors: props.colors, colors: props.colors,
type: 'gradient', type: "gradient",
opacity: 1, opacity: 1,
gradient: { gradient: {
shade: 'light', shade: "light",
type: 'vertical', type: "vertical",
shadeIntensity: 0, shadeIntensity: 0,
gradientToColors: props.colors, gradientToColors: props.colors,
inverseColors: true, inverseColors: true,
@ -269,40 +269,40 @@ const chartOptions = computed(() => {
}, },
} }
: {}, : {},
} };
}) });
const chart = ref(null) const chart = ref(null);
const legendValues = ref( const legendValues = ref(
[...props.data].map((project, index) => { [...props.data].map((project, index) => {
return { name: project.name, visible: true, color: props.colors[index] } return { name: project.name, visible: true, color: props.colors[index] };
}) }),
) );
const flipLegend = (legend, newVal) => { const flipLegend = (legend, newVal) => {
legend.visible = newVal legend.visible = newVal;
chart.value.toggleSeries(legend.name) chart.value.toggleSeries(legend.name);
} };
const resetChart = () => { const resetChart = () => {
if (!chart.value) return if (!chart.value) return;
chart.value.updateSeries([...props.data]) chart.value.updateSeries([...props.data]);
chart.value.updateOptions({ chart.value.updateOptions({
xaxis: { xaxis: {
categories: props.labels, categories: props.labels,
}, },
}) });
chart.value.resetSeries() chart.value.resetSeries();
legendValues.value.forEach((legend) => { legendValues.value.forEach((legend) => {
legend.visible = true legend.visible = true;
}) });
} };
defineExpose({ defineExpose({
resetChart, resetChart,
flipLegend, flipLegend,
}) });
</script> </script>
<template> <template>

View File

@ -86,7 +86,9 @@
v-model="selectedRange" v-model="selectedRange"
:options="selectableRanges" :options="selectableRanges"
name="Time range" name="Time range"
:display-name="(o: typeof selectableRanges[number] | undefined) => o?.label || 'Custom'" :display-name="
(o: (typeof selectableRanges)[number] | undefined) => o?.label || 'Custom'
"
/> />
</div> </div>
</div> </div>
@ -218,7 +220,7 @@
:style="{ :style="{
width: formatPercent( width: formatPercent(
count, count,
analytics.formattedData.value.downloadsByCountry.sum analytics.formattedData.value.downloadsByCountry.sum,
), ),
backgroundColor: 'var(--color-brand)', backgroundColor: 'var(--color-brand)',
}" }"
@ -266,7 +268,7 @@
v-tooltip=" v-tooltip="
`${ `${
Math.round( Math.round(
(count / analytics.formattedData.value.viewsByCountry.sum) * 10000 (count / analytics.formattedData.value.viewsByCountry.sum) * 10000,
) / 100 ) / 100
}%` }%`
" "
@ -289,56 +291,56 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button, Card, DropdownSelect } from '@modrinth/ui' import { Button, Card, DropdownSelect } from "@modrinth/ui";
import { formatMoney, formatNumber, formatCategoryHeader } from '@modrinth/utils' import { formatMoney, formatNumber, formatCategoryHeader } from "@modrinth/utils";
import { UpdatedIcon, DownloadIcon } from '@modrinth/assets' import { UpdatedIcon, DownloadIcon } from "@modrinth/assets";
import dayjs from 'dayjs' import dayjs from "dayjs";
import { computed } from 'vue' import { computed } from "vue";
import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js' import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js";
import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from '#components' import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from "#components";
import PaletteIcon from '~/assets/icons/palette.svg?component' import PaletteIcon from "~/assets/icons/palette.svg?component";
const router = useNativeRouter() const router = useNativeRouter();
const theme = useTheme() const theme = useTheme();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
projects?: any[] projects?: any[];
/** /**
* @deprecated Use `ranges` instead * @deprecated Use `ranges` instead
*/ */
resoloutions?: Record<string, number> resoloutions?: Record<string, number>;
ranges?: Record<number, [string, number] | string> ranges?: Record<number, [string, number] | string>;
personal?: boolean personal?: boolean;
}>(), }>(),
{ {
projects: undefined, projects: undefined,
resoloutions: () => defaultResoloutions, resoloutions: () => defaultResoloutions,
ranges: () => defaultRanges, ranges: () => defaultRanges,
personal: false, personal: false,
} },
) );
const projects = ref(props.projects || []) const projects = ref(props.projects || []);
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({ const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
label: typeof extra === 'string' ? extra : extra[0], label: typeof extra === "string" ? extra : extra[0],
value: Number(duration), value: Number(duration),
res: typeof extra === 'string' ? Number(duration) : extra[1], res: typeof extra === "string" ? Number(duration) : extra[1],
})) }));
// const selectedChart = ref('downloads') // const selectedChart = ref('downloads')
const selectedChart = computed({ const selectedChart = computed({
get: () => { get: () => {
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads' const id = (router.currentRoute.value.query?.chart as string | undefined) || "downloads";
// if the id is anything but the 3 charts we have or undefined, throw an error // if the id is anything but the 3 charts we have or undefined, throw an error
if (!['downloads', 'views', 'revenue'].includes(id)) { if (!["downloads", "views", "revenue"].includes(id)) {
throw new Error(`Unknown chart ${id}`) throw new Error(`Unknown chart ${id}`);
} }
return id return id;
}, },
set: (chart) => { set: (chart) => {
router.push({ router.push({
@ -346,153 +348,153 @@ const selectedChart = computed({
...router.currentRoute.value.query, ...router.currentRoute.value.query,
chart, chart,
}, },
}) });
}, },
}) });
// Chart refs // Chart refs
const downloadsChart = ref() const downloadsChart = ref();
const viewsChart = ref() const viewsChart = ref();
const revenueChart = ref() const revenueChart = ref();
const tinyDownloadChart = ref() const tinyDownloadChart = ref();
const tinyViewChart = ref() const tinyViewChart = ref();
const tinyRevenueChart = ref() const tinyRevenueChart = ref();
const selectedDisplayProjects = ref(props.projects || []) const selectedDisplayProjects = ref(props.projects || []);
const removeProjectFromDisplay = (id: string) => { const removeProjectFromDisplay = (id: string) => {
selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id) selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id);
} };
const addProjectToDisplay = (id: string) => { const addProjectToDisplay = (id: string) => {
selectedDisplayProjects.value = [ selectedDisplayProjects.value = [
...selectedDisplayProjects.value, ...selectedDisplayProjects.value,
props.projects?.find((p) => p.id === id), props.projects?.find((p) => p.id === id),
].filter(Boolean) ].filter(Boolean);
} };
const projectIsOnDisplay = (id: string) => { const projectIsOnDisplay = (id: string) => {
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false;
} };
const resetCharts = () => { const resetCharts = () => {
downloadsChart.value?.resetChart() downloadsChart.value?.resetChart();
viewsChart.value?.resetChart() viewsChart.value?.resetChart();
revenueChart.value?.resetChart() revenueChart.value?.resetChart();
tinyDownloadChart.value?.resetChart() tinyDownloadChart.value?.resetChart();
tinyViewChart.value?.resetChart() tinyViewChart.value?.resetChart();
tinyRevenueChart.value?.resetChart() tinyRevenueChart.value?.resetChart();
} };
const isUsingProjectColors = computed({ const isUsingProjectColors = computed({
get: () => { get: () => {
return ( return (
router.currentRoute.value.query?.colors === 'true' || router.currentRoute.value.query?.colors === "true" ||
router.currentRoute.value.query?.colors === undefined router.currentRoute.value.query?.colors === undefined
) );
}, },
set: (newValue) => { set: (newValue) => {
router.push({ router.push({
query: { query: {
...router.currentRoute.value.query, ...router.currentRoute.value.query,
colors: newValue ? 'true' : 'false', colors: newValue ? "true" : "false",
}, },
}) });
}, },
}) });
const analytics = useFetchAllAnalytics( const analytics = useFetchAllAnalytics(
resetCharts, resetCharts,
projects, projects,
selectedDisplayProjects, selectedDisplayProjects,
props.personal props.personal,
) );
const { startDate, endDate, timeRange, timeResolution } = analytics const { startDate, endDate, timeRange, timeResolution } = analytics;
const selectedRange = computed({ const selectedRange = computed({
get: () => { get: () => {
return ( return (
selectableRanges.find((option) => option.value === timeRange.value) || { selectableRanges.find((option) => option.value === timeRange.value) || {
label: 'Custom', label: "Custom",
value: timeRange.value, value: timeRange.value,
} }
) );
}, },
set: (newRange: { label: string; value: number; res?: number }) => { set: (newRange: { label: string; value: number; res?: number }) => {
timeRange.value = newRange.value timeRange.value = newRange.value;
startDate.value = Date.now() - timeRange.value * 60 * 1000 startDate.value = Date.now() - timeRange.value * 60 * 1000;
endDate.value = Date.now() endDate.value = Date.now();
if (newRange?.res) { if (newRange?.res) {
timeResolution.value = newRange.res timeResolution.value = newRange.res;
} }
}, },
}) });
const selectedDataSet = computed(() => { const selectedDataSet = computed(() => {
switch (selectedChart.value) { switch (selectedChart.value) {
case 'downloads': case "downloads":
return analytics.totalData.value.downloads return analytics.totalData.value.downloads;
case 'views': case "views":
return analytics.totalData.value.views return analytics.totalData.value.views;
case 'revenue': case "revenue":
return analytics.totalData.value.revenue return analytics.totalData.value.revenue;
default: default:
throw new Error(`Unknown chart ${selectedChart.value}`) throw new Error(`Unknown chart ${selectedChart.value}`);
} }
}) });
const selectedDataSetProjects = computed(() => { const selectedDataSetProjects = computed(() => {
return selectedDataSet.value.projectIds return selectedDataSet.value.projectIds
.map((id) => props.projects?.find((p) => p?.id === id)) .map((id) => props.projects?.find((p) => p?.id === id))
.filter(Boolean) .filter(Boolean);
}) });
const downloadSelectedSetAsCSV = () => { const downloadSelectedSetAsCSV = () => {
const selectedChartName = selectedChart.value const selectedChartName = selectedChart.value;
const csv = analyticsSetToCSVString(selectedDataSet.value) const csv = analyticsSetToCSVString(selectedDataSet.value);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const link = document.createElement('a') const link = document.createElement("a");
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob);
link.setAttribute('href', url) link.setAttribute("href", url);
link.setAttribute('download', `${selectedChartName}-data.csv`) link.setAttribute("download", `${selectedChartName}-data.csv`);
link.style.visibility = 'hidden' link.style.visibility = "hidden";
document.body.appendChild(link) document.body.appendChild(link);
link.click() link.click();
} };
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV()) const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV());
const onToggleColors = () => { const onToggleColors = () => {
isUsingProjectColors.value = !isUsingProjectColors.value isUsingProjectColors.value = !isUsingProjectColors.value;
} };
</script> </script>
<script lang="ts"> <script lang="ts">
const defaultResoloutions: Record<string, number> = { const defaultResoloutions: Record<string, number> = {
'5 minutes': 5, "5 minutes": 5,
'30 minutes': 30, "30 minutes": 30,
'An hour': 60, "An hour": 60,
'12 hours': 720, "12 hours": 720,
'A day': 1440, "A day": 1440,
'A week': 10080, "A week": 10080,
} };
const defaultRanges: Record<number, [string, number] | string> = { const defaultRanges: Record<number, [string, number] | string> = {
30: ['Last 30 minutes', 1], 30: ["Last 30 minutes", 1],
60: ['Last hour', 5], 60: ["Last hour", 5],
720: ['Last 12 hours', 15], 720: ["Last 12 hours", 15],
1440: ['Last day', 60], 1440: ["Last day", 60],
10080: ['Last week', 720], 10080: ["Last week", 720],
43200: ['Last month', 1440], 43200: ["Last month", 1440],
129600: ['Last quarter', 10080], 129600: ["Last quarter", 10080],
525600: ['Last year', 20160], 525600: ["Last year", 20160],
1051200: ['Last two years', 40320], 1051200: ["Last two years", 40320],
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -590,7 +592,9 @@ const defaultRanges: Record<number, [string, number] | string> = {
.chart-button-base__selected { .chart-button-base__selected {
color: var(--color-contrast); color: var(--color-contrast);
background-color: var(--color-brand-highlight); 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);
&:hover { &:hover {
background-color: var(--color-brand-highlight); background-color: var(--color-brand-highlight);
@ -662,7 +666,7 @@ const defaultRanges: Record<number, [string, number] | string> = {
.country-value { .country-value {
display: grid; display: grid;
grid-template-areas: 'flag text bar'; grid-template-areas: "flag text bar";
grid-template-columns: auto 1fr 10rem; grid-template-columns: auto 1fr 10rem;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { Card } from '@modrinth/ui' import { Card } from "@modrinth/ui";
import VueApexCharts from 'vue3-apexcharts' import VueApexCharts from "vue3-apexcharts";
// let VueApexCharts // let VueApexCharts
// if (process.client) { // if (process.client) {
@ -10,11 +10,11 @@ import VueApexCharts from 'vue3-apexcharts'
const props = defineProps({ const props = defineProps({
value: { value: {
type: String, type: String,
default: '', default: "",
}, },
title: { title: {
type: String, type: String,
default: '', default: "",
}, },
data: { data: {
type: Array, type: Array,
@ -26,11 +26,11 @@ const props = defineProps({
}, },
prefix: { prefix: {
type: String, type: String,
default: '', default: "",
}, },
suffix: { suffix: {
type: String, type: String,
default: '', default: "",
}, },
isMoney: { isMoney: {
type: Boolean, type: Boolean,
@ -38,17 +38,17 @@ const props = defineProps({
}, },
color: { color: {
type: String, type: String,
default: 'var(--color-brand)', default: "var(--color-brand)",
}, },
}) });
// no grid lines, no toolbar, no legend, no data labels // no grid lines, no toolbar, no legend, no data labels
const chartOptions = { const chartOptions = {
chart: { chart: {
id: props.title, id: props.title,
fontFamily: fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif', "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
foreColor: 'var(--color-base)', foreColor: "var(--color-base)",
toolbar: { toolbar: {
show: false, show: false,
}, },
@ -61,16 +61,16 @@ const chartOptions = {
parentHeightOffset: 0, parentHeightOffset: 0,
}, },
stroke: { stroke: {
curve: 'smooth', curve: "smooth",
width: 2, width: 2,
}, },
fill: { fill: {
colors: [props.color], colors: [props.color],
type: 'gradient', type: "gradient",
opacity: 1, opacity: 1,
gradient: { gradient: {
shade: 'light', shade: "light",
type: 'vertical', type: "vertical",
shadeIntensity: 0, shadeIntensity: 0,
gradientToColors: [props.color], gradientToColors: [props.color],
inverseColors: true, inverseColors: true,
@ -91,7 +91,7 @@ const chartOptions = {
enabled: false, enabled: false,
}, },
xaxis: { xaxis: {
type: 'datetime', type: "datetime",
categories: props.labels, categories: props.labels,
labels: { labels: {
show: false, show: false,
@ -120,23 +120,23 @@ const chartOptions = {
tooltip: { tooltip: {
enabled: false, enabled: false,
}, },
} };
const chart = ref(null) const chart = ref(null);
const resetChart = () => { const resetChart = () => {
chart.value?.updateSeries([...props.data]) chart.value?.updateSeries([...props.data]);
chart.value?.updateOptions({ chart.value?.updateOptions({
xaxis: { xaxis: {
categories: props.labels, categories: props.labels,
}, },
}) });
chart.value?.resetSeries() chart.value?.resetSeries();
} };
defineExpose({ defineExpose({
resetChart, resetChart,
}) });
</script> </script>
<template> <template>

View File

@ -1,8 +1,8 @@
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue' import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
import { startLoading, stopLoading, useNuxtApp } from '#imports' import { startLoading, stopLoading, useNuxtApp } from "#imports";
export default defineComponent({ export default defineComponent({
name: 'ModrinthLoadingIndicator', name: "ModrinthLoadingIndicator",
props: { props: {
throttle: { throttle: {
type: Number, type: Number,
@ -19,115 +19,115 @@ export default defineComponent({
color: { color: {
type: [String, Boolean], type: [String, Boolean],
default: default:
'repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)', "repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)",
}, },
}, },
setup(props, { slots }) { setup(props, { slots }) {
const indicator = useLoadingIndicator({ const indicator = useLoadingIndicator({
duration: props.duration, duration: props.duration,
throttle: props.throttle, throttle: props.throttle,
}) });
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp();
nuxtApp.hook('page:start', () => { nuxtApp.hook("page:start", () => {
startLoading() startLoading();
indicator.start() indicator.start();
}) });
nuxtApp.hook('page:finish', () => { nuxtApp.hook("page:finish", () => {
stopLoading() stopLoading();
indicator.finish() indicator.finish();
}) });
onBeforeUnmount(() => indicator.clear) onBeforeUnmount(() => indicator.clear);
const loading = useLoading() const loading = useLoading();
watch(loading, (newValue) => { watch(loading, (newValue) => {
if (newValue) { if (newValue) {
indicator.start() indicator.start();
} else { } else {
indicator.finish() indicator.finish();
} }
}) });
return () => return () =>
h( h(
'div', "div",
{ {
class: 'nuxt-loading-indicator', class: "nuxt-loading-indicator",
style: { style: {
position: 'fixed', position: "fixed",
top: 0, top: 0,
right: 0, right: 0,
left: 0, left: 0,
pointerEvents: 'none', pointerEvents: "none",
width: `${indicator.progress.value}%`, width: `${indicator.progress.value}%`,
height: `${props.height}px`, height: `${props.height}px`,
opacity: indicator.isLoading.value ? 1 : 0, opacity: indicator.isLoading.value ? 1 : 0,
background: props.color || undefined, background: props.color || undefined,
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`, backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
transition: 'width 0.1s, height 0.4s, opacity 0.4s', transition: "width 0.1s, height 0.4s, opacity 0.4s",
zIndex: 999999, zIndex: 999999,
}, },
}, },
slots slots,
) );
}, },
}) });
function useLoadingIndicator(opts: { duration: number; throttle: number }) { function useLoadingIndicator(opts: { duration: number; throttle: number }) {
const progress = ref(0) const progress = ref(0);
const isLoading = ref(false) const isLoading = ref(false);
const step = computed(() => 10000 / opts.duration) const step = computed(() => 10000 / opts.duration);
let _timer: any = null let _timer: any = null;
let _throttle: any = null let _throttle: any = null;
function start() { function start() {
clear() clear();
progress.value = 0 progress.value = 0;
if (opts.throttle && process.client) { if (opts.throttle && process.client) {
_throttle = setTimeout(() => { _throttle = setTimeout(() => {
isLoading.value = true isLoading.value = true;
_startTimer() _startTimer();
}, opts.throttle) }, opts.throttle);
} else { } else {
isLoading.value = true isLoading.value = true;
_startTimer() _startTimer();
} }
} }
function finish() { function finish() {
progress.value = 100 progress.value = 100;
_hide() _hide();
} }
function clear() { function clear() {
clearInterval(_timer) clearInterval(_timer);
clearTimeout(_throttle) clearTimeout(_throttle);
_timer = null _timer = null;
_throttle = null _throttle = null;
} }
function _increase(num: number) { function _increase(num: number) {
progress.value = Math.min(100, progress.value + num) progress.value = Math.min(100, progress.value + num);
} }
function _hide() { function _hide() {
clear() clear();
if (process.client) { if (process.client) {
setTimeout(() => { setTimeout(() => {
isLoading.value = false isLoading.value = false;
setTimeout(() => { setTimeout(() => {
progress.value = 0 progress.value = 0;
}, 400) }, 400);
}, 500) }, 500);
} }
} }
function _startTimer() { function _startTimer() {
if (process.client) { if (process.client) {
_timer = setInterval(() => { _timer = setInterval(() => {
_increase(step.value) _increase(step.value);
}, 100) }, 100);
} }
} }
@ -137,5 +137,5 @@ function useLoadingIndicator(opts: { duration: number; throttle: number }) {
start, start,
finish, finish,
clear, clear,
} };
} }

View File

@ -12,7 +12,7 @@
<span class="title">{{ report.project.title }}</span> <span class="title">{{ report.project.title }}</span>
<span>{{ <span>{{
$formatProjectType( $formatProjectType(
$getProjectTypeForUrl(report.project.project_type, report.project.loaders) $getProjectTypeForUrl(report.project.project_type, report.project.loaders),
) )
}}</span> }}</span>
</div> </div>
@ -42,7 +42,7 @@
<span class="title">{{ report.project.title }}</span> <span class="title">{{ report.project.title }}</span>
<span>{{ <span>{{
$formatProjectType( $formatProjectType(
$getProjectTypeForUrl(report.project.project_type, report.project.loaders) $getProjectTypeForUrl(report.project.project_type, report.project.loaders),
) )
}}</span> }}</span>
</div> </div>
@ -88,14 +88,14 @@
</template> </template>
<script setup> <script setup>
import { renderHighlightedString } from '~/helpers/highlight.js' import { renderHighlightedString } from "~/helpers/highlight.js";
import Avatar from '~/components/ui/Avatar.vue' import Avatar from "~/components/ui/Avatar.vue";
import Badge from '~/components/ui/Badge.vue' import Badge from "~/components/ui/Badge.vue";
import ReportIcon from '~/assets/images/utils/report.svg?component' import ReportIcon from "~/assets/images/utils/report.svg?component";
import UnknownIcon from '~/assets/images/utils/unknown.svg?component' import UnknownIcon from "~/assets/images/utils/unknown.svg?component";
import VersionIcon from '~/assets/images/utils/version.svg?component' import VersionIcon from "~/assets/images/utils/version.svg?component";
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue' import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import CopyCode from '~/components/ui/CopyCode.vue' import CopyCode from "~/components/ui/CopyCode.vue";
defineProps({ defineProps({
report: { report: {
@ -122,9 +122,9 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const flags = useFeatureFlags() const flags = useFeatureFlags();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -21,10 +21,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue' import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import ConversationThread from '~/components/ui/thread/ConversationThread.vue' import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import ReportInfo from '~/components/ui/report/ReportInfo.vue' import ReportInfo from "~/components/ui/report/ReportInfo.vue";
import { addReportMessage } from '~/helpers/threads.js' import { addReportMessage } from "~/helpers/threads.js";
const props = defineProps({ const props = defineProps({
reportId: { reportId: {
@ -39,76 +39,76 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const report = ref(null) const report = ref(null);
await fetchReport().then((result) => { await fetchReport().then((result) => {
report.value = result report.value = result;
}) });
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () => const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
useBaseFetch(`thread/${report.value.thread_id}`) useBaseFetch(`thread/${report.value.thread_id}`),
) );
const thread = computed(() => addReportMessage(rawThread.value, report.value)) const thread = computed(() => addReportMessage(rawThread.value, report.value));
async function updateThread(newThread) { async function updateThread(newThread) {
rawThread.value = newThread rawThread.value = newThread;
report.value = await fetchReport() report.value = await fetchReport();
} }
async function fetchReport() { async function fetchReport() {
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () => const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
useBaseFetch(`report/${props.reportId}`) useBaseFetch(`report/${props.reportId}`),
) );
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '') rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, "");
const userIds = [] const userIds = [];
userIds.push(rawReport.value.reporter) userIds.push(rawReport.value.reporter);
if (rawReport.value.item_type === 'user') { if (rawReport.value.item_type === "user") {
userIds.push(rawReport.value.item_id) userIds.push(rawReport.value.item_id);
} }
const versionId = rawReport.value.item_type === 'version' ? rawReport.value.item_id : null const versionId = rawReport.value.item_type === "version" ? rawReport.value.item_id : null;
let users = [] let users = [];
if (userIds.length > 0) { if (userIds.length > 0) {
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () => const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`) useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
) );
users = usersVal.value users = usersVal.value;
} }
let version = null let version = null;
if (versionId) { if (versionId) {
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () => const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
useBaseFetch(`version/${versionId}`) useBaseFetch(`version/${versionId}`),
) );
version = versionVal.value version = versionVal.value;
} }
const projectId = version const projectId = version
? version.project_id ? version.project_id
: rawReport.value.item_type === 'project' : rawReport.value.item_type === "project"
? rawReport.value.item_id ? rawReport.value.item_id
: null : null;
let project = null let project = null;
if (projectId) { if (projectId) {
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () => const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
useBaseFetch(`project/${projectId}`) useBaseFetch(`project/${projectId}`),
) );
project = projectVal.value project = projectVal.value;
} }
const reportData = rawReport.value const reportData = rawReport.value;
reportData.project = project reportData.project = project;
reportData.version = version reportData.version = version;
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter) reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter);
if (rawReport.value.item_type === 'user') { if (rawReport.value.item_type === "user") {
reportData.user = users.find((user) => user.id === rawReport.value.item_id) reportData.user = users.find((user) => user.id === rawReport.value.item_id);
} }
return reportData return reportData;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
v-for="report in reports.filter( v-for="report in reports.filter(
(x) => (x) =>
(moderation || x.reporterUser.id === auth.user.id) && (moderation || x.reporterUser.id === auth.user.id) &&
(viewMode === 'open' ? x.open : !x.open) (viewMode === 'open' ? x.open : !x.open),
)" )"
:key="report.id" :key="report.id"
:report="report" :report="report"
@ -17,9 +17,9 @@
<p v-if="reports.length === 0">You don't have any active reports.</p> <p v-if="reports.length === 0">You don't have any active reports.</p>
</template> </template>
<script setup> <script setup>
import Chips from '~/components/ui/Chips.vue' import Chips from "~/components/ui/Chips.vue";
import ReportInfo from '~/components/ui/report/ReportInfo.vue' import ReportInfo from "~/components/ui/report/ReportInfo.vue";
import { addReportMessage } from '~/helpers/threads.js' import { addReportMessage } from "~/helpers/threads.js";
defineProps({ defineProps({
moderation: { moderation: {
@ -30,68 +30,68 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const viewMode = ref('open') const viewMode = ref("open");
const reports = ref([]) const reports = ref([]);
let { data: rawReports } = await useAsyncData('report', () => useBaseFetch('report')) let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report"));
rawReports = rawReports.value.map((report) => { rawReports = rawReports.value.map((report) => {
report.item_id = report.item_id.replace(/"/g, '') report.item_id = report.item_id.replace(/"/g, "");
return report return report;
}) });
const reporterUsers = rawReports.map((report) => report.reporter) const reporterUsers = rawReports.map((report) => report.reporter);
const reportedUsers = rawReports const reportedUsers = rawReports
.filter((report) => report.item_type === 'user') .filter((report) => report.item_type === "user")
.map((report) => report.item_id) .map((report) => report.item_id);
const versionReports = rawReports.filter((report) => report.item_type === 'version') const versionReports = rawReports.filter((report) => report.item_type === "version");
const versionIds = [...new Set(versionReports.map((report) => report.item_id))] const versionIds = [...new Set(versionReports.map((report) => report.item_id))];
const userIds = [...new Set(reporterUsers.concat(reportedUsers))] const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
const threadIds = [ const threadIds = [
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)), ...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
] ];
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([ const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () => await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`) useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
), ),
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () => await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`) useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
), ),
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () => await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`) useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`),
), ),
]) ]);
const reportedProjects = rawReports const reportedProjects = rawReports
.filter((report) => report.item_type === 'project') .filter((report) => report.item_type === "project")
.map((report) => report.item_id) .map((report) => report.item_id);
const versionProjects = versions.value.map((version) => version.project_id) const versionProjects = versions.value.map((version) => version.project_id);
const projectIds = [...new Set(reportedProjects.concat(versionProjects))] const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () => const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`) useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`),
) );
reports.value = rawReports.map((report) => { reports.value = rawReports.map((report) => {
report.reporterUser = users.value.find((user) => user.id === report.reporter) report.reporterUser = users.value.find((user) => user.id === report.reporter);
if (report.item_type === 'user') { if (report.item_type === "user") {
report.user = users.value.find((user) => user.id === report.item_id) report.user = users.value.find((user) => user.id === report.item_id);
} else if (report.item_type === 'project') { } else if (report.item_type === "project") {
report.project = projects.value.find((project) => project.id === report.item_id) report.project = projects.value.find((project) => project.id === report.item_id);
} else if (report.item_type === 'version') { } else if (report.item_type === "version") {
report.version = versions.value.find((version) => version.id === report.item_id) report.version = versions.value.find((version) => version.id === report.item_id);
report.project = projects.value.find((project) => project.id === report.version.project_id) report.project = projects.value.find((project) => project.id === report.version.project_id);
} }
if (report.thread_id) { if (report.thread_id) {
report.thread = addReportMessage( report.thread = addReportMessage(
threads.value.find((thread) => report.thread_id === thread.id), threads.value.find((thread) => report.thread_id === thread.id),
report report,
) );
} }
report.open = true report.open = true;
return report return report;
}) });
</script> </script>

View File

@ -15,7 +15,7 @@ export default {
categories: { categories: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
type: { type: {
@ -24,9 +24,9 @@ export default {
}, },
}, },
setup() { setup() {
const tags = useTags() const tags = useTags();
return { tags } return { tags };
}, },
computed: { computed: {
categoriesFiltered() { categoriesFiltered() {
@ -34,11 +34,11 @@ export default {
.concat(this.tags.loaders) .concat(this.tags.loaders)
.filter( .filter(
(x) => (x) =>
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type) this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type),
) );
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -16,7 +16,7 @@
</template> </template>
<script> <script>
import Checkbox from '~/components/ui/Checkbox.vue' import Checkbox from "~/components/ui/Checkbox.vue";
export default { export default {
components: { components: {
@ -25,30 +25,30 @@ export default {
props: { props: {
facetName: { facetName: {
type: String, type: String,
default: '', default: "",
}, },
displayName: { displayName: {
type: String, type: String,
default: '', default: "",
}, },
icon: { icon: {
type: String, type: String,
default: '', default: "",
}, },
activeFilters: { activeFilters: {
type: Array, type: Array,
default() { default() {
return [] return [];
}, },
}, },
}, },
emits: ['toggle'], emits: ["toggle"],
methods: { methods: {
toggle() { toggle() {
this.$emit('toggle', this.facetName) this.$emit("toggle", this.facetName);
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -163,7 +163,7 @@
id: 'withhold-reply', id: 'withhold-reply',
color: 'danger', color: 'danger',
action: () => { action: () => {
sendReply('withheld') sendReply('withheld');
}, },
hoverFilled: true, hoverFilled: true,
disabled: project.status === 'withheld', disabled: project.status === 'withheld',
@ -174,7 +174,7 @@
id: 'withhold', id: 'withhold',
color: 'danger', color: 'danger',
action: () => { action: () => {
setStatus('withheld') setStatus('withheld');
}, },
hoverFilled: true, hoverFilled: true,
disabled: project.status === 'withheld', disabled: project.status === 'withheld',
@ -196,22 +196,22 @@
</template> </template>
<script setup> <script setup>
import { OverflowMenu, MarkdownEditor } from '@modrinth/ui' import { OverflowMenu, MarkdownEditor } from "@modrinth/ui";
import { DropdownIcon } from '@modrinth/assets' import { DropdownIcon } from "@modrinth/assets";
import { useImageUpload } from '~/composables/image-upload.ts' import { useImageUpload } from "~/composables/image-upload.ts";
import CopyCode from '~/components/ui/CopyCode.vue' import CopyCode from "~/components/ui/CopyCode.vue";
import ReplyIcon from '~/assets/images/utils/reply.svg?component' import ReplyIcon from "~/assets/images/utils/reply.svg?component";
import SendIcon from '~/assets/images/utils/send.svg?component' import SendIcon from "~/assets/images/utils/send.svg?component";
import CloseIcon from '~/assets/images/utils/check-circle.svg?component' import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?component' import EyeOffIcon from "~/assets/images/utils/eye-off.svg?component";
import CheckIcon from '~/assets/images/utils/check.svg?component' import CheckIcon from "~/assets/images/utils/check.svg?component";
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component' import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue' import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
import { isStaff } from '~/helpers/users.js' import { isStaff } from "~/helpers/users.js";
import { isApproved, isRejected } from '~/helpers/projects.js' import { isApproved, isRejected } from "~/helpers/projects.js";
import Modal from '~/components/ui/Modal.vue' import Modal from "~/components/ui/Modal.vue";
import Checkbox from '~/components/ui/Checkbox.vue' import Checkbox from "~/components/ui/Checkbox.vue";
const props = defineProps({ const props = defineProps({
thread: { thread: {
@ -236,166 +236,166 @@ const props = defineProps({
currentMember: { currentMember: {
type: Object, type: Object,
default() { default() {
return null return null;
}, },
}, },
auth: { auth: {
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const emit = defineEmits(['update-thread']) const emit = defineEmits(["update-thread"]);
const app = useNuxtApp() const app = useNuxtApp();
const flags = useFeatureFlags() const flags = useFeatureFlags();
const members = computed(() => { const members = computed(() => {
const members = {} const members = {};
for (const member of props.thread.members) { for (const member of props.thread.members) {
members[member.id] = member members[member.id] = member;
} }
return members return members;
}) });
const replyBody = ref('') const replyBody = ref("");
const sortedMessages = computed(() => { const sortedMessages = computed(() => {
if (props.thread !== null) { if (props.thread !== null) {
return props.thread.messages return props.thread.messages
.slice() .slice()
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created)) .sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created));
} }
return [] return [];
}) });
const modalSubmit = ref(null) const modalSubmit = ref(null);
async function updateThreadLocal() { async function updateThreadLocal() {
let threadId = null let threadId = null;
if (props.project) { if (props.project) {
threadId = props.project.thread_id threadId = props.project.thread_id;
} else if (props.report) { } else if (props.report) {
threadId = props.report.thread_id threadId = props.report.thread_id;
} }
let thread = null let thread = null;
if (threadId) { if (threadId) {
thread = await useBaseFetch(`thread/${threadId}`) thread = await useBaseFetch(`thread/${threadId}`);
} }
emit('update-thread', thread) emit("update-thread", thread);
} }
const imageIDs = ref([]) const imageIDs = ref([]);
async function onUploadImage(file) { async function onUploadImage(file) {
const response = await useImageUpload(file, { context: 'thread_message' }) const response = await useImageUpload(file, { context: "thread_message" });
imageIDs.value.push(response.id) imageIDs.value.push(response.id);
// Keep the last 10 entries of image IDs // Keep the last 10 entries of image IDs
imageIDs.value = imageIDs.value.slice(-10) imageIDs.value = imageIDs.value.slice(-10);
return response.url return response.url;
} }
async function sendReply(status = null, privateMessage = false) { async function sendReply(status = null, privateMessage = false) {
try { try {
const body = { const body = {
body: { body: {
type: 'text', type: "text",
body: replyBody.value, body: replyBody.value,
private: privateMessage, private: privateMessage,
}, },
} };
if (imageIDs.value.length > 0) { if (imageIDs.value.length > 0) {
body.body = { body.body = {
...body.body, ...body.body,
uploaded_images: imageIDs.value, uploaded_images: imageIDs.value,
} };
} }
await useBaseFetch(`thread/${props.thread.id}`, { await useBaseFetch(`thread/${props.thread.id}`, {
method: 'POST', method: "POST",
body, body,
}) });
replyBody.value = '' replyBody.value = "";
await updateThreadLocal() await updateThreadLocal();
if (status !== null) { if (status !== null) {
props.setStatus(status) props.setStatus(status);
} }
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Error sending message', title: "Error sending message",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
} }
async function closeReport(reply) { async function closeReport(reply) {
if (reply) { if (reply) {
await sendReply() await sendReply();
} }
try { try {
await useBaseFetch(`report/${props.report.id}`, { await useBaseFetch(`report/${props.report.id}`, {
method: 'PATCH', method: "PATCH",
body: { body: {
closed: true, closed: true,
}, },
}) });
await updateThreadLocal() await updateThreadLocal();
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Error closing report', title: "Error closing report",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
} }
async function reopenReport() { async function reopenReport() {
try { try {
await useBaseFetch(`report/${props.report.id}`, { await useBaseFetch(`report/${props.report.id}`, {
method: 'PATCH', method: "PATCH",
body: { body: {
closed: false, closed: false,
}, },
}) });
await updateThreadLocal() await updateThreadLocal();
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Error reopening report', title: "Error reopening report",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
} }
} }
const replyWithSubmission = ref(false) const replyWithSubmission = ref(false);
const submissionConfirmation = ref(false) const submissionConfirmation = ref(false);
function openResubmitModal(reply) { function openResubmitModal(reply) {
submissionConfirmation.value = false submissionConfirmation.value = false;
replyWithSubmission.value = reply replyWithSubmission.value = reply;
modalSubmit.value.show() modalSubmit.value.show();
} }
async function resubmit() { async function resubmit() {
if (replyWithSubmission.value) { if (replyWithSubmission.value) {
await sendReply('processing') await sendReply("processing");
} else { } else {
await props.setStatus('processing') await props.setStatus("processing");
} }
modalSubmit.value.hide() modalSubmit.value.hide();
} }
const requestedStatus = computed(() => props.project.requested_status ?? 'approved') const requestedStatus = computed(() => props.project.requested_status ?? "approved");
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -106,12 +106,12 @@ import {
LockIcon, LockIcon,
ModrinthIcon, ModrinthIcon,
ScaleIcon, ScaleIcon,
} from '@modrinth/assets' } from "@modrinth/assets";
import { OverflowMenu, ConditionalNuxtLink } from '@modrinth/ui' import { OverflowMenu, ConditionalNuxtLink } from "@modrinth/ui";
import { renderString } from '@modrinth/utils' import { renderString } from "@modrinth/utils";
import Avatar from '~/components/ui/Avatar.vue' import Avatar from "~/components/ui/Avatar.vue";
import Badge from '~/components/ui/Badge.vue' import Badge from "~/components/ui/Badge.vue";
import { isStaff } from '~/helpers/users.js' import { isStaff } from "~/helpers/users.js";
const props = defineProps({ const props = defineProps({
message: { message: {
@ -142,34 +142,34 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const emit = defineEmits(['update-thread']) const emit = defineEmits(["update-thread"]);
const formattedMessage = computed(() => { const formattedMessage = computed(() => {
const body = renderString(props.message.body.body) const body = renderString(props.message.body.body);
if (props.forceCompact) { if (props.forceCompact) {
const hasImage = body.includes('<img') const hasImage = body.includes("<img");
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, '') const noHtml = body.replace(/<\/?[^>]+(>|$)/g, "");
if (noHtml.trim()) { if (noHtml.trim()) {
return noHtml return noHtml;
} else if (hasImage) { } else if (hasImage) {
return 'sent an image.' return "sent an image.";
} else { } else {
return 'sent a message.' return "sent a message.";
} }
} }
return body return body;
}) });
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime();
const timeSincePosted = ref(formatRelativeTime(props.message.created)) const timeSincePosted = ref(formatRelativeTime(props.message.created));
async function deleteMessage() { async function deleteMessage() {
await useBaseFetch(`message/${props.message.id}`, { await useBaseFetch(`message/${props.message.id}`, {
method: 'DELETE', method: "DELETE",
}) });
emit('update-thread') emit("update-thread");
} }
</script> </script>
@ -194,9 +194,9 @@ async function deleteMessage() {
--gap-size: var(--spacing-card-sm); --gap-size: var(--spacing-card-sm);
display: grid; display: grid;
grid-template: grid-template:
'icon author actions' "icon author actions"
'icon body actions' "icon body actions"
'date date date'; "date date date";
grid-template-columns: min-content auto 1fr; grid-template-columns: min-content auto 1fr;
column-gap: var(--gap-size); column-gap: var(--gap-size);
row-gap: var(--spacing-card-xs); row-gap: var(--spacing-card-xs);
@ -312,9 +312,9 @@ role-moderator {
&.has-body { &.has-body {
grid-template: grid-template:
'icon author actions' "icon author actions"
'icon body actions' "icon body actions"
'date date date'; "date date date";
grid-template-columns: min-content auto 1fr; grid-template-columns: min-content auto 1fr;
} }
} }
@ -327,8 +327,8 @@ role-moderator {
&.has-body { &.has-body {
grid-template: grid-template:
'icon author date actions' "icon author date actions"
'icon body body actions'; "icon body body actions";
grid-template-columns: min-content auto 1fr; grid-template-columns: min-content auto 1fr;
grid-template-rows: min-content 1fr auto; grid-template-rows: min-content 1fr auto;
} }

View File

@ -24,8 +24,8 @@
</template> </template>
<script setup> <script setup>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component' import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue' import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
const props = defineProps({ const props = defineProps({
thread: { thread: {
@ -49,36 +49,36 @@ const props = defineProps({
type: Array, type: Array,
required: false, required: false,
default() { default() {
return [] return [];
}, },
}, },
auth: { auth: {
type: Object, type: Object,
required: true, required: true,
}, },
}) });
const app = useNuxtApp() const app = useNuxtApp();
const members = computed(() => { const members = computed(() => {
const members = {} const members = {};
for (const member of props.thread.members) { for (const member of props.thread.members) {
members[member.id] = member members[member.id] = member;
} }
members[props.auth.user.id] = props.auth.user members[props.auth.user.id] = props.auth.user;
return members return members;
}) });
const displayMessages = computed(() => { const displayMessages = computed(() => {
const sortedMessages = props.thread.messages const sortedMessages = props.thread.messages
.slice() .slice()
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created)) .sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created));
if (props.messages.length > 0) { if (props.messages.length > 0) {
return sortedMessages.filter((msg) => props.messages.includes(msg.id)) return sortedMessages.filter((msg) => props.messages.includes(msg.id));
} else { } else {
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : [] return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : [];
} }
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,129 +1,132 @@
/* eslint-disable no-undef */
export const useAuth = async (oldToken = null) => { export const useAuth = async (oldToken = null) => {
const auth = useState('auth', () => ({ const auth = useState("auth", () => ({
user: null, user: null,
token: '', token: "",
headers: {}, headers: {},
})) }));
if (!auth.value.user || oldToken) { if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken) auth.value = await initAuth(oldToken);
} }
return auth return auth;
} };
export const initAuth = async (oldToken = null) => { export const initAuth = async (oldToken = null) => {
const auth = { const auth = {
user: null, user: null,
token: '', token: "",
};
if (oldToken === "none") {
return auth;
} }
if (oldToken === 'none') { const route = useRoute();
return auth const authCookie = useCookie("auth-token", {
}
const route = useRoute()
const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
}) });
if (oldToken) { if (oldToken) {
authCookie.value = oldToken authCookie.value = oldToken;
} }
if (route.query.code && !route.fullPath.includes('new_account=true')) { if (route.query.code && !route.fullPath.includes("new_account=true")) {
authCookie.value = route.query.code authCookie.value = route.query.code;
} }
if (authCookie.value) { if (authCookie.value) {
auth.token = authCookie.value auth.token = authCookie.value;
if (!auth.token || !auth.token.startsWith('mra_')) { if (!auth.token || !auth.token.startsWith("mra_")) {
return auth return auth;
} }
try { try {
auth.user = await useBaseFetch( auth.user = await useBaseFetch(
'user', "user",
{ {
headers: { headers: {
Authorization: auth.token, Authorization: auth.token,
}, },
}, },
true true,
) );
} catch {} } catch {
/* empty */
}
} }
if (!auth.user && auth.token) { if (!auth.user && auth.token) {
try { try {
const session = await useBaseFetch( const session = await useBaseFetch(
'session/refresh', "session/refresh",
{ {
method: 'POST', method: "POST",
headers: { headers: {
Authorization: auth.token, Authorization: auth.token,
}, },
}, },
true true,
) );
auth.token = session.session auth.token = session.session;
authCookie.value = auth.token authCookie.value = auth.token;
auth.user = await useBaseFetch( auth.user = await useBaseFetch(
'user', "user",
{ {
headers: { headers: {
Authorization: auth.token, Authorization: auth.token,
}, },
}, },
true true,
) );
} catch { } catch {
authCookie.value = null authCookie.value = null;
} }
} }
return auth return auth;
} };
export const getAuthUrl = (provider, redirect = '') => { export const getAuthUrl = (provider, redirect = "") => {
const config = useRuntimeConfig() const config = useRuntimeConfig();
const route = useNativeRoute() const route = useNativeRoute();
if (redirect === '') { if (redirect === "") {
redirect = route.path redirect = route.path;
} }
const fullURL = `${config.public.siteUrl}${redirect}` const fullURL = `${config.public.siteUrl}${redirect}`;
return `${config.public.apiBaseUrl}auth/init?url=${fullURL}&provider=${provider}` return `${config.public.apiBaseUrl}auth/init?url=${fullURL}&provider=${provider}`;
} };
export const removeAuthProvider = async (provider) => { export const removeAuthProvider = async (provider) => {
startLoading() startLoading();
try { try {
const auth = await useAuth() const auth = await useAuth();
await useBaseFetch('auth/provider', { await useBaseFetch("auth/provider", {
method: 'DELETE', method: "DELETE",
body: { body: {
provider, provider,
}, },
}) });
await useAuth(auth.value.token) await useAuth(auth.value.token);
} catch (err) { } catch (err) {
const data = useNuxtApp() const data = useNuxtApp();
data.$notify({ data.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data.description, text: err.data.description,
type: 'error', type: "error",
}) });
}
stopLoading()
} }
stopLoading();
};

View File

@ -1,573 +1,577 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
export const scopeMessages = defineMessages({ export const scopeMessages = defineMessages({
userReadEmailLabel: { userReadEmailLabel: {
id: 'scopes.userReadEmail.label', id: "scopes.userReadEmail.label",
defaultMessage: 'Read user email', defaultMessage: "Read user email",
}, },
userReadEmailDescription: { userReadEmailDescription: {
id: 'scopes.userReadEmail.description', id: "scopes.userReadEmail.description",
defaultMessage: 'Read your email', defaultMessage: "Read your email",
}, },
userReadLabel: { userReadLabel: {
id: 'scopes.userRead.label', id: "scopes.userRead.label",
defaultMessage: 'Read user data', defaultMessage: "Read user data",
}, },
userReadDescription: { userReadDescription: {
id: 'scopes.userRead.description', id: "scopes.userRead.description",
defaultMessage: 'Access your public profile information', defaultMessage: "Access your public profile information",
}, },
userWriteLabel: { userWriteLabel: {
id: 'scopes.userWrite.label', id: "scopes.userWrite.label",
defaultMessage: 'Write user data', defaultMessage: "Write user data",
}, },
userWriteDescription: { userWriteDescription: {
id: 'scopes.userWrite.description', id: "scopes.userWrite.description",
defaultMessage: 'Write to your profile', defaultMessage: "Write to your profile",
}, },
userDeleteLabel: { userDeleteLabel: {
id: 'scopes.userDelete.label', id: "scopes.userDelete.label",
defaultMessage: 'Delete your account', defaultMessage: "Delete your account",
}, },
userDeleteDescription: { userDeleteDescription: {
id: 'scopes.userDelete.description', id: "scopes.userDelete.description",
defaultMessage: 'Delete your account', defaultMessage: "Delete your account",
}, },
userAuthWriteLabel: { userAuthWriteLabel: {
id: 'scopes.userAuthWrite.label', id: "scopes.userAuthWrite.label",
defaultMessage: 'Write auth data', defaultMessage: "Write auth data",
}, },
userAuthWriteDescription: { userAuthWriteDescription: {
id: 'scopes.userAuthWrite.description', id: "scopes.userAuthWrite.description",
defaultMessage: 'Modify your authentication data', defaultMessage: "Modify your authentication data",
}, },
notificationReadLabel: { notificationReadLabel: {
id: 'scopes.notificationRead.label', id: "scopes.notificationRead.label",
defaultMessage: 'Read notifications', defaultMessage: "Read notifications",
}, },
notificationReadDescription: { notificationReadDescription: {
id: 'scopes.notificationRead.description', id: "scopes.notificationRead.description",
defaultMessage: 'Read your notifications', defaultMessage: "Read your notifications",
}, },
notificationWriteLabel: { notificationWriteLabel: {
id: 'scopes.notificationWrite.label', id: "scopes.notificationWrite.label",
defaultMessage: 'Write notifications', defaultMessage: "Write notifications",
}, },
notificationWriteDescription: { notificationWriteDescription: {
id: 'scopes.notificationWrite.description', id: "scopes.notificationWrite.description",
defaultMessage: 'Delete/View your notifications', defaultMessage: "Delete/View your notifications",
}, },
payoutsReadLabel: { payoutsReadLabel: {
id: 'scopes.payoutsRead.label', id: "scopes.payoutsRead.label",
defaultMessage: 'Read payouts', defaultMessage: "Read payouts",
}, },
payoutsReadDescription: { payoutsReadDescription: {
id: 'scopes.payoutsRead.description', id: "scopes.payoutsRead.description",
defaultMessage: 'Read your payouts data', defaultMessage: "Read your payouts data",
}, },
payoutsWriteLabel: { payoutsWriteLabel: {
id: 'scopes.payoutsWrite.label', id: "scopes.payoutsWrite.label",
defaultMessage: 'Write payouts', defaultMessage: "Write payouts",
}, },
payoutsWriteDescription: { payoutsWriteDescription: {
id: 'scopes.payoutsWrite.description', id: "scopes.payoutsWrite.description",
defaultMessage: 'Withdraw money', defaultMessage: "Withdraw money",
}, },
analyticsLabel: { analyticsLabel: {
id: 'scopes.analytics.label', id: "scopes.analytics.label",
defaultMessage: 'Read analytics', defaultMessage: "Read analytics",
}, },
analyticsDescription: { analyticsDescription: {
id: 'scopes.analytics.description', id: "scopes.analytics.description",
defaultMessage: 'Access your analytics data', defaultMessage: "Access your analytics data",
}, },
projectCreateLabel: { projectCreateLabel: {
id: 'scopes.projectCreate.label', id: "scopes.projectCreate.label",
defaultMessage: 'Create projects', defaultMessage: "Create projects",
}, },
projectCreateDescription: { projectCreateDescription: {
id: 'scopes.projectCreate.description', id: "scopes.projectCreate.description",
defaultMessage: 'Create new projects', defaultMessage: "Create new projects",
}, },
projectReadLabel: { projectReadLabel: {
id: 'scopes.projectRead.label', id: "scopes.projectRead.label",
defaultMessage: 'Read projects', defaultMessage: "Read projects",
}, },
projectReadDescription: { projectReadDescription: {
id: 'scopes.projectRead.description', id: "scopes.projectRead.description",
defaultMessage: 'Read all your projects', defaultMessage: "Read all your projects",
}, },
projectWriteLabel: { projectWriteLabel: {
id: 'scopes.projectWrite.label', id: "scopes.projectWrite.label",
defaultMessage: 'Write projects', defaultMessage: "Write projects",
}, },
projectWriteDescription: { projectWriteDescription: {
id: 'scopes.projectWrite.description', id: "scopes.projectWrite.description",
defaultMessage: 'Write to project data', defaultMessage: "Write to project data",
}, },
projectDeleteLabel: { projectDeleteLabel: {
id: 'scopes.projectDelete.label', id: "scopes.projectDelete.label",
defaultMessage: 'Delete projects', defaultMessage: "Delete projects",
}, },
projectDeleteDescription: { projectDeleteDescription: {
id: 'scopes.projectDelete.description', id: "scopes.projectDelete.description",
defaultMessage: 'Delete your projects', defaultMessage: "Delete your projects",
}, },
versionCreateLabel: { versionCreateLabel: {
id: 'scopes.versionCreate.label', id: "scopes.versionCreate.label",
defaultMessage: 'Create versions', defaultMessage: "Create versions",
}, },
versionCreateDescription: { versionCreateDescription: {
id: 'scopes.versionCreate.description', id: "scopes.versionCreate.description",
defaultMessage: 'Create new versions', defaultMessage: "Create new versions",
}, },
versionReadLabel: { versionReadLabel: {
id: 'scopes.versionRead.label', id: "scopes.versionRead.label",
defaultMessage: 'Read versions', defaultMessage: "Read versions",
}, },
versionReadDescription: { versionReadDescription: {
id: 'scopes.versionRead.description', id: "scopes.versionRead.description",
defaultMessage: 'Read all versions', defaultMessage: "Read all versions",
}, },
versionWriteLabel: { versionWriteLabel: {
id: 'scopes.versionWrite.label', id: "scopes.versionWrite.label",
defaultMessage: 'Write versions', defaultMessage: "Write versions",
}, },
versionWriteDescription: { versionWriteDescription: {
id: 'scopes.versionWrite.description', id: "scopes.versionWrite.description",
defaultMessage: 'Write to version data', defaultMessage: "Write to version data",
}, },
versionDeleteLabel: { versionDeleteLabel: {
id: 'scopes.versionDelete.label', id: "scopes.versionDelete.label",
defaultMessage: 'Delete versions', defaultMessage: "Delete versions",
}, },
versionDeleteDescription: { versionDeleteDescription: {
id: 'scopes.versionDelete.description', id: "scopes.versionDelete.description",
defaultMessage: 'Delete a version', defaultMessage: "Delete a version",
}, },
reportCreateLabel: { reportCreateLabel: {
id: 'scopes.reportCreate.label', id: "scopes.reportCreate.label",
defaultMessage: 'Create reports', defaultMessage: "Create reports",
}, },
reportCreateDescription: { reportCreateDescription: {
id: 'scopes.reportCreate.description', id: "scopes.reportCreate.description",
defaultMessage: 'Create reports', defaultMessage: "Create reports",
}, },
reportReadLabel: { reportReadLabel: {
id: 'scopes.reportRead.label', id: "scopes.reportRead.label",
defaultMessage: 'Read reports', defaultMessage: "Read reports",
}, },
reportReadDescription: { reportReadDescription: {
id: 'scopes.reportRead.description', id: "scopes.reportRead.description",
defaultMessage: 'Read reports', defaultMessage: "Read reports",
}, },
reportWriteLabel: { reportWriteLabel: {
id: 'scopes.reportWrite.label', id: "scopes.reportWrite.label",
defaultMessage: 'Write reports', defaultMessage: "Write reports",
}, },
reportWriteDescription: { reportWriteDescription: {
id: 'scopes.reportWrite.description', id: "scopes.reportWrite.description",
defaultMessage: 'Edit reports', defaultMessage: "Edit reports",
}, },
reportDeleteLabel: { reportDeleteLabel: {
id: 'scopes.reportDelete.label', id: "scopes.reportDelete.label",
defaultMessage: 'Delete reports', defaultMessage: "Delete reports",
}, },
reportDeleteDescription: { reportDeleteDescription: {
id: 'scopes.reportDelete.description', id: "scopes.reportDelete.description",
defaultMessage: 'Delete reports', defaultMessage: "Delete reports",
}, },
threadReadLabel: { threadReadLabel: {
id: 'scopes.threadRead.label', id: "scopes.threadRead.label",
defaultMessage: 'Read threads', defaultMessage: "Read threads",
}, },
threadReadDescription: { threadReadDescription: {
id: 'scopes.threadRead.description', id: "scopes.threadRead.description",
defaultMessage: 'Read threads', defaultMessage: "Read threads",
}, },
threadWriteLabel: { threadWriteLabel: {
id: 'scopes.threadWrite.label', id: "scopes.threadWrite.label",
defaultMessage: 'Write threads', defaultMessage: "Write threads",
}, },
threadWriteDescription: { threadWriteDescription: {
id: 'scopes.threadWrite.description', id: "scopes.threadWrite.description",
defaultMessage: 'Write to threads', defaultMessage: "Write to threads",
}, },
patCreateLabel: { patCreateLabel: {
id: 'scopes.patCreate.label', id: "scopes.patCreate.label",
defaultMessage: 'Create PATs', defaultMessage: "Create PATs",
}, },
patCreateDescription: { patCreateDescription: {
id: 'scopes.patCreate.description', id: "scopes.patCreate.description",
defaultMessage: 'Create personal API tokens', defaultMessage: "Create personal API tokens",
}, },
patReadLabel: { patReadLabel: {
id: 'scopes.patRead.label', id: "scopes.patRead.label",
defaultMessage: 'Read PATs', defaultMessage: "Read PATs",
}, },
patReadDescription: { patReadDescription: {
id: 'scopes.patRead.description', id: "scopes.patRead.description",
defaultMessage: 'View created API tokens', defaultMessage: "View created API tokens",
}, },
patWriteLabel: { patWriteLabel: {
id: 'scopes.patWrite.label', id: "scopes.patWrite.label",
defaultMessage: 'Write PATs', defaultMessage: "Write PATs",
}, },
patWriteDescription: { patWriteDescription: {
id: 'scopes.patWrite.description', id: "scopes.patWrite.description",
defaultMessage: 'Edit personal API tokens', defaultMessage: "Edit personal API tokens",
}, },
patDeleteLabel: { patDeleteLabel: {
id: 'scopes.patDelete.label', id: "scopes.patDelete.label",
defaultMessage: 'Delete PATs', defaultMessage: "Delete PATs",
}, },
patDeleteDescription: { patDeleteDescription: {
id: 'scopes.patDelete.description', id: "scopes.patDelete.description",
defaultMessage: 'Delete your personal API tokens', defaultMessage: "Delete your personal API tokens",
}, },
sessionReadLabel: { sessionReadLabel: {
id: 'scopes.sessionRead.label', id: "scopes.sessionRead.label",
defaultMessage: 'Read sessions', defaultMessage: "Read sessions",
}, },
sessionReadDescription: { sessionReadDescription: {
id: 'scopes.sessionRead.description', id: "scopes.sessionRead.description",
defaultMessage: 'Read active sessions', defaultMessage: "Read active sessions",
}, },
sessionDeleteLabel: { sessionDeleteLabel: {
id: 'scopes.sessionDelete.label', id: "scopes.sessionDelete.label",
defaultMessage: 'Delete sessions', defaultMessage: "Delete sessions",
}, },
sessionDeleteDescription: { sessionDeleteDescription: {
id: 'scopes.sessionDelete.description', id: "scopes.sessionDelete.description",
defaultMessage: 'Delete sessions', defaultMessage: "Delete sessions",
}, },
performAnalyticsLabel: { performAnalyticsLabel: {
id: 'scopes.performAnalytics.label', id: "scopes.performAnalytics.label",
defaultMessage: 'Perform analytics', defaultMessage: "Perform analytics",
}, },
performAnalyticsDescription: { performAnalyticsDescription: {
id: 'scopes.performAnalytics.description', id: "scopes.performAnalytics.description",
defaultMessage: 'Perform analytics actions', defaultMessage: "Perform analytics actions",
}, },
collectionCreateLabel: { collectionCreateLabel: {
id: 'scopes.collectionCreate.label', id: "scopes.collectionCreate.label",
defaultMessage: 'Create collections', defaultMessage: "Create collections",
}, },
collectionCreateDescription: { collectionCreateDescription: {
id: 'scopes.collectionCreate.description', id: "scopes.collectionCreate.description",
defaultMessage: 'Create collections', defaultMessage: "Create collections",
}, },
collectionReadLabel: { collectionReadLabel: {
id: 'scopes.collectionRead.label', id: "scopes.collectionRead.label",
defaultMessage: 'Read collections', defaultMessage: "Read collections",
}, },
collectionReadDescription: { collectionReadDescription: {
id: 'scopes.collectionRead.description', id: "scopes.collectionRead.description",
defaultMessage: 'Read collections', defaultMessage: "Read collections",
}, },
collectionWriteLabel: { collectionWriteLabel: {
id: 'scopes.collectionWrite.label', id: "scopes.collectionWrite.label",
defaultMessage: 'Write collections', defaultMessage: "Write collections",
}, },
collectionWriteDescription: { collectionWriteDescription: {
id: 'scopes.collectionWrite.description', id: "scopes.collectionWrite.description",
defaultMessage: 'Write to collections', defaultMessage: "Write to collections",
}, },
collectionDeleteLabel: { collectionDeleteLabel: {
id: 'scopes.collectionDelete.label', id: "scopes.collectionDelete.label",
defaultMessage: 'Delete collections', defaultMessage: "Delete collections",
}, },
collectionDeleteDescription: { collectionDeleteDescription: {
id: 'scopes.collectionDelete.description', id: "scopes.collectionDelete.description",
defaultMessage: 'Delete collections', defaultMessage: "Delete collections",
}, },
organizationCreateLabel: { organizationCreateLabel: {
id: 'scopes.organizationCreate.label', id: "scopes.organizationCreate.label",
defaultMessage: 'Create organizations', defaultMessage: "Create organizations",
}, },
organizationCreateDescription: { organizationCreateDescription: {
id: 'scopes.organizationCreate.description', id: "scopes.organizationCreate.description",
defaultMessage: 'Create organizations', defaultMessage: "Create organizations",
}, },
organizationReadLabel: { organizationReadLabel: {
id: 'scopes.organizationRead.label', id: "scopes.organizationRead.label",
defaultMessage: 'Read organizations', defaultMessage: "Read organizations",
}, },
organizationReadDescription: { organizationReadDescription: {
id: 'scopes.organizationRead.description', id: "scopes.organizationRead.description",
defaultMessage: 'Read organizations', defaultMessage: "Read organizations",
}, },
organizationWriteLabel: { organizationWriteLabel: {
id: 'scopes.organizationWrite.label', id: "scopes.organizationWrite.label",
defaultMessage: 'Write organizations', defaultMessage: "Write organizations",
}, },
organizationWriteDescription: { organizationWriteDescription: {
id: 'scopes.organizationWrite.description', id: "scopes.organizationWrite.description",
defaultMessage: 'Write to organizations', defaultMessage: "Write to organizations",
}, },
organizationDeleteLabel: { organizationDeleteLabel: {
id: 'scopes.organizationDelete.label', id: "scopes.organizationDelete.label",
defaultMessage: 'Delete organizations', defaultMessage: "Delete organizations",
}, },
organizationDeleteDescription: { organizationDeleteDescription: {
id: 'scopes.organizationDelete.description', id: "scopes.organizationDelete.description",
defaultMessage: 'Delete organizations', defaultMessage: "Delete organizations",
}, },
sessionAccessLabel: { sessionAccessLabel: {
id: 'scopes.sessionAccess.label', id: "scopes.sessionAccess.label",
defaultMessage: 'Access sessions', defaultMessage: "Access sessions",
}, },
sessionAccessDescription: { sessionAccessDescription: {
id: 'scopes.sessionAccess.description', id: "scopes.sessionAccess.description",
defaultMessage: 'Access modrinth-issued sessions', defaultMessage: "Access modrinth-issued sessions",
}, },
}) });
const scopeDefinitions = [ const scopeDefinitions = [
{ {
id: 'USER_READ_EMAIL', id: "USER_READ_EMAIL",
value: BigInt(1) << BigInt(0), value: BigInt(1) << BigInt(0),
label: scopeMessages.userReadEmailLabel, label: scopeMessages.userReadEmailLabel,
desc: scopeMessages.userReadEmailDescription, desc: scopeMessages.userReadEmailDescription,
}, },
{ {
id: 'USER_READ', id: "USER_READ",
value: BigInt(1) << BigInt(1), value: BigInt(1) << BigInt(1),
label: scopeMessages.userReadLabel, label: scopeMessages.userReadLabel,
desc: scopeMessages.userReadDescription, desc: scopeMessages.userReadDescription,
}, },
{ {
id: 'USER_WRITE', id: "USER_WRITE",
value: BigInt(1) << BigInt(2), value: BigInt(1) << BigInt(2),
label: scopeMessages.userWriteLabel, label: scopeMessages.userWriteLabel,
desc: scopeMessages.userWriteDescription, desc: scopeMessages.userWriteDescription,
}, },
{ {
id: 'USER_DELETE', id: "USER_DELETE",
value: BigInt(1) << BigInt(3), value: BigInt(1) << BigInt(3),
label: scopeMessages.userDeleteLabel, label: scopeMessages.userDeleteLabel,
desc: scopeMessages.userDeleteDescription, desc: scopeMessages.userDeleteDescription,
}, },
{ {
id: 'USER_AUTH_WRITE', id: "USER_AUTH_WRITE",
value: BigInt(1) << BigInt(4), value: BigInt(1) << BigInt(4),
label: scopeMessages.userAuthWriteLabel, label: scopeMessages.userAuthWriteLabel,
desc: scopeMessages.userAuthWriteDescription, desc: scopeMessages.userAuthWriteDescription,
}, },
{ {
id: 'NOTIFICATION_READ', id: "NOTIFICATION_READ",
value: BigInt(1) << BigInt(5), value: BigInt(1) << BigInt(5),
label: scopeMessages.notificationReadLabel, label: scopeMessages.notificationReadLabel,
desc: scopeMessages.notificationReadDescription, desc: scopeMessages.notificationReadDescription,
}, },
{ {
id: 'NOTIFICATION_WRITE', id: "NOTIFICATION_WRITE",
value: BigInt(1) << BigInt(6), value: BigInt(1) << BigInt(6),
label: scopeMessages.notificationWriteLabel, label: scopeMessages.notificationWriteLabel,
desc: scopeMessages.notificationWriteDescription, desc: scopeMessages.notificationWriteDescription,
}, },
{ {
id: 'PAYOUTS_READ', id: "PAYOUTS_READ",
value: BigInt(1) << BigInt(7), value: BigInt(1) << BigInt(7),
label: scopeMessages.payoutsReadLabel, label: scopeMessages.payoutsReadLabel,
desc: scopeMessages.payoutsReadDescription, desc: scopeMessages.payoutsReadDescription,
}, },
{ {
id: 'PAYOUTS_WRITE', id: "PAYOUTS_WRITE",
value: BigInt(1) << BigInt(8), value: BigInt(1) << BigInt(8),
label: scopeMessages.payoutsWriteLabel, label: scopeMessages.payoutsWriteLabel,
desc: scopeMessages.payoutsWriteDescription, desc: scopeMessages.payoutsWriteDescription,
}, },
{ {
id: 'ANALYTICS', id: "ANALYTICS",
value: BigInt(1) << BigInt(9), value: BigInt(1) << BigInt(9),
label: scopeMessages.analyticsLabel, label: scopeMessages.analyticsLabel,
desc: scopeMessages.analyticsDescription, desc: scopeMessages.analyticsDescription,
}, },
{ {
id: 'PROJECT_CREATE', id: "PROJECT_CREATE",
value: BigInt(1) << BigInt(10), value: BigInt(1) << BigInt(10),
label: scopeMessages.projectCreateLabel, label: scopeMessages.projectCreateLabel,
desc: scopeMessages.projectCreateDescription, desc: scopeMessages.projectCreateDescription,
}, },
{ {
id: 'PROJECT_READ', id: "PROJECT_READ",
value: BigInt(1) << BigInt(11), value: BigInt(1) << BigInt(11),
label: scopeMessages.projectReadLabel, label: scopeMessages.projectReadLabel,
desc: scopeMessages.projectReadDescription, desc: scopeMessages.projectReadDescription,
}, },
{ {
id: 'PROJECT_WRITE', id: "PROJECT_WRITE",
value: BigInt(1) << BigInt(12), value: BigInt(1) << BigInt(12),
label: scopeMessages.projectWriteLabel, label: scopeMessages.projectWriteLabel,
desc: scopeMessages.projectWriteDescription, desc: scopeMessages.projectWriteDescription,
}, },
{ {
id: 'PROJECT_DELETE', id: "PROJECT_DELETE",
value: BigInt(1) << BigInt(13), value: BigInt(1) << BigInt(13),
label: scopeMessages.projectDeleteLabel, label: scopeMessages.projectDeleteLabel,
desc: scopeMessages.projectDeleteDescription, desc: scopeMessages.projectDeleteDescription,
}, },
{ {
id: 'VERSION_CREATE', id: "VERSION_CREATE",
value: BigInt(1) << BigInt(14), value: BigInt(1) << BigInt(14),
label: scopeMessages.versionCreateLabel, label: scopeMessages.versionCreateLabel,
desc: scopeMessages.versionCreateDescription, desc: scopeMessages.versionCreateDescription,
}, },
{ {
id: 'VERSION_READ', id: "VERSION_READ",
value: BigInt(1) << BigInt(15), value: BigInt(1) << BigInt(15),
label: scopeMessages.versionReadLabel, label: scopeMessages.versionReadLabel,
desc: scopeMessages.versionReadDescription, desc: scopeMessages.versionReadDescription,
}, },
{ {
id: 'VERSION_WRITE', id: "VERSION_WRITE",
value: BigInt(1) << BigInt(16), value: BigInt(1) << BigInt(16),
label: scopeMessages.versionWriteLabel, label: scopeMessages.versionWriteLabel,
desc: scopeMessages.versionWriteDescription, desc: scopeMessages.versionWriteDescription,
}, },
{ {
id: 'VERSION_DELETE', id: "VERSION_DELETE",
value: BigInt(1) << BigInt(17), value: BigInt(1) << BigInt(17),
label: scopeMessages.versionDeleteLabel, label: scopeMessages.versionDeleteLabel,
desc: scopeMessages.versionDeleteDescription, desc: scopeMessages.versionDeleteDescription,
}, },
{ {
id: 'REPORT_CREATE', id: "REPORT_CREATE",
value: BigInt(1) << BigInt(18), value: BigInt(1) << BigInt(18),
label: scopeMessages.reportCreateLabel, label: scopeMessages.reportCreateLabel,
desc: scopeMessages.reportCreateDescription, desc: scopeMessages.reportCreateDescription,
}, },
{ {
id: 'REPORT_READ', id: "REPORT_READ",
value: BigInt(1) << BigInt(19), value: BigInt(1) << BigInt(19),
label: scopeMessages.reportReadLabel, label: scopeMessages.reportReadLabel,
desc: scopeMessages.reportReadDescription, desc: scopeMessages.reportReadDescription,
}, },
{ {
id: 'REPORT_WRITE', id: "REPORT_WRITE",
value: BigInt(1) << BigInt(20), value: BigInt(1) << BigInt(20),
label: scopeMessages.reportWriteLabel, label: scopeMessages.reportWriteLabel,
desc: scopeMessages.reportWriteDescription, desc: scopeMessages.reportWriteDescription,
}, },
{ {
id: 'REPORT_DELETE', id: "REPORT_DELETE",
value: BigInt(1) << BigInt(21), value: BigInt(1) << BigInt(21),
label: scopeMessages.reportDeleteLabel, label: scopeMessages.reportDeleteLabel,
desc: scopeMessages.reportDeleteDescription, desc: scopeMessages.reportDeleteDescription,
}, },
{ {
id: 'THREAD_READ', id: "THREAD_READ",
value: BigInt(1) << BigInt(22), value: BigInt(1) << BigInt(22),
label: scopeMessages.threadReadLabel, label: scopeMessages.threadReadLabel,
desc: scopeMessages.threadReadDescription, desc: scopeMessages.threadReadDescription,
}, },
{ {
id: 'THREAD_WRITE', id: "THREAD_WRITE",
value: BigInt(1) << BigInt(23), value: BigInt(1) << BigInt(23),
label: scopeMessages.threadWriteLabel, label: scopeMessages.threadWriteLabel,
desc: scopeMessages.threadWriteDescription, desc: scopeMessages.threadWriteDescription,
}, },
{ {
id: 'PAT_CREATE', id: "PAT_CREATE",
value: BigInt(1) << BigInt(24), value: BigInt(1) << BigInt(24),
label: scopeMessages.patCreateLabel, label: scopeMessages.patCreateLabel,
desc: scopeMessages.patCreateDescription, desc: scopeMessages.patCreateDescription,
}, },
{ {
id: 'PAT_READ', id: "PAT_READ",
value: BigInt(1) << BigInt(25), value: BigInt(1) << BigInt(25),
label: scopeMessages.patReadLabel, label: scopeMessages.patReadLabel,
desc: scopeMessages.patReadDescription, desc: scopeMessages.patReadDescription,
}, },
{ {
id: 'PAT_WRITE', id: "PAT_WRITE",
value: BigInt(1) << BigInt(26), value: BigInt(1) << BigInt(26),
label: scopeMessages.patWriteLabel, label: scopeMessages.patWriteLabel,
desc: scopeMessages.patWriteDescription, desc: scopeMessages.patWriteDescription,
}, },
{ {
id: 'PAT_DELETE', id: "PAT_DELETE",
value: BigInt(1) << BigInt(27), value: BigInt(1) << BigInt(27),
label: scopeMessages.patDeleteLabel, label: scopeMessages.patDeleteLabel,
desc: scopeMessages.patDeleteDescription, desc: scopeMessages.patDeleteDescription,
}, },
{ {
id: 'SESSION_READ', id: "SESSION_READ",
value: BigInt(1) << BigInt(28), value: BigInt(1) << BigInt(28),
label: scopeMessages.sessionReadLabel, label: scopeMessages.sessionReadLabel,
desc: scopeMessages.sessionReadDescription, desc: scopeMessages.sessionReadDescription,
}, },
{ {
id: 'SESSION_DELETE', id: "SESSION_DELETE",
value: BigInt(1) << BigInt(29), value: BigInt(1) << BigInt(29),
label: scopeMessages.sessionDeleteLabel, label: scopeMessages.sessionDeleteLabel,
desc: scopeMessages.sessionDeleteDescription, desc: scopeMessages.sessionDeleteDescription,
}, },
{ {
id: 'PERFORM_ANALYTICS', id: "PERFORM_ANALYTICS",
value: BigInt(1) << BigInt(30), value: BigInt(1) << BigInt(30),
label: scopeMessages.performAnalyticsLabel, label: scopeMessages.performAnalyticsLabel,
desc: scopeMessages.performAnalyticsDescription, desc: scopeMessages.performAnalyticsDescription,
}, },
{ {
id: 'COLLECTION_CREATE', id: "COLLECTION_CREATE",
value: BigInt(1) << BigInt(31), value: BigInt(1) << BigInt(31),
label: scopeMessages.collectionCreateLabel, label: scopeMessages.collectionCreateLabel,
desc: scopeMessages.collectionCreateDescription, desc: scopeMessages.collectionCreateDescription,
}, },
{ {
id: 'COLLECTION_READ', id: "COLLECTION_READ",
value: BigInt(1) << BigInt(32), value: BigInt(1) << BigInt(32),
label: scopeMessages.collectionReadLabel, label: scopeMessages.collectionReadLabel,
desc: scopeMessages.collectionReadDescription, desc: scopeMessages.collectionReadDescription,
}, },
{ {
id: 'COLLECTION_WRITE', id: "COLLECTION_WRITE",
value: BigInt(1) << BigInt(33), value: BigInt(1) << BigInt(33),
label: scopeMessages.collectionWriteLabel, label: scopeMessages.collectionWriteLabel,
desc: scopeMessages.collectionWriteDescription, desc: scopeMessages.collectionWriteDescription,
}, },
{ {
id: 'COLLECTION_DELETE', id: "COLLECTION_DELETE",
value: BigInt(1) << BigInt(34), value: BigInt(1) << BigInt(34),
label: scopeMessages.collectionDeleteLabel, label: scopeMessages.collectionDeleteLabel,
desc: scopeMessages.collectionDeleteDescription, desc: scopeMessages.collectionDeleteDescription,
}, },
{ {
id: 'ORGANIZATION_CREATE', id: "ORGANIZATION_CREATE",
value: BigInt(1) << BigInt(35), value: BigInt(1) << BigInt(35),
label: scopeMessages.organizationCreateLabel, label: scopeMessages.organizationCreateLabel,
desc: scopeMessages.organizationCreateDescription, desc: scopeMessages.organizationCreateDescription,
}, },
{ {
id: 'ORGANIZATION_READ', id: "ORGANIZATION_READ",
value: BigInt(1) << BigInt(36), value: BigInt(1) << BigInt(36),
label: scopeMessages.organizationReadLabel, label: scopeMessages.organizationReadLabel,
desc: scopeMessages.organizationReadDescription, desc: scopeMessages.organizationReadDescription,
}, },
{ {
id: 'ORGANIZATION_WRITE', id: "ORGANIZATION_WRITE",
value: BigInt(1) << BigInt(37), value: BigInt(1) << BigInt(37),
label: scopeMessages.organizationWriteLabel, label: scopeMessages.organizationWriteLabel,
desc: scopeMessages.organizationWriteDescription, desc: scopeMessages.organizationWriteDescription,
}, },
{ {
id: 'ORGANIZATION_DELETE', id: "ORGANIZATION_DELETE",
value: BigInt(1) << BigInt(38), value: BigInt(1) << BigInt(38),
label: scopeMessages.organizationDeleteLabel, label: scopeMessages.organizationDeleteLabel,
desc: scopeMessages.organizationDeleteDescription, desc: scopeMessages.organizationDeleteDescription,
}, },
{ {
id: 'SESSION_ACCESS', id: "SESSION_ACCESS",
value: BigInt(1) << BigInt(39), value: BigInt(1) << BigInt(39),
label: scopeMessages.sessionAccessLabel, label: scopeMessages.sessionAccessLabel,
desc: scopeMessages.sessionAccessDescription, desc: scopeMessages.sessionAccessDescription,
}, },
] ];
const Scopes = scopeDefinitions.reduce((acc, scope) => { const Scopes = scopeDefinitions.reduce(
acc[scope.id] = scope.value (acc, scope) => {
return acc acc[scope.id] = scope.value;
}, {} as Record<string, bigint>) return acc;
},
{} as Record<string, bigint>,
);
export const restrictedScopes = [ export const restrictedScopes = [
Scopes.PAT_READ, Scopes.PAT_READ,
@ -580,18 +584,18 @@ export const restrictedScopes = [
Scopes.USER_AUTH_WRITE, Scopes.USER_AUTH_WRITE,
Scopes.USER_DELETE, Scopes.USER_DELETE,
Scopes.PERFORM_ANALYTICS, Scopes.PERFORM_ANALYTICS,
] ];
export const scopeList = Object.entries(Scopes) export const scopeList = Object.entries(Scopes)
.filter(([_, value]) => !restrictedScopes.includes(value)) .filter(([_, value]) => !restrictedScopes.includes(value))
.map(([key, _]) => key) .map(([key, _]) => key);
export const getScopeValue = (scope: string) => { export const getScopeValue = (scope: string) => {
return Scopes[scope] return Scopes[scope];
} };
export const encodeScopes = (scopes: string[]) => { export const encodeScopes = (scopes: string[]) => {
let scopeFlag = BigInt(0) let scopeFlag = BigInt(0);
// We iterate over the provided scopes // We iterate over the provided scopes
for (const scope of scopes) { for (const scope of scopes) {
@ -599,77 +603,77 @@ export const encodeScopes = (scopes: string[]) => {
for (const [scopeName, scopeFlagValue] of Object.entries(Scopes)) { for (const [scopeName, scopeFlagValue] of Object.entries(Scopes)) {
// If the scope name is the same as the provided scope, add the scope flag to the scopeFlag variable // If the scope name is the same as the provided scope, add the scope flag to the scopeFlag variable
if (scopeName === scope) { if (scopeName === scope) {
scopeFlag = scopeFlag | scopeFlagValue scopeFlag = scopeFlag | scopeFlagValue;
} }
} }
} }
return scopeFlag return scopeFlag;
} };
export const decodeScopes = (scopes: bigint | number) => { export const decodeScopes = (scopes: bigint | number) => {
if (typeof scopes === 'number') { if (typeof scopes === "number") {
scopes = BigInt(scopes) scopes = BigInt(scopes);
} }
const authorizedScopes = [] const authorizedScopes = [];
// We iterate over the entries of the Scopes object // We iterate over the entries of the Scopes object
for (const [scopeName, scopeFlag] of Object.entries(Scopes)) { for (const [scopeName, scopeFlag] of Object.entries(Scopes)) {
// If the scope flag is present in the provided number, add the scope name to the list // If the scope flag is present in the provided number, add the scope name to the list
if ((scopes & scopeFlag) === scopeFlag) { if ((scopes & scopeFlag) === scopeFlag) {
authorizedScopes.push(scopeName) authorizedScopes.push(scopeName);
} }
} }
return authorizedScopes return authorizedScopes;
} };
export const hasScope = (scopes: bigint, scope: string) => { export const hasScope = (scopes: bigint, scope: string) => {
const authorizedScopes = decodeScopes(scopes) const authorizedScopes = decodeScopes(scopes);
return authorizedScopes.includes(scope) return authorizedScopes.includes(scope);
} };
export const toggleScope = (scopes: bigint, scope: string) => { export const toggleScope = (scopes: bigint, scope: string) => {
const authorizedScopes = decodeScopes(scopes) const authorizedScopes = decodeScopes(scopes);
if (authorizedScopes.includes(scope)) { if (authorizedScopes.includes(scope)) {
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope)) return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope));
} else { } else {
return encodeScopes([...authorizedScopes, scope]) return encodeScopes([...authorizedScopes, scope]);
}
} }
};
export const useScopes = () => { export const useScopes = () => {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl();
const scopesToDefinitions = (scopes: bigint) => { const scopesToDefinitions = (scopes: bigint) => {
const authorizedScopes = decodeScopes(scopes) const authorizedScopes = decodeScopes(scopes);
return authorizedScopes.map((scope) => { return authorizedScopes.map((scope) => {
const scopeDefinition = scopeDefinitions.find( const scopeDefinition = scopeDefinitions.find(
(scopeDefinition) => scopeDefinition.id === scope (scopeDefinition) => scopeDefinition.id === scope,
) );
if (!scopeDefinition) { if (!scopeDefinition) {
throw new Error(`Scope ${scope} not found`) throw new Error(`Scope ${scope} not found`);
}
return formatMessage(scopeDefinition.desc)
})
} }
return formatMessage(scopeDefinition.desc);
});
};
const scopesToLabels = (scopes: bigint) => { const scopesToLabels = (scopes: bigint) => {
const authorizedScopes = decodeScopes(scopes) const authorizedScopes = decodeScopes(scopes);
return authorizedScopes.map((scope) => { return authorizedScopes.map((scope) => {
const scopeDefinition = scopeDefinitions.find( const scopeDefinition = scopeDefinitions.find(
(scopeDefinition) => scopeDefinition.id === scope (scopeDefinition) => scopeDefinition.id === scope,
) );
if (!scopeDefinition) { if (!scopeDefinition) {
throw new Error(`Scope ${scope} not found`) throw new Error(`Scope ${scope} not found`);
}
return formatMessage(scopeDefinition.label)
})
} }
return formatMessage(scopeDefinition.label);
});
};
return { return {
scopesToDefinitions, scopesToDefinitions,
scopesToLabels, scopesToLabels,
} };
} };

View File

@ -1,6 +1,6 @@
export type AutoRef<T> = [T] extends [(...args: any[]) => any] export type AutoRef<T> = [T] extends [(...args: any[]) => any]
? Ref<T> | (() => T) ? Ref<T> | (() => T)
: T | Ref<T> | (() => T) : T | Ref<T> | (() => T);
/** /**
* Accepts a value directly, a ref with the value or a getter function and returns a Vue ref. * Accepts a value directly, a ref with the value or a getter function and returns a Vue ref.
@ -8,6 +8,6 @@ export type AutoRef<T> = [T] extends [(...args: any[]) => any]
* @returns Either the original or newly created ref. * @returns Either the original or newly created ref.
*/ */
export function useAutoRef<T>(value: AutoRef<T>): Ref<T> { export function useAutoRef<T>(value: AutoRef<T>): Ref<T> {
if (typeof value === 'function') return computed(() => value()) if (typeof value === "function") return computed(() => value());
return isRef(value) ? value : ref(value as any) return isRef(value) ? value : ref(value as any);
} }

View File

@ -1,18 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/compact-number' import { createFormatter, type Formatter } from "@vintl/compact-number";
import type { IntlController } from '@vintl/vintl/controller' import type { IntlController } from "@vintl/vintl/controller";
const formatters = new WeakMap<IntlController<any>, Formatter>() const formatters = new WeakMap<IntlController<any>, Formatter>();
export function useCompactNumber(): Formatter { export function useCompactNumber(): Formatter {
const vintl = useVIntl() const vintl = useVIntl();
let formatter = formatters.get(vintl) let formatter = formatters.get(vintl);
if (formatter == null) { if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl)) const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options) formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter) formatters.set(vintl, formatter);
} }
return formatter return formatter;
} }

View File

@ -1,12 +1,13 @@
/* eslint-disable no-undef */
export const useCosmetics = () => export const useCosmetics = () =>
useState('cosmetics', () => { useState("cosmetics", () => {
const cosmetics = useCookie('cosmetics', { const cosmetics = useCookie("cosmetics", {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
}) });
if (!cosmetics.value) { if (!cosmetics.value) {
cosmetics.value = { cosmetics.value = {
@ -16,37 +17,37 @@ export const useCosmetics = () =>
externalLinksNewTab: true, externalLinksNewTab: true,
notUsingBlockers: false, notUsingBlockers: false,
hideModrinthAppPromos: false, hideModrinthAppPromos: false,
preferredDarkTheme: 'dark', preferredDarkTheme: "dark",
searchDisplayMode: { searchDisplayMode: {
mod: 'list', mod: "list",
plugin: 'list', plugin: "list",
resourcepack: 'gallery', resourcepack: "gallery",
modpack: 'list', modpack: "list",
shader: 'gallery', shader: "gallery",
datapack: 'list', datapack: "list",
user: 'list', user: "list",
collection: 'list', collection: "list",
}, },
hideStagingBanner: false, hideStagingBanner: false,
} };
} }
return cosmetics.value return cosmetics.value;
}) });
export const saveCosmetics = () => { export const saveCosmetics = () => {
const cosmetics = useCosmetics() const cosmetics = useCosmetics();
console.log('SAVING COSMETICS:') console.log("SAVING COSMETICS:");
console.log(cosmetics) console.log(cosmetics);
const cosmeticsCookie = useCookie('cosmetics', { const cosmeticsCookie = useCookie("cosmetics", {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
}) });
cosmeticsCookie.value = cosmetics.value cosmeticsCookie.value = cosmetics.value;
} };

View File

@ -1,17 +1,18 @@
import dayjs from 'dayjs' /* eslint-disable no-undef */
import relativeTime from 'dayjs/plugin/relativeTime' import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime) dayjs.extend(relativeTime);
export const useCurrentDate = () => useState('currentDate', () => Date.now()) export const useCurrentDate = () => useState("currentDate", () => Date.now());
export const updateCurrentDate = () => { export const updateCurrentDate = () => {
const currentDate = useCurrentDate() const currentDate = useCurrentDate();
currentDate.value = Date.now() currentDate.value = Date.now();
} };
export const fromNow = (date) => { export const fromNow = (date) => {
const currentDate = useCurrentDate() const currentDate = useCurrentDate();
return dayjs(date).from(currentDate.value) return dayjs(date).from(currentDate.value);
} };

View File

@ -1,91 +1,91 @@
import { useAutoRef, type AutoRef } from './auto-ref.ts' import { useAutoRef, type AutoRef } from "./auto-ref.ts";
const safeTags = new Map<string, string>() const safeTags = new Map<string, string>();
function safeTagFor(locale: string) { function safeTagFor(locale: string) {
let safeTag = safeTags.get(locale) let safeTag = safeTags.get(locale);
if (safeTag == null) { if (safeTag == null) {
safeTag = new Intl.Locale(locale).baseName safeTag = new Intl.Locale(locale).baseName;
safeTags.set(locale, safeTag) safeTags.set(locale, safeTag);
} }
return safeTag return safeTag;
} }
type DisplayNamesWrapper = Intl.DisplayNames & { type DisplayNamesWrapper = Intl.DisplayNames & {
of(tag: string): string | undefined of(tag: string): string | undefined;
} };
const displayNamesDicts = new Map<string, DisplayNamesWrapper>() const displayNamesDicts = new Map<string, DisplayNamesWrapper>();
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) { function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
return JSON.stringify({ ...options, locale }) return JSON.stringify({ ...options, locale });
} }
export function createDisplayNames( export function createDisplayNames(
locale: string, locale: string,
options: Intl.DisplayNamesOptions = { type: 'language' } options: Intl.DisplayNamesOptions = { type: "language" },
) { ) {
const wrapperKey = getWrapperKey(locale, options) const wrapperKey = getWrapperKey(locale, options);
let wrapper = displayNamesDicts.get(wrapperKey) let wrapper = displayNamesDicts.get(wrapperKey);
if (wrapper == null) { if (wrapper == null) {
const dict = new Intl.DisplayNames(locale, options) const dict = new Intl.DisplayNames(locale, options);
const badTags: string[] = [] const badTags: string[] = [];
wrapper = { wrapper = {
resolvedOptions() { resolvedOptions() {
return dict.resolvedOptions() return dict.resolvedOptions();
}, },
of(tag: string) { of(tag: string) {
let attempt = 0 let attempt = 0;
// eslint-disable-next-line no-labels // eslint-disable-next-line no-labels
lookupLoop: do { lookupLoop: do {
let lookup: string let lookup: string;
switch (attempt) { switch (attempt) {
case 0: case 0:
lookup = tag lookup = tag;
break break;
case 1: case 1:
lookup = safeTagFor(tag) lookup = safeTagFor(tag);
break break;
default: default:
// eslint-disable-next-line no-labels // eslint-disable-next-line no-labels
break lookupLoop break lookupLoop;
} }
if (badTags.includes(lookup)) continue if (badTags.includes(lookup)) continue;
try { try {
return dict.of(lookup) return dict.of(lookup);
} catch (err) { } catch (err) {
console.warn( console.warn(
`Failed to get display name for ${lookup} using dictionary for ${ `Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale this.resolvedOptions().locale
}` }`,
) );
badTags.push(lookup) badTags.push(lookup);
continue continue;
} }
} while (++attempt < 5) } while (++attempt < 5);
return undefined return undefined;
}, },
};
displayNamesDicts.set(wrapperKey, wrapper);
} }
displayNamesDicts.set(wrapperKey, wrapper) return wrapper;
}
return wrapper
} }
export function useDisplayNames( export function useDisplayNames(
locale: AutoRef<string>, locale: AutoRef<string>,
options?: AutoRef<Intl.DisplayNamesOptions | undefined> options?: AutoRef<Intl.DisplayNamesOptions | undefined>,
) { ) {
const $locale = useAutoRef(locale) const $locale = useAutoRef(locale);
const $options = useAutoRef(options) const $options = useAutoRef(options);
return computed(() => createDisplayNames($locale.value, $options.value)) return computed(() => createDisplayNames($locale.value, $options.value));
} }

View File

@ -1,20 +1,20 @@
import type { CookieOptions } from '#app' import type { CookieOptions } from "#app";
export type ProjectDisplayMode = 'list' | 'grid' | 'gallery' export type ProjectDisplayMode = "list" | "grid" | "gallery";
export type DarkColorTheme = 'dark' | 'oled' | 'retro' export type DarkColorTheme = "dark" | "oled" | "retro";
export interface NumberFlag { export interface NumberFlag {
min: number min: number;
max: number max: number;
} }
export type BooleanFlag = boolean export type BooleanFlag = boolean;
export type RadioFlag = ProjectDisplayMode | DarkColorTheme export type RadioFlag = ProjectDisplayMode | DarkColorTheme;
export type FlagValue = BooleanFlag /* | NumberFlag | RadioFlag */ export type FlagValue = BooleanFlag; /* | NumberFlag | RadioFlag */
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags;
export const DEFAULT_FEATURE_FLAGS = validateValues({ export const DEFAULT_FEATURE_FLAGS = validateValues({
// Developer flags // Developer flags
@ -48,58 +48,58 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
// dataPackSearchDisplayMode: 'list', // dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: 'list', // userProjectDisplayMode: 'list',
// collectionProjectDisplayMode: 'list', // collectionProjectDisplayMode: 'list',
} as const) } as const);
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS;
export type AllFeatureFlags = { export type AllFeatureFlags = {
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key] [key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key];
} };
export type PartialFeatureFlags = Partial<AllFeatureFlags> export type PartialFeatureFlags = Partial<AllFeatureFlags>;
const COOKIE_OPTIONS = { const COOKIE_OPTIONS = {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
} satisfies CookieOptions<PartialFeatureFlags> } satisfies CookieOptions<PartialFeatureFlags>;
export const useFeatureFlags = () => export const useFeatureFlags = () =>
useState<AllFeatureFlags>('featureFlags', () => { useState<AllFeatureFlags>("featureFlags", () => {
const config = useRuntimeConfig() const config = useRuntimeConfig();
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS) const savedFlags = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
if (!savedFlags.value) { if (!savedFlags.value) {
savedFlags.value = {} savedFlags.value = {};
} }
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS)) const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS));
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags const overrides = config.public.featureFlagOverrides as PartialFeatureFlags;
for (const key in overrides) { for (const key in overrides) {
if (key in flags) { if (key in flags) {
const flag = key as FeatureFlag const flag = key as FeatureFlag;
const value = overrides[flag] as (typeof flags)[FeatureFlag] const value = overrides[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value flags[flag] = value;
} }
} }
for (const key in savedFlags.value) { for (const key in savedFlags.value) {
if (key in flags) { if (key in flags) {
const flag = key as FeatureFlag const flag = key as FeatureFlag;
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag] const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value flags[flag] = value;
} }
} }
return flags return flags;
}) });
export const saveFeatureFlags = () => { export const saveFeatureFlags = () => {
const flags = useFeatureFlags() const flags = useFeatureFlags();
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS) const cookie = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
cookie.value = flags.value cookie.value = flags.value;
} };

View File

@ -1,36 +1,37 @@
/* eslint-disable no-undef */
export const useBaseFetch = async (url, options = {}, skipAuth = false) => { export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig() const config = useRuntimeConfig();
let base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl let base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl;
if (!options.headers) { if (!options.headers) {
options.headers = {} options.headers = {};
} }
if (process.server) { if (process.server) {
options.headers['x-ratelimit-key'] = config.rateLimitKey options.headers["x-ratelimit-key"] = config.rateLimitKey;
} }
if (!skipAuth) { if (!skipAuth) {
const auth = await useAuth() const auth = await useAuth();
options.headers.Authorization = auth.value.token options.headers.Authorization = auth.value.token;
} }
if (options.apiVersion || options.internal) { if (options.apiVersion || options.internal) {
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number // Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
// and keep the trailing slash if it exists // and keep the trailing slash if it exists
const baseVersion = base.match(/\/v\d\//) const baseVersion = base.match(/\/v\d\//);
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/` const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`;
if (baseVersion) { if (baseVersion) {
base = base.replace(baseVersion[0], replaceStr) base = base.replace(baseVersion[0], replaceStr);
} else { } else {
base = base.replace(/\/v\d$/, replaceStr) base = base.replace(/\/v\d$/, replaceStr);
} }
delete options.apiVersion delete options.apiVersion;
} }
return await $fetch(`${base}${url}`, options) return await $fetch(`${base}${url}`, options);
} };

View File

@ -1,18 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/how-ago' import { createFormatter, type Formatter } from "@vintl/how-ago";
import type { IntlController } from '@vintl/vintl/controller' import type { IntlController } from "@vintl/vintl/controller";
const formatters = new WeakMap<IntlController<any>, Formatter>() const formatters = new WeakMap<IntlController<any>, Formatter>();
export function useRelativeTime(): Formatter { export function useRelativeTime(): Formatter {
const vintl = useVIntl() const vintl = useVIntl();
let formatter = formatters.get(vintl) let formatter = formatters.get(vintl);
if (formatter == null) { if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl)) const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options) formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter) formatters.set(vintl, formatter);
} }
return formatter return formatter;
} }

View File

@ -1,46 +1,46 @@
type ImageUploadContext = { type ImageUploadContext = {
projectID?: string projectID?: string;
context: 'project' | 'version' | 'thread_message' | 'report' context: "project" | "version" | "thread_message" | "report";
} };
interface ImageUploadResponse { interface ImageUploadResponse {
id: string id: string;
url: string url: string;
} }
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => { export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp // Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
if ( if (
!file.type.startsWith('image/') || !file.type.startsWith("image/") ||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1]) !["png", "jpeg", "gif", "webp"].includes(file.type.split("/")[1])
) { ) {
throw new Error('File is not an accepted image type') throw new Error("File is not an accepted image type");
} }
// Make sure file is less than 1MB // Make sure file is less than 1MB
if (file.size > 1024 * 1024) { if (file.size > 1024 * 1024) {
throw new Error('File is too large') throw new Error("File is too large");
} }
const qs = new URLSearchParams() const qs = new URLSearchParams();
if (ctx.projectID) qs.set('project_id', ctx.projectID) if (ctx.projectID) qs.set("project_id", ctx.projectID);
qs.set('context', ctx.context) qs.set("context", ctx.context);
qs.set('ext', file.type.split('/')[1]) qs.set("ext", file.type.split("/")[1]);
const url = `image?${qs.toString()}` const url = `image?${qs.toString()}`;
const response = (await useBaseFetch(url, { const response = (await useBaseFetch(url, {
method: 'POST', method: "POST",
body: file, body: file,
apiVersion: 3, apiVersion: 3,
})) as ImageUploadResponse })) as ImageUploadResponse;
// Type check to see if response has a url property and an id property // Type check to see if response has a url property and an id property
if (!response?.id || typeof response.id !== 'string') { if (!response?.id || typeof response.id !== "string") {
throw new Error('Unexpected response from server') throw new Error("Unexpected response from server");
} }
if (!response?.url || typeof response.url !== 'string') { if (!response?.url || typeof response.url !== "string") {
throw new Error('Unexpected response from server') throw new Error("Unexpected response from server");
} }
return response return response;
} };

View File

@ -1,13 +1,14 @@
export const useLoading = () => useState('loading', () => false) /* eslint-disable no-undef */
export const useLoading = () => useState("loading", () => false);
export const startLoading = () => { export const startLoading = () => {
const loading = useLoading() const loading = useLoading();
loading.value = true loading.value = true;
} };
export const stopLoading = () => { export const stopLoading = () => {
const loading = useLoading() const loading = useLoading();
loading.value = false loading.value = false;
} };

View File

@ -1,34 +1,37 @@
export const useNotifications = () => useState('notifications', () => []) /* eslint-disable no-undef */
export const useNotifications = () => useState("notifications", () => []);
export const addNotification = (notification) => { export const addNotification = (notification) => {
const notifications = useNotifications() const notifications = useNotifications();
const existingNotif = notifications.value.find( const existingNotif = notifications.value.find(
(x) => (x) =>
x.text === notification.text && x.title === notification.title && x.type === notification.type x.text === notification.text &&
) x.title === notification.title &&
x.type === notification.type,
);
if (existingNotif) { if (existingNotif) {
setNotificationTimer(existingNotif) setNotificationTimer(existingNotif);
return return;
} }
notification.id = new Date() notification.id = new Date();
setNotificationTimer(notification) setNotificationTimer(notification);
notifications.value.push(notification) notifications.value.push(notification);
} };
export const setNotificationTimer = (notification) => { export const setNotificationTimer = (notification) => {
if (!notification) return if (!notification) return;
const notifications = useNotifications() const notifications = useNotifications();
if (notification.timer) { if (notification.timer) {
clearTimeout(notification.timer) clearTimeout(notification.timer);
} }
notification.timer = setTimeout(() => { notification.timer = setTimeout(() => {
notifications.value.splice(notifications.value.indexOf(notification), 1) notifications.value.splice(notifications.value.indexOf(notification), 1);
}, 30000) }, 30000);
} };

View File

@ -1 +1 @@
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from 'vue-router' export { useRoute as useNativeRoute, useRouter as useNativeRouter } from "vue-router";

View File

@ -1,7 +1,7 @@
export const getArrayOrString = (x) => { export const getArrayOrString = (x) => {
if (typeof x === 'string' || x instanceof String) { if (typeof x === "string" || x instanceof String) {
return [x] return [x];
} else { } else {
return x return x;
}
} }
};

View File

@ -1,10 +1,11 @@
/* eslint-disable no-undef */
/** /**
* Extracts the [id] from the route params and returns it as a ref. * Extracts the [id] from the route params and returns it as a ref.
* *
* @param {string?} key The key of the route param to extract. * @param {string?} key The key of the route param to extract.
* @returns {import('vue').Ref<string | string[] | undefined>} * @returns {import('vue').Ref<string | string[] | undefined>}
*/ */
export const useRouteId = (key = 'id') => { export const useRouteId = (key = "id") => {
const route = useNativeRoute() const route = useNativeRoute();
return route.params?.[key] || undefined return route.params?.[key] || undefined;
} };

View File

@ -1,7 +1,8 @@
import tags from '~/generated/state.json' /* eslint-disable no-undef */
import tags from "~/generated/state.json";
export const useTags = () => export const useTags = () =>
useState('tags', () => ({ useState("tags", () => ({
categories: tags.categories, categories: tags.categories,
loaders: tags.loaders, loaders: tags.loaders,
gameVersions: tags.gameVersions, gameVersions: tags.gameVersions,
@ -9,56 +10,56 @@ export const useTags = () =>
reportTypes: tags.reportTypes, reportTypes: tags.reportTypes,
projectTypes: [ projectTypes: [
{ {
actual: 'mod', actual: "mod",
id: 'mod', id: "mod",
display: 'mod', display: "mod",
}, },
{ {
actual: 'mod', actual: "mod",
id: 'plugin', id: "plugin",
display: 'plugin', display: "plugin",
}, },
{ {
actual: 'mod', actual: "mod",
id: 'datapack', id: "datapack",
display: 'data pack', display: "data pack",
}, },
{ {
actual: 'shader', actual: "shader",
id: 'shader', id: "shader",
display: 'shader', display: "shader",
}, },
{ {
actual: 'resourcepack', actual: "resourcepack",
id: 'resourcepack', id: "resourcepack",
display: 'resource pack', display: "resource pack",
}, },
{ {
actual: 'modpack', actual: "modpack",
id: 'modpack', id: "modpack",
display: 'modpack', display: "modpack",
}, },
], ],
loaderData: { loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'], pluginLoaders: ["bukkit", "spigot", "paper", "purpur", "sponge", "folia"],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'], pluginPlatformLoaders: ["bungeecord", "waterfall", "velocity"],
allPluginLoaders: [ allPluginLoaders: [
'bukkit', "bukkit",
'spigot', "spigot",
'paper', "paper",
'purpur', "purpur",
'sponge', "sponge",
'bungeecord', "bungeecord",
'waterfall', "waterfall",
'velocity', "velocity",
'folia', "folia",
], ],
dataPackLoaders: ['datapack'], dataPackLoaders: ["datapack"],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'], modLoaders: ["forge", "fabric", "quilt", "liteloader", "modloader", "rift", "neoforge"],
hiddenModLoaders: ['liteloader', 'modloader', 'rift'], hiddenModLoaders: ["liteloader", "modloader", "rift"],
}, },
projectViewModes: ['list', 'grid', 'gallery'], projectViewModes: ["list", "grid", "gallery"],
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'], approvedStatuses: ["approved", "archived", "unlisted", "private"],
rejectedStatuses: ['rejected', 'withheld'], rejectedStatuses: ["rejected", "withheld"],
staffRoles: ['moderator', 'admin'], staffRoles: ["moderator", "admin"],
})) }));

View File

@ -1,58 +1,59 @@
/* eslint-disable no-undef */
export const useTheme = () => export const useTheme = () =>
useState('theme', () => { useState("theme", () => {
const colorMode = useCookie('color-mode', { const colorMode = useCookie("color-mode", {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
}) });
if (!colorMode.value) { if (!colorMode.value) {
colorMode.value = { colorMode.value = {
value: 'dark', value: "dark",
preference: 'system', preference: "system",
} };
} }
if (colorMode.value.preference !== 'system') { if (colorMode.value.preference !== "system") {
colorMode.value.value = colorMode.value.preference colorMode.value.value = colorMode.value.preference;
} }
return colorMode.value return colorMode.value;
}) });
export const updateTheme = (value, updatePreference = false) => { export const updateTheme = (value, updatePreference = false) => {
const theme = useTheme() const theme = useTheme();
const cosmetics = useCosmetics() const cosmetics = useCosmetics();
const themeCookie = useCookie('color-mode', { const themeCookie = useCookie("color-mode", {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax', sameSite: "lax",
secure: true, secure: true,
httpOnly: false, httpOnly: false,
path: '/', path: "/",
}) });
if (value === 'system') { if (value === "system") {
theme.value.preference = 'system' theme.value.preference = "system";
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: light)') const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: light)");
if (colorSchemeQueryList.matches) { if (colorSchemeQueryList.matches) {
theme.value.value = 'light' theme.value.value = "light";
} else { } else {
theme.value.value = cosmetics.value.preferredDarkTheme theme.value.value = cosmetics.value.preferredDarkTheme;
} }
} else { } else {
theme.value.value = value theme.value.value = value;
if (updatePreference) theme.value.preference = value if (updatePreference) theme.value.preference = value;
} }
if (process.client) { if (process.client) {
document.documentElement.className = `${theme.value.value}-mode` document.documentElement.className = `${theme.value.value}-mode`;
} }
themeCookie.value = theme.value themeCookie.value = theme.value;
} };
export const DARK_THEMES = ['dark', 'oled', 'retro'] export const DARK_THEMES = ["dark", "oled", "retro"];

View File

@ -1,36 +1,36 @@
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult> type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
type ErrorFunction = (err: any) => void | Promise<void> type ErrorFunction = (err: any) => void | Promise<void>;
type VoidFunction = () => void | Promise<void> type VoidFunction = () => void | Promise<void>;
type useClientTry = <TArgs extends any[], TResult>( type useClientTry = <TArgs extends any[], TResult>(
fn: AsyncFunction<TArgs, TResult>, fn: AsyncFunction<TArgs, TResult>,
onFail?: ErrorFunction, onFail?: ErrorFunction,
onFinish?: VoidFunction onFinish?: VoidFunction,
) => (...args: TArgs) => Promise<TResult | undefined> ) => (...args: TArgs) => Promise<TResult | undefined>;
const defaultOnError: ErrorFunction = (error) => { const defaultOnError: ErrorFunction = (error) => {
addNotification({ addNotification({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: error?.data?.description || error.message || error || 'Unknown error', text: error?.data?.description || error.message || error || "Unknown error",
type: 'error', type: "error",
}) });
} };
export const useClientTry: useClientTry = export const useClientTry: useClientTry =
(fn, onFail = defaultOnError, onFinish) => (fn, onFail = defaultOnError, onFinish) =>
async (...args) => { async (...args) => {
startLoading() startLoading();
try { try {
return await fn(...args) return await fn(...args);
} catch (err) { } catch (err) {
if (onFail) { if (onFail) {
await onFail(err) await onFail(err);
} else { } else {
console.error('[CLIENT TRY ERROR]', err) console.error("[CLIENT TRY ERROR]", err);
} }
} finally { } finally {
if (onFinish) await onFinish() if (onFinish) await onFinish();
stopLoading() stopLoading();
}
} }
};

View File

@ -1,173 +1,176 @@
/* eslint-disable no-undef */
export const useUser = async (force = false) => { export const useUser = async (force = false) => {
const user = useState('user', () => {}) const user = useState("user", () => {});
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) { if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
user.value = await initUser() user.value = await initUser();
} }
return user return user;
} };
export const initUser = async () => { export const initUser = async () => {
const auth = (await useAuth()).value const auth = (await useAuth()).value;
const user = { const user = {
notifications: [], notifications: [],
follows: [], follows: [],
lastUpdated: 0, lastUpdated: 0,
} };
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
const [follows, collections] = await Promise.all([ const [follows, collections] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`), useBaseFetch(`user/${auth.user.id}/follows`),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 }), useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 }),
]) ]);
user.collections = collections user.collections = collections;
user.follows = follows user.follows = follows;
user.lastUpdated = Date.now() user.lastUpdated = Date.now();
} catch (err) { } catch (err) {
console.error(err) console.error(err);
} }
} }
return user return user;
} };
export const initUserCollections = async () => { export const initUserCollections = async () => {
const auth = (await useAuth()).value const auth = (await useAuth()).value;
const user = (await useUser()).value const user = (await useUser()).value;
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 }) user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 });
} catch (err) { } catch (err) {
console.error(err) console.error(err);
}
} }
} }
};
export const initUserFollows = async () => { export const initUserFollows = async () => {
const auth = (await useAuth()).value const auth = (await useAuth()).value;
const user = (await useUser()).value const user = (await useUser()).value;
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`) user.follows = await useBaseFetch(`user/${auth.user.id}/follows`);
} catch (err) { } catch (err) {
console.error(err) console.error(err);
}
} }
} }
};
export const initUserProjects = async () => { export const initUserProjects = async () => {
const auth = (await useAuth()).value const auth = (await useAuth()).value;
const user = (await useUser()).value const user = (await useUser()).value;
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`) user.projects = await useBaseFetch(`user/${auth.user.id}/projects`);
} catch (err) { } catch (err) {
console.error(err) console.error(err);
}
} }
} }
};
export const userCollectProject = async (collection, projectId) => { export const userCollectProject = async (collection, projectId) => {
const user = (await useUser()).value const user = (await useUser()).value;
await initUserCollections() await initUserCollections();
const collectionId = collection.id const collectionId = collection.id;
const latestCollection = user.collections.find((x) => x.id === collectionId) const latestCollection = user.collections.find((x) => x.id === collectionId);
if (!latestCollection) { if (!latestCollection) {
throw new Error('This collection was not found. Has it been deleted?') throw new Error("This collection was not found. Has it been deleted?");
} }
const add = !latestCollection.projects.includes(projectId) const add = !latestCollection.projects.includes(projectId);
const projects = add const projects = add
? [...latestCollection.projects, projectId] ? [...latestCollection.projects, projectId]
: [...latestCollection.projects].filter((x) => x !== projectId) : [...latestCollection.projects].filter((x) => x !== projectId);
const idx = user.collections.findIndex((x) => x.id === latestCollection.id) const idx = user.collections.findIndex((x) => x.id === latestCollection.id);
if (idx >= 0) { if (idx >= 0) {
user.collections[idx].projects = projects user.collections[idx].projects = projects;
} }
await useBaseFetch(`collection/${collection.id}`, { await useBaseFetch(`collection/${collection.id}`, {
method: 'PATCH', method: "PATCH",
body: { body: {
new_projects: projects, new_projects: projects,
}, },
apiVersion: 3, apiVersion: 3,
}) });
} };
export const userFollowProject = async (project) => { export const userFollowProject = async (project) => {
const user = (await useUser()).value const user = (await useUser()).value;
user.follows = user.follows.concat(project) user.follows = user.follows.concat(project);
project.followers++ project.followers++;
setTimeout(() => { setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, { useBaseFetch(`project/${project.id}/follow`, {
method: 'POST', method: "POST",
}) });
}) });
} };
export const userUnfollowProject = async (project) => { export const userUnfollowProject = async (project) => {
const user = (await useUser()).value const user = (await useUser()).value;
user.follows = user.follows.filter((x) => x.id !== project.id) user.follows = user.follows.filter((x) => x.id !== project.id);
project.followers-- project.followers--;
setTimeout(() => { setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, { useBaseFetch(`project/${project.id}/follow`, {
method: 'DELETE', method: "DELETE",
}) });
}) });
} };
export const resendVerifyEmail = async () => { export const resendVerifyEmail = async () => {
const app = useNuxtApp() const app = useNuxtApp();
startLoading() startLoading();
try { try {
await useBaseFetch('auth/email/resend_verify', { await useBaseFetch("auth/email/resend_verify", {
method: 'POST', method: "POST",
}) });
const auth = await useAuth() const auth = await useAuth();
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Email sent', title: "Email sent",
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`, text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
type: 'success', type: "success",
}) });
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'An error occurred', title: "An error occurred",
text: err.data.description, text: err.data.description,
type: 'error', type: "error",
}) });
}
stopLoading()
} }
stopLoading();
};
export const logout = async () => { export const logout = async () => {
startLoading() startLoading();
const auth = await useAuth() const auth = await useAuth();
try { try {
await useBaseFetch(`session/${auth.value.token}`, { await useBaseFetch(`session/${auth.value.token}`, {
method: 'DELETE', method: "DELETE",
}) });
} catch {} } catch {
/* empty */
await useAuth('none')
useCookie('auth-token').value = null
await navigateTo('/')
stopLoading()
} }
await useAuth("none");
useCookie("auth-token").value = null;
await navigateTo("/");
stopLoading();
};

View File

@ -23,7 +23,7 @@
</template> </template>
<script setup> <script setup>
import Logo404 from '~/assets/images/404.svg' import Logo404 from "~/assets/images/404.svg";
defineProps({ defineProps({
error: { error: {
@ -31,11 +31,11 @@ defineProps({
default() { default() {
return { return {
statusCode: 1000, statusCode: 1000,
message: 'Unknown error', message: "Unknown error",
} };
}, },
}, },
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
* @returns Whether any of the modifier keys is pressed. * @returns Whether any of the modifier keys is pressed.
*/ */
export function isModifierKeyDown( export function isModifierKeyDown(
e: Pick<KeyboardEvent, 'ctrlKey' | 'altKey' | 'metaKey' | 'shiftKey'> e: Pick<KeyboardEvent, "ctrlKey" | "altKey" | "metaKey" | "shiftKey">,
) { ) {
return e.ctrlKey || e.altKey || e.metaKey || e.shiftKey return e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
} }

View File

@ -1,32 +1,32 @@
import { formatBytes } from '~/plugins/shorthands.js' import { formatBytes } from "~/plugins/shorthands.js";
export const fileIsValid = (file, validationOptions) => { export const fileIsValid = (file, validationOptions) => {
const { maxSize, alertOnInvalid } = validationOptions const { maxSize, alertOnInvalid } = validationOptions;
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) { if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
if (alertOnInvalid) { if (alertOnInvalid) {
alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`) alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`);
} }
return false return false;
} }
return true return true;
} };
export const acceptFileFromProjectType = (projectType) => { export const acceptFileFromProjectType = (projectType) => {
switch (projectType) { switch (projectType) {
case 'mod': case "mod":
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip' return ".jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip";
case 'plugin': case "plugin":
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip' return ".jar,.zip,application/java-archive,application/x-java-archive,application/zip";
case 'resourcepack': case "resourcepack":
return '.zip,application/zip' return ".zip,application/zip";
case 'shader': case "shader":
return '.zip,application/zip' return ".zip,application/zip";
case 'datapack': case "datapack":
return '.zip,application/zip' return ".zip,application/zip";
case 'modpack': case "modpack":
return '.mrpack,application/x-modrinth-modpack+zip,application/zip' return ".mrpack,application/x-modrinth-modpack+zip,application/zip";
default: default:
return '*' return "*";
}
} }
};

View File

@ -1,51 +1,51 @@
import hljs from 'highlight.js/lib/core' import hljs from "highlight.js/lib/core";
// Scripting // Scripting
import javascript from 'highlight.js/lib/languages/javascript' import javascript from "highlight.js/lib/languages/javascript";
import python from 'highlight.js/lib/languages/python' import python from "highlight.js/lib/languages/python";
import lua from 'highlight.js/lib/languages/lua' import lua from "highlight.js/lib/languages/lua";
// Coding // Coding
import java from 'highlight.js/lib/languages/java' import java from "highlight.js/lib/languages/java";
import kotlin from 'highlight.js/lib/languages/kotlin' import kotlin from "highlight.js/lib/languages/kotlin";
import scala from 'highlight.js/lib/languages/scala' import scala from "highlight.js/lib/languages/scala";
import groovy from 'highlight.js/lib/languages/groovy' import groovy from "highlight.js/lib/languages/groovy";
// Configs // Configs
import gradle from 'highlight.js/lib/languages/gradle' import gradle from "highlight.js/lib/languages/gradle";
import json from 'highlight.js/lib/languages/json' import json from "highlight.js/lib/languages/json";
import ini from 'highlight.js/lib/languages/ini' import ini from "highlight.js/lib/languages/ini";
import yaml from 'highlight.js/lib/languages/yaml' import yaml from "highlight.js/lib/languages/yaml";
import xml from 'highlight.js/lib/languages/xml' import xml from "highlight.js/lib/languages/xml";
import properties from 'highlight.js/lib/languages/properties' import properties from "highlight.js/lib/languages/properties";
import { md, configuredXss } from '@modrinth/utils' import { md, configuredXss } from "@modrinth/utils";
/* REGISTRATION */ /* REGISTRATION */
// Scripting // Scripting
hljs.registerLanguage('javascript', javascript) hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage('python', python) hljs.registerLanguage("python", python);
hljs.registerLanguage('lua', lua) hljs.registerLanguage("lua", lua);
// Coding // Coding
hljs.registerLanguage('java', java) hljs.registerLanguage("java", java);
hljs.registerLanguage('kotlin', kotlin) hljs.registerLanguage("kotlin", kotlin);
hljs.registerLanguage('scala', scala) hljs.registerLanguage("scala", scala);
hljs.registerLanguage('groovy', groovy) hljs.registerLanguage("groovy", groovy);
// Configs // Configs
hljs.registerLanguage('gradle', gradle) hljs.registerLanguage("gradle", gradle);
hljs.registerLanguage('json', json) hljs.registerLanguage("json", json);
hljs.registerLanguage('ini', ini) hljs.registerLanguage("ini", ini);
hljs.registerLanguage('yaml', yaml) hljs.registerLanguage("yaml", yaml);
hljs.registerLanguage('xml', xml) hljs.registerLanguage("xml", xml);
hljs.registerLanguage('properties', properties) hljs.registerLanguage("properties", properties);
/* ALIASES */ /* ALIASES */
// Scripting // Scripting
hljs.registerAliases(['js'], { languageName: 'javascript' }) hljs.registerAliases(["js"], { languageName: "javascript" });
hljs.registerAliases(['py'], { languageName: 'python' }) hljs.registerAliases(["py"], { languageName: "python" });
// Coding // Coding
hljs.registerAliases(['kt'], { languageName: 'kotlin' }) hljs.registerAliases(["kt"], { languageName: "kotlin" });
// Configs // Configs
hljs.registerAliases(['json5'], { languageName: 'json' }) hljs.registerAliases(["json5"], { languageName: "json" });
hljs.registerAliases(['toml'], { languageName: 'ini' }) hljs.registerAliases(["toml"], { languageName: "ini" });
hljs.registerAliases(['yml'], { languageName: 'yaml' }) hljs.registerAliases(["yml"], { languageName: "yaml" });
hljs.registerAliases(['html', 'htm', 'xhtml', 'mcui', 'fxml'], { languageName: 'xml' }) hljs.registerAliases(["html", "htm", "xhtml", "mcui", "fxml"], { languageName: "xml" });
export const renderHighlightedString = (string) => export const renderHighlightedString = (string) =>
configuredXss.process( configuredXss.process(
@ -53,11 +53,13 @@ export const renderHighlightedString = (string) =>
highlight: function (str, lang) { highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs.highlight(str, { language: lang }).value return hljs.highlight(str, { language: lang }).value;
} catch (__) {} } catch (__) {
/* empty */
}
} }
return '' return "";
}, },
}).render(string) }).render(string),
) );

View File

@ -1,154 +1,154 @@
import TOML from '@ltd/j-toml' import TOML from "@ltd/j-toml";
import JSZip from 'jszip' import JSZip from "jszip";
import yaml from 'js-yaml' import yaml from "js-yaml";
import { satisfies } from 'semver' import { satisfies } from "semver";
export const inferVersionInfo = async function (rawFile, project, gameVersions) { export const inferVersionInfo = async function (rawFile, project, gameVersions) {
function versionType(number) { function versionType(number) {
if (number.includes('alpha')) { if (number.includes("alpha")) {
return 'alpha' return "alpha";
} else if ( } else if (
number.includes('beta') || number.includes("beta") ||
number.match(/[^A-z](rc)[^A-z]/) || // includes `rc` number.match(/[^A-z](rc)[^A-z]/) || // includes `rc`
number.match(/[^A-z](pre)[^A-z]/) // includes `pre` number.match(/[^A-z](pre)[^A-z]/) // includes `pre`
) { ) {
return 'beta' return "beta";
} else { } else {
return 'release' return "release";
} }
} }
function getGameVersionsMatchingSemverRange(range, gameVersions) { function getGameVersionsMatchingSemverRange(range, gameVersions) {
if (!range) { if (!range) {
return [] return [];
} }
const ranges = Array.isArray(range) ? range : [range] const ranges = Array.isArray(range) ? range : [range];
return gameVersions.filter((version) => { return gameVersions.filter((version) => {
const semverVersion = version.split('.').length === 2 ? `${version}.0` : version // add patch version if missing (e.g. 1.16 -> 1.16.0) const semverVersion = version.split(".").length === 2 ? `${version}.0` : version; // add patch version if missing (e.g. 1.16 -> 1.16.0)
return ranges.some((v) => satisfies(semverVersion, v)) return ranges.some((v) => satisfies(semverVersion, v));
}) });
} }
function getGameVersionsMatchingMavenRange(range, gameVersions) { function getGameVersionsMatchingMavenRange(range, gameVersions) {
if (!range) { if (!range) {
return [] return [];
} }
const ranges = [] const ranges = [];
while (range.startsWith('[') || range.startsWith('(')) { while (range.startsWith("[") || range.startsWith("(")) {
let index = range.indexOf(')') let index = range.indexOf(")");
const index2 = range.indexOf(']') const index2 = range.indexOf("]");
if (index === -1 || (index2 !== -1 && index2 < index)) { if (index === -1 || (index2 !== -1 && index2 < index)) {
index = index2 index = index2;
} }
if (index === -1) break if (index === -1) break;
ranges.push(range.substring(0, index + 1)) ranges.push(range.substring(0, index + 1));
range = range.substring(index + 1).trim() range = range.substring(index + 1).trim();
if (range.startsWith(',')) { if (range.startsWith(",")) {
range = range.substring(1).trim() range = range.substring(1).trim();
} }
} }
if (range) { if (range) {
ranges.push(range) ranges.push(range);
} }
const LESS_THAN_EQUAL = /^\(,(.*)]$/ const LESS_THAN_EQUAL = /^\(,(.*)]$/;
const LESS_THAN = /^\(,(.*)\)$/ const LESS_THAN = /^\(,(.*)\)$/;
const EQUAL = /^\[(.*)]$/ const EQUAL = /^\[(.*)]$/;
const GREATER_THAN_EQUAL = /^\[(.*),\)$/ const GREATER_THAN_EQUAL = /^\[(.*),\)$/;
const GREATER_THAN = /^\((.*),\)$/ const GREATER_THAN = /^\((.*),\)$/;
const BETWEEN = /^\((.*),(.*)\)$/ const BETWEEN = /^\((.*),(.*)\)$/;
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/ const BETWEEN_EQUAL = /^\[(.*),(.*)]$/;
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/ const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/;
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/ const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/;
const semverRanges = [] const semverRanges = [];
for (const range of ranges) { for (const range of ranges) {
let result let result;
if ((result = range.match(LESS_THAN_EQUAL))) { if ((result = range.match(LESS_THAN_EQUAL))) {
semverRanges.push(`<=${result[1]}`) semverRanges.push(`<=${result[1]}`);
} else if ((result = range.match(LESS_THAN))) { } else if ((result = range.match(LESS_THAN))) {
semverRanges.push(`<${result[1]}`) semverRanges.push(`<${result[1]}`);
} else if ((result = range.match(EQUAL))) { } else if ((result = range.match(EQUAL))) {
semverRanges.push(`${result[1]}`) semverRanges.push(`${result[1]}`);
} else if ((result = range.match(GREATER_THAN_EQUAL))) { } else if ((result = range.match(GREATER_THAN_EQUAL))) {
semverRanges.push(`>=${result[1]}`) semverRanges.push(`>=${result[1]}`);
} else if ((result = range.match(GREATER_THAN))) { } else if ((result = range.match(GREATER_THAN))) {
semverRanges.push(`>${result[1]}`) semverRanges.push(`>${result[1]}`);
} else if ((result = range.match(BETWEEN))) { } else if ((result = range.match(BETWEEN))) {
semverRanges.push(`>${result[1]} <${result[2]}`) semverRanges.push(`>${result[1]} <${result[2]}`);
} else if ((result = range.match(BETWEEN_EQUAL))) { } else if ((result = range.match(BETWEEN_EQUAL))) {
semverRanges.push(`>=${result[1]} <=${result[2]}`) semverRanges.push(`>=${result[1]} <=${result[2]}`);
} else if ((result = range.match(BETWEEN_LESS_THAN_EQUAL))) { } else if ((result = range.match(BETWEEN_LESS_THAN_EQUAL))) {
semverRanges.push(`>${result[1]} <=${result[2]}`) semverRanges.push(`>${result[1]} <=${result[2]}`);
} else if ((result = range.match(BETWEEN_GREATER_THAN_EQUAL))) { } else if ((result = range.match(BETWEEN_GREATER_THAN_EQUAL))) {
semverRanges.push(`>=${result[1]} <${result[2]}`) semverRanges.push(`>=${result[1]} <${result[2]}`);
} }
} }
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions) return getGameVersionsMatchingSemverRange(semverRanges, gameVersions);
} }
const simplifiedGameVersions = gameVersions const simplifiedGameVersions = gameVersions
.filter((it) => it.version_type === 'release') .filter((it) => it.version_type === "release")
.map((it) => it.version) .map((it) => it.version);
const inferFunctions = { const inferFunctions = {
// Forge 1.13+ and NeoForge // Forge 1.13+ and NeoForge
'META-INF/mods.toml': async (file, zip) => { "META-INF/mods.toml": async (file, zip) => {
const metadata = TOML.parse(file, { joiner: '\n' }) const metadata = TOML.parse(file, { joiner: "\n" });
if (metadata.mods && metadata.mods.length > 0) { if (metadata.mods && metadata.mods.length > 0) {
let versionNum = metadata.mods[0].version let versionNum = metadata.mods[0].version;
// ${file.jarVersion} -> Implementation-Version from manifest // ${file.jarVersion} -> Implementation-Version from manifest
const manifestFile = zip.file('META-INF/MANIFEST.MF') const manifestFile = zip.file("META-INF/MANIFEST.MF");
if ( if (
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
metadata.mods[0].version.includes('${file.jarVersion}') && metadata.mods[0].version.includes("${file.jarVersion}") &&
manifestFile !== null manifestFile !== null
) { ) {
const manifestText = await manifestFile.async('text') const manifestText = await manifestFile.async("text");
const regex = /Implementation-Version: (.*)$/m const regex = /Implementation-Version: (.*)$/m;
const match = manifestText.match(regex) const match = manifestText.match(regex);
if (match) { if (match) {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
versionNum = versionNum.replace('${file.jarVersion}', match[1]) versionNum = versionNum.replace("${file.jarVersion}", match[1]);
} }
} }
let gameVersions = [] let gameVersions = [];
const mcDependencies = Object.values(metadata.dependencies) const mcDependencies = Object.values(metadata.dependencies)
.flat() .flat()
.filter((dependency) => dependency.modId === 'minecraft') .filter((dependency) => dependency.modId === "minecraft");
if (mcDependencies.length > 0) { if (mcDependencies.length > 0) {
gameVersions = getGameVersionsMatchingMavenRange( gameVersions = getGameVersionsMatchingMavenRange(
mcDependencies[0].versionRange, mcDependencies[0].versionRange,
simplifiedGameVersions simplifiedGameVersions,
) );
} }
const hasNeoForge = const hasNeoForge =
Object.values(metadata.dependencies) Object.values(metadata.dependencies)
.flat() .flat()
.filter((dependency) => dependency.modId === 'neoforge').length > 0 .filter((dependency) => dependency.modId === "neoforge").length > 0;
const hasForge = const hasForge =
Object.values(metadata.dependencies) Object.values(metadata.dependencies)
.flat() .flat()
.filter((dependency) => dependency.modId === 'forge').length > 0 .filter((dependency) => dependency.modId === "forge").length > 0;
// Checks if game version is below 1.20.2 as NeoForge full split and id change was in 1.20.2 // Checks if game version is below 1.20.2 as NeoForge full split and id change was in 1.20.2
const below1202 = getGameVersionsMatchingSemverRange('<=1.20.1', simplifiedGameVersions) const below1202 = getGameVersionsMatchingSemverRange("<=1.20.1", simplifiedGameVersions);
const isOlderThan1202 = below1202.some((r) => gameVersions.includes(r)) const isOlderThan1202 = below1202.some((r) => gameVersions.includes(r));
const loaders = [] const loaders = [];
if (hasNeoForge) loaders.push('neoforge') if (hasNeoForge) loaders.push("neoforge");
if (hasForge || isOlderThan1202) loaders.push('forge') if (hasForge || isOlderThan1202) loaders.push("forge");
return { return {
name: `${project.title} ${versionNum}`, name: `${project.title} ${versionNum}`,
@ -156,61 +156,61 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
version_type: versionType(versionNum), version_type: versionType(versionNum),
loaders, loaders,
game_versions: gameVersions, game_versions: gameVersions,
} };
} else { } else {
return {} return {};
} }
}, },
// Old Forge // Old Forge
'mcmod.info': (file) => { "mcmod.info": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
return { return {
name: metadata.version ? `${project.title} ${metadata.version}` : '', name: metadata.version ? `${project.title} ${metadata.version}` : "",
version_number: metadata.version, version_number: metadata.version,
version_type: versionType(metadata.version), version_type: versionType(metadata.version),
loaders: ['forge'], loaders: ["forge"],
game_versions: simplifiedGameVersions.filter((version) => game_versions: simplifiedGameVersions.filter((version) =>
version.startsWith(metadata.mcversion) version.startsWith(metadata.mcversion),
), ),
} };
}, },
// Fabric // Fabric
'fabric.mod.json': (file) => { "fabric.mod.json": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
return { return {
name: `${project.title} ${metadata.version}`, name: `${project.title} ${metadata.version}`,
version_number: metadata.version, version_number: metadata.version,
loaders: ['fabric'], loaders: ["fabric"],
version_type: versionType(metadata.version), version_type: versionType(metadata.version),
game_versions: metadata.depends game_versions: metadata.depends
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions) ? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
: [], : [],
} };
}, },
// Quilt // Quilt
'quilt.mod.json': (file) => { "quilt.mod.json": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
return { return {
name: `${project.title} ${metadata.quilt_loader.version}`, name: `${project.title} ${metadata.quilt_loader.version}`,
version_number: metadata.quilt_loader.version, version_number: metadata.quilt_loader.version,
loaders: ['quilt'], loaders: ["quilt"],
version_type: versionType(metadata.quilt_loader.version), version_type: versionType(metadata.quilt_loader.version),
game_versions: metadata.quilt_loader.depends game_versions: metadata.quilt_loader.depends
? getGameVersionsMatchingSemverRange( ? getGameVersionsMatchingSemverRange(
metadata.quilt_loader.depends.find((x) => x.id === 'minecraft') metadata.quilt_loader.depends.find((x) => x.id === "minecraft")
? metadata.quilt_loader.depends.find((x) => x.id === 'minecraft').versions ? metadata.quilt_loader.depends.find((x) => x.id === "minecraft").versions
: [], : [],
simplifiedGameVersions simplifiedGameVersions,
) )
: [], : [],
} };
}, },
// Bukkit + Other Forks // Bukkit + Other Forks
'plugin.yml': (file) => { "plugin.yml": (file) => {
const metadata = yaml.load(file) const metadata = yaml.load(file);
return { return {
name: `${project.title} ${metadata.version}`, name: `${project.title} ${metadata.version}`,
@ -220,65 +220,65 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
loaders: [], loaders: [],
game_versions: gameVersions game_versions: gameVersions
.filter( .filter(
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release' (x) => x.version.startsWith(metadata["api-version"]) && x.version_type === "release",
) )
.map((x) => x.version), .map((x) => x.version),
} };
}, },
// Paper 1.19.3+ // Paper 1.19.3+
'paper-plugin.yml': (file) => { "paper-plugin.yml": (file) => {
const metadata = yaml.load(file) const metadata = yaml.load(file);
return { return {
name: `${project.title} ${metadata.version}`, name: `${project.title} ${metadata.version}`,
version_number: metadata.version, version_number: metadata.version,
version_type: versionType(metadata.version), version_type: versionType(metadata.version),
loaders: ['paper'], loaders: ["paper"],
game_versions: gameVersions game_versions: gameVersions
.filter( .filter(
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release' (x) => x.version.startsWith(metadata["api-version"]) && x.version_type === "release",
) )
.map((x) => x.version), .map((x) => x.version),
} };
}, },
// Bungeecord + Waterfall // Bungeecord + Waterfall
'bungee.yml': (file) => { "bungee.yml": (file) => {
const metadata = yaml.load(file) const metadata = yaml.load(file);
return { return {
name: `${project.title} ${metadata.version}`, name: `${project.title} ${metadata.version}`,
version_number: metadata.version, version_number: metadata.version,
version_type: versionType(metadata.version), version_type: versionType(metadata.version),
loaders: ['bungeecord'], loaders: ["bungeecord"],
} };
}, },
// Velocity // Velocity
'velocity-plugin.json': (file) => { "velocity-plugin.json": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
return { return {
name: `${project.title} ${metadata.version}`, name: `${project.title} ${metadata.version}`,
version_number: metadata.version, version_number: metadata.version,
version_type: versionType(metadata.version), version_type: versionType(metadata.version),
loaders: ['velocity'], loaders: ["velocity"],
} };
}, },
// Modpacks // Modpacks
'modrinth.index.json': (file) => { "modrinth.index.json": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
const loaders = [] const loaders = [];
if ('forge' in metadata.dependencies) { if ("forge" in metadata.dependencies) {
loaders.push('forge') loaders.push("forge");
} }
if ('neoforge' in metadata.dependencies) { if ("neoforge" in metadata.dependencies) {
loaders.push('neoforge') loaders.push("neoforge");
} }
if ('fabric-loader' in metadata.dependencies) { if ("fabric-loader" in metadata.dependencies) {
loaders.push('fabric') loaders.push("fabric");
} }
if ('quilt-loader' in metadata.dependencies) { if ("quilt-loader" in metadata.dependencies) {
loaders.push('quilt') loaders.push("quilt");
} }
return { return {
@ -289,106 +289,106 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
game_versions: gameVersions game_versions: gameVersions
.filter((x) => x.version === metadata.dependencies.minecraft) .filter((x) => x.version === metadata.dependencies.minecraft)
.map((x) => x.version), .map((x) => x.version),
} };
}, },
// Resource Packs + Data Packs // Resource Packs + Data Packs
'pack.mcmeta': (file) => { "pack.mcmeta": (file) => {
const metadata = JSON.parse(file) const metadata = JSON.parse(file);
function getRange(versionA, versionB) { function getRange(versionA, versionB) {
const startingIndex = gameVersions.findIndex((x) => x.version === versionA) const startingIndex = gameVersions.findIndex((x) => x.version === versionA);
const endingIndex = gameVersions.findIndex((x) => x.version === versionB) const endingIndex = gameVersions.findIndex((x) => x.version === versionB);
const final = [] const final = [];
const filterOnlyRelease = gameVersions[startingIndex].version_type === 'release' const filterOnlyRelease = gameVersions[startingIndex].version_type === "release";
for (let i = startingIndex; i >= endingIndex; i--) { for (let i = startingIndex; i >= endingIndex; i--) {
if (gameVersions[i].version_type === 'release' || !filterOnlyRelease) { if (gameVersions[i].version_type === "release" || !filterOnlyRelease) {
final.push(gameVersions[i].version) final.push(gameVersions[i].version);
} }
} }
return final return final;
} }
const loaders = [] const loaders = [];
let newGameVersions = [] let newGameVersions = [];
if (project.actualProjectType === 'mod') { if (project.actualProjectType === "mod") {
loaders.push('datapack') loaders.push("datapack");
switch (metadata.pack.pack_format) { switch (metadata.pack.pack_format) {
case 4: case 4:
newGameVersions = getRange('1.13', '1.14.4') newGameVersions = getRange("1.13", "1.14.4");
break break;
case 5: case 5:
newGameVersions = getRange('1.15', '1.16.1') newGameVersions = getRange("1.15", "1.16.1");
break break;
case 6: case 6:
newGameVersions = getRange('1.16.2', '1.16.5') newGameVersions = getRange("1.16.2", "1.16.5");
break break;
case 7: case 7:
newGameVersions = getRange('1.17', '1.17.1') newGameVersions = getRange("1.17", "1.17.1");
break break;
case 8: case 8:
newGameVersions = getRange('1.18', '1.18.1') newGameVersions = getRange("1.18", "1.18.1");
break break;
case 9: case 9:
newGameVersions.push('1.18.2') newGameVersions.push("1.18.2");
break break;
case 10: case 10:
newGameVersions = getRange('1.19', '1.19.3') newGameVersions = getRange("1.19", "1.19.3");
break break;
case 11: case 11:
newGameVersions = getRange('23w03a', '23w05a') newGameVersions = getRange("23w03a", "23w05a");
break break;
case 12: case 12:
newGameVersions.push('1.19.4') newGameVersions.push("1.19.4");
break break;
default: default:
} }
} }
if (project.actualProjectType === 'resourcepack') { if (project.actualProjectType === "resourcepack") {
loaders.push('minecraft') loaders.push("minecraft");
switch (metadata.pack.pack_format) { switch (metadata.pack.pack_format) {
case 1: case 1:
newGameVersions = getRange('1.6.1', '1.8.9') newGameVersions = getRange("1.6.1", "1.8.9");
break break;
case 2: case 2:
newGameVersions = getRange('1.9', '1.10.2') newGameVersions = getRange("1.9", "1.10.2");
break break;
case 3: case 3:
newGameVersions = getRange('1.11', '1.12.2') newGameVersions = getRange("1.11", "1.12.2");
break break;
case 4: case 4:
newGameVersions = getRange('1.13', '1.14.4') newGameVersions = getRange("1.13", "1.14.4");
break break;
case 5: case 5:
newGameVersions = getRange('1.15', '1.16.1') newGameVersions = getRange("1.15", "1.16.1");
break break;
case 6: case 6:
newGameVersions = getRange('1.16.2', '1.16.5') newGameVersions = getRange("1.16.2", "1.16.5");
break break;
case 7: case 7:
newGameVersions = getRange('1.17', '1.17.1') newGameVersions = getRange("1.17", "1.17.1");
break break;
case 8: case 8:
newGameVersions = getRange('1.18', '1.18.2') newGameVersions = getRange("1.18", "1.18.2");
break break;
case 9: case 9:
newGameVersions = getRange('1.19', '1.19.2') newGameVersions = getRange("1.19", "1.19.2");
break break;
case 11: case 11:
newGameVersions = getRange('22w42a', '22w44a') newGameVersions = getRange("22w42a", "22w44a");
break break;
case 12: case 12:
newGameVersions.push('1.19.3') newGameVersions.push("1.19.3");
break break;
case 13: case 13:
newGameVersions.push('1.19.4') newGameVersions.push("1.19.4");
break break;
default: default:
} }
} }
@ -396,20 +396,20 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
return { return {
loaders, loaders,
game_versions: newGameVersions, game_versions: newGameVersions,
} };
}, },
} };
const zipReader = new JSZip() const zipReader = new JSZip();
const zip = await zipReader.loadAsync(rawFile) const zip = await zipReader.loadAsync(rawFile);
for (const fileName in inferFunctions) { for (const fileName in inferFunctions) {
const file = zip.file(fileName) const file = zip.file(fileName);
if (file !== null) { if (file !== null) {
const text = await file.async('text') const text = await file.async("text");
return inferFunctions[fileName](text, zip) return inferFunctions[fileName](text, zip);
}
} }
} }
};

View File

@ -1,12 +1,13 @@
import { useNuxtApp } from '#imports' /* eslint-disable no-undef */
import { useNuxtApp } from "#imports";
async function getBulk(type, ids, apiVersion = 2) { async function getBulk(type, ids, apiVersion = 2) {
if (ids.length === 0) { if (ids.length === 0) {
return [] return [];
} }
const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}` const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}`;
return await useBaseFetch(url, { apiVersion }) return await useBaseFetch(url, { apiVersion });
} }
export async function fetchExtraNotificationData(notifications) { export async function fetchExtraNotificationData(notifications) {
@ -17,154 +18,154 @@ export async function fetchExtraNotificationData(notifications) {
users: [], users: [],
versions: [], versions: [],
organizations: [], organizations: [],
} };
for (const notification of notifications) { for (const notification of notifications) {
if (notification.body) { if (notification.body) {
if (notification.body.project_id) { if (notification.body.project_id) {
bulk.projects.push(notification.body.project_id) bulk.projects.push(notification.body.project_id);
} }
if (notification.body.version_id) { if (notification.body.version_id) {
bulk.versions.push(notification.body.version_id) bulk.versions.push(notification.body.version_id);
} }
if (notification.body.report_id) { if (notification.body.report_id) {
bulk.reports.push(notification.body.report_id) bulk.reports.push(notification.body.report_id);
} }
if (notification.body.thread_id) { if (notification.body.thread_id) {
bulk.threads.push(notification.body.thread_id) bulk.threads.push(notification.body.thread_id);
} }
if (notification.body.invited_by) { if (notification.body.invited_by) {
bulk.users.push(notification.body.invited_by) bulk.users.push(notification.body.invited_by);
} }
if (notification.body.organization_id) { if (notification.body.organization_id) {
bulk.organizations.push(notification.body.organization_id) bulk.organizations.push(notification.body.organization_id);
} }
} }
} }
const reports = await getBulk('reports', bulk.reports) const reports = await getBulk("reports", bulk.reports);
for (const report of reports) { for (const report of reports) {
if (report.item_type === 'project') { if (report.item_type === "project") {
bulk.projects.push(report.item_id) bulk.projects.push(report.item_id);
} else if (report.item_type === 'user') { } else if (report.item_type === "user") {
bulk.users.push(report.item_id) bulk.users.push(report.item_id);
} else if (report.item_type === 'version') { } else if (report.item_type === "version") {
bulk.versions.push(report.item_id) bulk.versions.push(report.item_id);
} }
} }
const versions = await getBulk('versions', bulk.versions) const versions = await getBulk("versions", bulk.versions);
for (const version of versions) { for (const version of versions) {
bulk.projects.push(version.project_id) bulk.projects.push(version.project_id);
} }
const [projects, threads, users, organizations] = await Promise.all([ const [projects, threads, users, organizations] = await Promise.all([
getBulk('projects', bulk.projects), getBulk("projects", bulk.projects),
getBulk('threads', bulk.threads), getBulk("threads", bulk.threads),
getBulk('users', bulk.users), getBulk("users", bulk.users),
getBulk('organizations', bulk.organizations, 3), getBulk("organizations", bulk.organizations, 3),
]) ]);
for (const notification of notifications) { for (const notification of notifications) {
notification.extra_data = {} notification.extra_data = {};
if (notification.body) { if (notification.body) {
if (notification.body.project_id) { if (notification.body.project_id) {
notification.extra_data.project = projects.find( notification.extra_data.project = projects.find(
(x) => x.id === notification.body.project_id (x) => x.id === notification.body.project_id,
) );
} }
if (notification.body.organization_id) { if (notification.body.organization_id) {
notification.extra_data.organization = organizations.find( notification.extra_data.organization = organizations.find(
(x) => x.id === notification.body.organization_id (x) => x.id === notification.body.organization_id,
) );
} }
if (notification.body.report_id) { if (notification.body.report_id) {
notification.extra_data.report = reports.find((x) => x.id === notification.body.report_id) notification.extra_data.report = reports.find((x) => x.id === notification.body.report_id);
const type = notification.extra_data.report.item_type const type = notification.extra_data.report.item_type;
if (type === 'project') { if (type === "project") {
notification.extra_data.project = projects.find( notification.extra_data.project = projects.find(
(x) => x.id === notification.extra_data.report.item_id (x) => x.id === notification.extra_data.report.item_id,
) );
} else if (type === 'user') { } else if (type === "user") {
notification.extra_data.user = users.find( notification.extra_data.user = users.find(
(x) => x.id === notification.extra_data.report.item_id (x) => x.id === notification.extra_data.report.item_id,
) );
} else if (type === 'version') { } else if (type === "version") {
notification.extra_data.version = versions.find( notification.extra_data.version = versions.find(
(x) => x.id === notification.extra_data.report.item_id (x) => x.id === notification.extra_data.report.item_id,
) );
notification.extra_data.project = projects.find( notification.extra_data.project = projects.find(
(x) => x.id === notification.extra_data.version.project_id (x) => x.id === notification.extra_data.version.project_id,
) );
} }
} }
if (notification.body.thread_id) { if (notification.body.thread_id) {
notification.extra_data.thread = threads.find((x) => x.id === notification.body.thread_id) notification.extra_data.thread = threads.find((x) => x.id === notification.body.thread_id);
} }
if (notification.body.invited_by) { if (notification.body.invited_by) {
notification.extra_data.invited_by = users.find( notification.extra_data.invited_by = users.find(
(x) => x.id === notification.body.invited_by (x) => x.id === notification.body.invited_by,
) );
} }
if (notification.body.version_id) { if (notification.body.version_id) {
notification.extra_data.version = versions.find( notification.extra_data.version = versions.find(
(x) => x.id === notification.body.version_id (x) => x.id === notification.body.version_id,
) );
} }
} }
} }
return notifications return notifications;
} }
export function groupNotifications(notifications) { export function groupNotifications(notifications) {
const grouped = [] const grouped = [];
for (let i = 0; i < notifications.length; i++) { for (let i = 0; i < notifications.length; i++) {
const current = notifications[i] const current = notifications[i];
const next = notifications[i + 1] const next = notifications[i + 1];
if (current.body && i < notifications.length - 1 && isSimilar(current, next)) { if (current.body && i < notifications.length - 1 && isSimilar(current, next)) {
current.grouped_notifs = [next] current.grouped_notifs = [next];
let j = i + 2 let j = i + 2;
while (j < notifications.length && isSimilar(current, notifications[j])) { while (j < notifications.length && isSimilar(current, notifications[j])) {
current.grouped_notifs.push(notifications[j]) current.grouped_notifs.push(notifications[j]);
j++ j++;
} }
grouped.push(current) grouped.push(current);
i = j - 1 // skip i to the last ungrouped i = j - 1; // skip i to the last ungrouped
} else { } else {
grouped.push(current) grouped.push(current);
} }
} }
return grouped return grouped;
} }
function isSimilar(notifA, notifB) { function isSimilar(notifA, notifB) {
return !!notifA.body.project_id && notifA.body.project_id === notifB.body.project_id return !!notifA.body.project_id && notifA.body.project_id === notifB.body.project_id;
} }
export async function markAsRead(ids) { export async function markAsRead(ids) {
try { try {
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, { await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
method: 'PATCH', method: "PATCH",
}) });
return (notifications) => { return (notifications) => {
const newNotifs = notifications const newNotifs = notifications;
newNotifs.forEach((notif) => { newNotifs.forEach((notif) => {
if (ids.includes(notif.id)) { if (ids.includes(notif.id)) {
notif.read = true notif.read = true;
}
})
return newNotifs
} }
});
return newNotifs;
};
} catch (err) { } catch (err) {
const app = useNuxtApp() const app = useNuxtApp();
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Error marking notification as read', title: "Error marking notification as read",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: "error",
}) });
return () => {} return () => {};
} }
} }

View File

@ -1,5 +1,6 @@
import JSZip from 'jszip' /* eslint-disable no-undef */
import TOML from '@ltd/j-toml' import JSZip from "jszip";
import TOML from "@ltd/j-toml";
export const createDataPackVersion = async function ( export const createDataPackVersion = async function (
project, project,
@ -7,18 +8,18 @@ export const createDataPackVersion = async function (
primaryFile, primaryFile,
members, members,
allGameVersions, allGameVersions,
loaders loaders,
) { ) {
// force version to start with number, as required by FML // force version to start with number, as required by FML
const newVersionNumber = version.version_number.match(/^\d/) const newVersionNumber = version.version_number.match(/^\d/)
? version.version_number ? version.version_number
: `1-${version.version_number}` : `1-${version.version_number}`;
const newSlug = `mr_${project.slug.replace('-', '_').replace(/\W/g, '')}`.substring(0, 63) const newSlug = `mr_${project.slug.replace("-", "_").replace(/\W/g, "")}`.substring(0, 63);
const iconPath = `${project.slug}_pack.png` const iconPath = `${project.slug}_pack.png`;
const config = useRuntimeConfig() const config = useRuntimeConfig();
const fabricModJson = { const fabricModJson = {
schemaVersion: 1, schemaVersion: 1,
@ -32,16 +33,16 @@ export const createDataPackVersion = async function (
}, },
license: project.license.id, license: project.license.id,
icon: iconPath, icon: iconPath,
environment: '*', environment: "*",
depends: { depends: {
'fabric-resource-loader-v0': '*', "fabric-resource-loader-v0": "*",
}, },
} };
const quiltModJson = { const quiltModJson = {
schema_version: 1, schema_version: 1,
quilt_loader: { quilt_loader: {
group: 'com.modrinth', group: "com.modrinth",
id: newSlug, id: newSlug,
version: newVersionNumber, version: newVersionNumber,
metadata: { metadata: {
@ -52,7 +53,7 @@ export const createDataPackVersion = async function (
...acc, ...acc,
[x.name]: x.role, [x.name]: x.role,
}), }),
{} {},
), ),
contact: { contact: {
homepage: `${config.public.siteUrl}/${project.project_type}/${ homepage: `${config.public.siteUrl}/${project.project_type}/${
@ -61,32 +62,32 @@ export const createDataPackVersion = async function (
}, },
icon: iconPath, icon: iconPath,
}, },
intermediate_mappings: 'net.fabricmc:intermediary', intermediate_mappings: "net.fabricmc:intermediary",
depends: [ depends: [
{ {
id: 'quilt_resource_loader', id: "quilt_resource_loader",
versions: '*', versions: "*",
unless: 'fabric-resource-loader-v0', unless: "fabric-resource-loader-v0",
}, },
], ],
}, },
} };
const cutoffIndex = allGameVersions.findIndex((x) => x.version === '1.18.2') const cutoffIndex = allGameVersions.findIndex((x) => x.version === "1.18.2");
let maximumIndex = Number.MIN_VALUE let maximumIndex = Number.MIN_VALUE;
for (const val of version.game_versions) { for (const val of version.game_versions) {
const index = allGameVersions.findIndex((x) => x.version === val) const index = allGameVersions.findIndex((x) => x.version === val);
if (index > maximumIndex) { if (index > maximumIndex) {
maximumIndex = index maximumIndex = index;
} }
} }
const newForge = maximumIndex < cutoffIndex const newForge = maximumIndex < cutoffIndex;
const forgeModsToml = { const forgeModsToml = {
modLoader: newForge ? 'lowcodefml' : 'javafml', modLoader: newForge ? "lowcodefml" : "javafml",
loaderVersion: newForge ? '[40,)' : '[25,)', loaderVersion: newForge ? "[40,)" : "[25,)",
license: project.license.id, license: project.license.id,
showAsResourcePack: false, showAsResourcePack: false,
mods: [ mods: [
@ -96,103 +97,103 @@ export const createDataPackVersion = async function (
displayName: project.title, displayName: project.title,
description: project.description, description: project.description,
logoFile: iconPath, logoFile: iconPath,
updateJSONURL: `${config.public.apiBaseUrl.replace('/v2/', '')}/updates/${ updateJSONURL: `${config.public.apiBaseUrl.replace("/v2/", "")}/updates/${
project.id project.id
}/forge_updates.json`, }/forge_updates.json`,
credits: 'Generated by Modrinth', credits: "Generated by Modrinth",
authors: members.map((x) => x.name).join(', '), authors: members.map((x) => x.name).join(", "),
displayURL: `${config.public.siteUrl}/${project.project_type}/${ displayURL: `${config.public.siteUrl}/${project.project_type}/${
project.slug ?? project.id project.slug ?? project.id
}`, }`,
}, },
], ],
} };
if (project.source_url) { if (project.source_url) {
quiltModJson.quilt_loader.metadata.contact.sources = project.source_url quiltModJson.quilt_loader.metadata.contact.sources = project.source_url;
fabricModJson.contact.sources = project.source_url fabricModJson.contact.sources = project.source_url;
} }
if (project.issues_url) { if (project.issues_url) {
quiltModJson.quilt_loader.metadata.contact.issues = project.issues_url quiltModJson.quilt_loader.metadata.contact.issues = project.issues_url;
fabricModJson.contact.issues = project.issues_url fabricModJson.contact.issues = project.issues_url;
forgeModsToml.issueTrackerURL = project.issues_url forgeModsToml.issueTrackerURL = project.issues_url;
} }
const primaryFileData = await (await fetch(primaryFile.url)).blob() const primaryFileData = await (await fetch(primaryFile.url)).blob();
const primaryZipReader = new JSZip() const primaryZipReader = new JSZip();
await primaryZipReader.loadAsync(primaryFileData) await primaryZipReader.loadAsync(primaryFileData);
if (loaders.includes('fabric')) { if (loaders.includes("fabric")) {
primaryZipReader.file('fabric.mod.json', JSON.stringify(fabricModJson)) primaryZipReader.file("fabric.mod.json", JSON.stringify(fabricModJson));
} }
if (loaders.includes('quilt')) { if (loaders.includes("quilt")) {
primaryZipReader.file('quilt.mod.json', JSON.stringify(quiltModJson)) primaryZipReader.file("quilt.mod.json", JSON.stringify(quiltModJson));
} }
if (loaders.includes('forge')) { if (loaders.includes("forge")) {
primaryZipReader.file('META-INF/mods.toml', TOML.stringify(forgeModsToml, { newline: '\n' })) primaryZipReader.file("META-INF/mods.toml", TOML.stringify(forgeModsToml, { newline: "\n" }));
} }
if (!newForge && loaders.includes('forge')) { if (!newForge && loaders.includes("forge")) {
const classFile = new Uint8Array( const classFile = new Uint8Array(
await ( await (
await fetch('https://cdn.modrinth.com/wrapper/ModrinthWrapperRestiched.class') await fetch("https://cdn.modrinth.com/wrapper/ModrinthWrapperRestiched.class")
).arrayBuffer() ).arrayBuffer(),
) );
let binary = '' let binary = "";
for (let i = 0; i < classFile.byteLength; i++) { for (let i = 0; i < classFile.byteLength; i++) {
binary += String.fromCharCode(classFile[i]) binary += String.fromCharCode(classFile[i]);
} }
let sanitizedId = project.id let sanitizedId = project.id;
if (project.id.match(/^(\d+)/g)) { if (project.id.match(/^(\d+)/g)) {
sanitizedId = '_' + sanitizedId sanitizedId = "_" + sanitizedId;
} }
sanitizedId = sanitizedId.substring(0, 8) sanitizedId = sanitizedId.substring(0, 8);
binary = binary binary = binary
.replace( .replace(
String.fromCharCode(32) + 'needs1to1be1changed1modrinth1mod', String.fromCharCode(32) + "needs1to1be1changed1modrinth1mod",
String.fromCharCode(newSlug.length) + newSlug String.fromCharCode(newSlug.length) + newSlug,
) )
.replace('/wrappera/', `/${sanitizedId}/`) .replace("/wrappera/", `/${sanitizedId}/`);
const newArr = [] const newArr = [];
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
newArr.push(binary.charCodeAt(i)) newArr.push(binary.charCodeAt(i));
} }
primaryZipReader.file( primaryZipReader.file(
`com/modrinth/${sanitizedId}/ModrinthWrapper.class`, `com/modrinth/${sanitizedId}/ModrinthWrapper.class`,
new Uint8Array(newArr) new Uint8Array(newArr),
) );
} }
const resourcePack = version.files.find((x) => x.file_type === 'required-resource-pack') const resourcePack = version.files.find((x) => x.file_type === "required-resource-pack");
const resourcePackData = resourcePack ? await (await fetch(resourcePack.url)).blob() : null const resourcePackData = resourcePack ? await (await fetch(resourcePack.url)).blob() : null;
if (resourcePackData) { if (resourcePackData) {
const resourcePackReader = new JSZip() const resourcePackReader = new JSZip();
await resourcePackReader.loadAsync(resourcePackData) await resourcePackReader.loadAsync(resourcePackData);
for (const [path, file] of Object.entries(resourcePackReader.files)) { for (const [path, file] of Object.entries(resourcePackReader.files)) {
if (!primaryZipReader.file(path) && !path.includes('.mcassetsroot')) { if (!primaryZipReader.file(path) && !path.includes(".mcassetsroot")) {
primaryZipReader.file(path, await file.async('uint8array')) primaryZipReader.file(path, await file.async("uint8array"));
} }
} }
} }
if (primaryZipReader.file('pack.png')) { if (primaryZipReader.file("pack.png")) {
primaryZipReader.file(iconPath, await primaryZipReader.file('pack.png').async('uint8array')) primaryZipReader.file(iconPath, await primaryZipReader.file("pack.png").async("uint8array"));
} }
return await primaryZipReader.generateAsync({ return await primaryZipReader.generateAsync({
type: 'blob', type: "blob",
mimeType: 'application/java-archive', mimeType: "application/java-archive",
}) });
} };

View File

@ -1,131 +1,132 @@
/* eslint-disable no-undef */
export const getProjectTypeForUrl = (type, categories) => { export const getProjectTypeForUrl = (type, categories) => {
return getProjectTypeForUrlShorthand(type, categories) return getProjectTypeForUrlShorthand(type, categories);
} };
export const getProjectTypeForUrlShorthand = (type, categories, overrideTags) => { export const getProjectTypeForUrlShorthand = (type, categories, overrideTags) => {
const tags = overrideTags ?? useTags().value const tags = overrideTags ?? useTags().value;
if (type === 'mod') { if (type === "mod") {
const isMod = categories.some((category) => { const isMod = categories.some((category) => {
return tags.loaderData.modLoaders.includes(category) return tags.loaderData.modLoaders.includes(category);
}) });
const isPlugin = categories.some((category) => { const isPlugin = categories.some((category) => {
return tags.loaderData.allPluginLoaders.includes(category) return tags.loaderData.allPluginLoaders.includes(category);
}) });
const isDataPack = categories.some((category) => { const isDataPack = categories.some((category) => {
return tags.loaderData.dataPackLoaders.includes(category) return tags.loaderData.dataPackLoaders.includes(category);
}) });
if (isDataPack) { if (isDataPack) {
return 'datapack' return "datapack";
} else if (isPlugin) { } else if (isPlugin) {
return 'plugin' return "plugin";
} else if (isMod) { } else if (isMod) {
return 'mod' return "mod";
} else { } else {
return 'mod' return "mod";
} }
} else { } else {
return type return type;
}
} }
};
export const getProjectLink = (project) => { export const getProjectLink = (project) => {
return `/${getProjectTypeForUrl(project.project_type, project.loaders)}/${ return `/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}` }`;
} };
export const getVersionLink = (project, version) => { export const getVersionLink = (project, version) => {
if (version) { if (version) {
return getProjectLink(project) + '/version/' + version.id return getProjectLink(project) + "/version/" + version.id;
} else { } else {
return getProjectLink(project) return getProjectLink(project);
}
} }
};
export const isApproved = (project) => { export const isApproved = (project) => {
return project && APPROVED_PROJECT_STATUSES.includes(project.status) return project && APPROVED_PROJECT_STATUSES.includes(project.status);
} };
export const isListed = (project) => { export const isListed = (project) => {
return project && LISTED_PROJECT_STATUSES.includes(project.status) return project && LISTED_PROJECT_STATUSES.includes(project.status);
} };
export const isUnlisted = (project) => { export const isUnlisted = (project) => {
return project && UNLISTED_PROJECT_STATUSES.includes(project.status) return project && UNLISTED_PROJECT_STATUSES.includes(project.status);
} };
export const isPrivate = (project) => { export const isPrivate = (project) => {
return project && PRIVATE_PROJECT_STATUSES.includes(project.status) return project && PRIVATE_PROJECT_STATUSES.includes(project.status);
} };
export const isRejected = (project) => { export const isRejected = (project) => {
return project && REJECTED_PROJECT_STATUSES.includes(project.status) return project && REJECTED_PROJECT_STATUSES.includes(project.status);
} };
export const isUnderReview = (project) => { export const isUnderReview = (project) => {
return project && UNDER_REVIEW_PROJECT_STATUSES.includes(project.status) return project && UNDER_REVIEW_PROJECT_STATUSES.includes(project.status);
} };
export const isDraft = (project) => { export const isDraft = (project) => {
return project && DRAFT_PROJECT_STATUSES.includes(project.status) return project && DRAFT_PROJECT_STATUSES.includes(project.status);
} };
export const APPROVED_PROJECT_STATUSES = ['approved', 'archived', 'unlisted', 'private'] export const APPROVED_PROJECT_STATUSES = ["approved", "archived", "unlisted", "private"];
export const LISTED_PROJECT_STATUSES = ['approved', 'archived'] export const LISTED_PROJECT_STATUSES = ["approved", "archived"];
export const UNLISTED_PROJECT_STATUSES = ['unlisted', 'withheld'] export const UNLISTED_PROJECT_STATUSES = ["unlisted", "withheld"];
export const PRIVATE_PROJECT_STATUSES = ['private', 'rejected', 'processing'] export const PRIVATE_PROJECT_STATUSES = ["private", "rejected", "processing"];
export const REJECTED_PROJECT_STATUSES = ['rejected', 'withheld'] export const REJECTED_PROJECT_STATUSES = ["rejected", "withheld"];
export const UNDER_REVIEW_PROJECT_STATUSES = ['processing'] export const UNDER_REVIEW_PROJECT_STATUSES = ["processing"];
export const DRAFT_PROJECT_STATUSES = ['draft'] export const DRAFT_PROJECT_STATUSES = ["draft"];
export function getVersionsToDisplay(project, overrideTags) { export function getVersionsToDisplay(project, overrideTags) {
const tags = overrideTags ?? useTags().value const tags = overrideTags ?? useTags().value;
const projectVersions = project.game_versions.slice() const projectVersions = project.game_versions.slice();
const allVersions = tags.gameVersions.slice() const allVersions = tags.gameVersions.slice();
const allSnapshots = allVersions.filter((version) => version.version_type === 'snapshot') const allSnapshots = allVersions.filter((version) => version.version_type === "snapshot");
const allReleases = allVersions.filter((version) => version.version_type === 'release') const allReleases = allVersions.filter((version) => version.version_type === "release");
const allLegacy = allVersions.filter( const allLegacy = allVersions.filter(
(version) => version.version_type !== 'snapshot' && version.version_type !== 'release' (version) => version.version_type !== "snapshot" && version.version_type !== "release",
) );
{ {
const indices = allVersions.reduce((map, gameVersion, index) => { const indices = allVersions.reduce((map, gameVersion, index) => {
map[gameVersion.version] = index map[gameVersion.version] = index;
return map return map;
}, {}) }, {});
projectVersions.sort((a, b) => indices[a] - indices[b]) projectVersions.sort((a, b) => indices[a] - indices[b]);
} }
const releaseVersions = projectVersions.filter((projVer) => const releaseVersions = projectVersions.filter((projVer) =>
allReleases.some((gameVer) => gameVer.version === projVer) allReleases.some((gameVer) => gameVer.version === projVer),
) );
const latestReleaseVersionDate = Date.parse( const latestReleaseVersionDate = Date.parse(
allReleases.find((version) => version.version === releaseVersions[0])?.date allReleases.find((version) => version.version === releaseVersions[0])?.date,
) );
const latestSnapshot = projectVersions.find((projVer) => const latestSnapshot = projectVersions.find((projVer) =>
allSnapshots.some( allSnapshots.some(
(gameVer) => (gameVer) =>
gameVer.version === projVer && gameVer.version === projVer &&
(!latestReleaseVersionDate || latestReleaseVersionDate < Date.parse(gameVer.date)) (!latestReleaseVersionDate || latestReleaseVersionDate < Date.parse(gameVer.date)),
) ),
) );
const allReleasesGrouped = groupVersions( const allReleasesGrouped = groupVersions(
allReleases.map((release) => release.version), allReleases.map((release) => release.version),
false false,
) );
const projectVersionsGrouped = groupVersions(releaseVersions, true) const projectVersionsGrouped = groupVersions(releaseVersions, true);
const releaseVersionsAsRanges = projectVersionsGrouped.map(({ major, minor }) => { const releaseVersionsAsRanges = projectVersionsGrouped.map(({ major, minor }) => {
if (minor.length === 1) { if (minor.length === 1) {
return formatVersion(major, minor[0]) return formatVersion(major, minor[0]);
} }
if ( if (
@ -133,120 +134,121 @@ export function getVersionsToDisplay(project, overrideTags) {
.find((x) => x.major === major) .find((x) => x.major === major)
.minor.every((value, index) => value === minor[index]) .minor.every((value, index) => value === minor[index])
) { ) {
return `${major}.x` return `${major}.x`;
} }
return `${formatVersion(major, minor[0])}${formatVersion(major, minor[minor.length - 1])}` return `${formatVersion(major, minor[0])}${formatVersion(major, minor[minor.length - 1])}`;
}) });
const legacyVersionsAsRanges = groupConsecutiveIndices( const legacyVersionsAsRanges = groupConsecutiveIndices(
projectVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)), projectVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)),
allLegacy allLegacy,
) );
let output = [...legacyVersionsAsRanges] let output = [...legacyVersionsAsRanges];
// show all snapshots if there's no release versions // show all snapshots if there's no release versions
if (releaseVersionsAsRanges.length === 0) { if (releaseVersionsAsRanges.length === 0) {
const snapshotVersionsAsRanges = groupConsecutiveIndices( const snapshotVersionsAsRanges = groupConsecutiveIndices(
projectVersions.filter((projVer) => projectVersions.filter((projVer) =>
allSnapshots.some((gameVer) => gameVer.version === projVer) allSnapshots.some((gameVer) => gameVer.version === projVer),
), ),
allSnapshots allSnapshots,
) );
output = [...snapshotVersionsAsRanges, ...output] output = [...snapshotVersionsAsRanges, ...output];
} else { } else {
output = [...releaseVersionsAsRanges, ...output] output = [...releaseVersionsAsRanges, ...output];
} }
if (latestSnapshot) { if (latestSnapshot) {
output = [latestSnapshot, ...output] output = [latestSnapshot, ...output];
} }
return output return output;
} }
const mcVersionRegex = /^([0-9]+.[0-9]+)(.[0-9]+)?$/ const mcVersionRegex = /^([0-9]+.[0-9]+)(.[0-9]+)?$/;
function groupVersions(versions, consecutive = false) { function groupVersions(versions, consecutive = false) {
return versions return versions
.slice() .slice()
.reverse() .reverse()
.reduce((ranges, version) => { .reduce((ranges, version) => {
const matchesVersion = version.match(mcVersionRegex) const matchesVersion = version.match(mcVersionRegex);
if (matchesVersion) { if (matchesVersion) {
const majorVersion = matchesVersion[1] const majorVersion = matchesVersion[1];
const minorVersion = matchesVersion[2] const minorVersion = matchesVersion[2];
const minorNumeric = minorVersion ? parseInt(minorVersion.replace('.', '')) : 0 const minorNumeric = minorVersion ? parseInt(minorVersion.replace(".", "")) : 0;
let prevInRange let prevInRange;
if ( if (
(prevInRange = ranges.find( (prevInRange = ranges.find(
(x) => x.major === majorVersion && (!consecutive || x.minor.at(-1) === minorNumeric - 1) (x) =>
x.major === majorVersion && (!consecutive || x.minor.at(-1) === minorNumeric - 1),
)) ))
) { ) {
prevInRange.minor.push(minorNumeric) prevInRange.minor.push(minorNumeric);
return ranges return ranges;
} }
return [...ranges, { major: majorVersion, minor: [minorNumeric] }] return [...ranges, { major: majorVersion, minor: [minorNumeric] }];
} }
return ranges return ranges;
}, []) }, [])
.reverse() .reverse();
} }
function groupConsecutiveIndices(versions, referenceList) { function groupConsecutiveIndices(versions, referenceList) {
if (!versions || versions.length === 0) { if (!versions || versions.length === 0) {
return [] return [];
} }
const referenceMap = new Map() const referenceMap = new Map();
referenceList.forEach((item, index) => { referenceList.forEach((item, index) => {
referenceMap.set(item.version, index) referenceMap.set(item.version, index);
}) });
const sortedList = versions.slice().sort((a, b) => referenceMap.get(a) - referenceMap.get(b)) const sortedList = versions.slice().sort((a, b) => referenceMap.get(a) - referenceMap.get(b));
const ranges = [] const ranges = [];
let start = sortedList[0] let start = sortedList[0];
let previous = sortedList[0] let previous = sortedList[0];
for (let i = 1; i < sortedList.length; i++) { for (let i = 1; i < sortedList.length; i++) {
const current = sortedList[i] const current = sortedList[i];
if (referenceMap.get(current) !== referenceMap.get(previous) + 1) { if (referenceMap.get(current) !== referenceMap.get(previous) + 1) {
ranges.push(validateRange(`${previous}${start}`)) ranges.push(validateRange(`${previous}${start}`));
start = current start = current;
} }
previous = current previous = current;
} }
ranges.push(validateRange(`${previous}${start}`)) ranges.push(validateRange(`${previous}${start}`));
return ranges return ranges;
} }
function validateRange(range) { function validateRange(range) {
switch (range) { switch (range) {
case 'rd-132211b1.8.1': case "rd-132211b1.8.1":
return 'All legacy versions' return "All legacy versions";
case 'a1.0.4b1.8.1': case "a1.0.4b1.8.1":
return 'All alpha and beta versions' return "All alpha and beta versions";
case 'a1.0.4a1.2.6': case "a1.0.4a1.2.6":
return 'All alpha versions' return "All alpha versions";
case 'b1.0b1.8.1': case "b1.0b1.8.1":
return 'All beta versions' return "All beta versions";
case 'rd-132211inf20100618': case "rd-132211inf20100618":
return 'All pre-alpha versions' return "All pre-alpha versions";
} }
const splitRange = range.split('') const splitRange = range.split("");
if (splitRange && splitRange[0] === splitRange[1]) { if (splitRange && splitRange[0] === splitRange[1]) {
return splitRange[0] return splitRange[0];
} }
return range return range;
} }
function formatVersion(major, minor) { function formatVersion(major, minor) {
return minor === 0 ? major : `${major}.${minor}` return minor === 0 ? major : `${major}.${minor}`;
} }

View File

@ -1,16 +1,17 @@
/* eslint-disable no-undef */
export const acceptTeamInvite = async (teamId) => { export const acceptTeamInvite = async (teamId) => {
await useBaseFetch(`team/${teamId}/join`, { await useBaseFetch(`team/${teamId}/join`, {
apiVersion: 3, apiVersion: 3,
method: 'POST', method: "POST",
}) });
} };
export const removeSelfFromTeam = async (teamId) => { export const removeSelfFromTeam = async (teamId) => {
const auth = await useAuth() const auth = await useAuth();
await removeTeamMember(teamId, auth.value.user.id) await removeTeamMember(teamId, auth.value.user.id);
} };
export const removeTeamMember = async (teamId, userId) => { export const removeTeamMember = async (teamId, userId) => {
await useBaseFetch(`team/${teamId}/members/${userId}`, { await useBaseFetch(`team/${teamId}/members/${userId}`, {
apiVersion: 3, apiVersion: 3,
method: 'DELETE', method: "DELETE",
}) });
} };

View File

@ -1,26 +1,26 @@
export function addReportMessage(thread, report) { export function addReportMessage(thread, report) {
if (!thread || !report) { if (!thread || !report) {
return thread return thread;
} }
if ( if (
!thread.members.some((user) => { !thread.members.some((user) => {
return user.id === report.reporterUser.id return user.id === report.reporterUser.id;
}) })
) { ) {
thread.members.push(report.reporterUser) thread.members.push(report.reporterUser);
} }
if (!thread.messages.some((message) => message.id === 'original')) { if (!thread.messages.some((message) => message.id === "original")) {
thread.messages.push({ thread.messages.push({
id: 'original', id: "original",
author_id: report.reporterUser.id, author_id: report.reporterUser.id,
body: { body: {
type: 'text', type: "text",
body: report.body, body: report.body,
private: false, private: false,
replying_to: null, replying_to: null,
}, },
created: report.created, created: report.created,
}) });
} }
return thread return thread;
} }

View File

@ -1,9 +1,9 @@
export const getUserLink = (user) => { export const getUserLink = (user) => {
return `/user/${user.username}` return `/user/${user.username}`;
} };
export const isStaff = (user) => { export const isStaff = (user) => {
return user && STAFF_ROLES.includes(user.role) return user && STAFF_ROLES.includes(user.role);
} };
export const STAFF_ROLES = ['moderator', 'admin'] export const STAFF_ROLES = ["moderator", "admin"];

View File

@ -282,8 +282,8 @@
:title="formatMessage(commonMessages.notificationsLabel)" :title="formatMessage(commonMessages.notificationsLabel)"
@click=" @click="
() => { () => {
isMobileMenuOpen = false isMobileMenuOpen = false;
isBrowseMenuOpen = false isBrowseMenuOpen = false;
} }
" "
> >
@ -426,294 +426,294 @@ import {
XIcon, XIcon,
IssuesIcon, IssuesIcon,
ReportIcon, ReportIcon,
} from '@modrinth/assets' } from "@modrinth/assets";
import { Button } from '@modrinth/ui' import { Button } from "@modrinth/ui";
import HamburgerIcon from '~/assets/images/utils/hamburger.svg?component' import HamburgerIcon from "~/assets/images/utils/hamburger.svg?component";
import CrossIcon from '~/assets/images/utils/x.svg?component' import CrossIcon from "~/assets/images/utils/x.svg?component";
import SearchIcon from '~/assets/images/utils/search.svg?component' import SearchIcon from "~/assets/images/utils/search.svg?component";
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?component' import NotificationIcon from "~/assets/images/sidebar/notifications.svg?component";
import SettingsIcon from '~/assets/images/sidebar/settings.svg?component' import SettingsIcon from "~/assets/images/sidebar/settings.svg?component";
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component' import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
import HomeIcon from '~/assets/images/sidebar/home.svg?component' import HomeIcon from "~/assets/images/sidebar/home.svg?component";
import MoonIcon from '~/assets/images/utils/moon.svg?component' import MoonIcon from "~/assets/images/utils/moon.svg?component";
import SunIcon from '~/assets/images/utils/sun.svg?component' import SunIcon from "~/assets/images/utils/sun.svg?component";
import PlusIcon from '~/assets/images/utils/plus.svg?component' import PlusIcon from "~/assets/images/utils/plus.svg?component";
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component' import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
import LogOutIcon from '~/assets/images/utils/log-out.svg?component' import LogOutIcon from "~/assets/images/utils/log-out.svg?component";
import ChartIcon from '~/assets/images/utils/chart.svg?component' import ChartIcon from "~/assets/images/utils/chart.svg?component";
import NavRow from '~/components/ui/NavRow.vue' import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from '~/components/ui/ModalCreation.vue' import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from '~/components/ui/Avatar.vue' import Avatar from "~/components/ui/Avatar.vue";
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts' import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
import { commonMessages } from '~/utils/common-messages.ts' import { commonMessages } from "~/utils/common-messages.ts";
import { DARK_THEMES } from '~/composables/theme.js' import { DARK_THEMES } from "~/composables/theme.js";
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl();
const app = useNuxtApp() const app = useNuxtApp();
const auth = await useAuth() const auth = await useAuth();
const cosmetics = useCosmetics() const cosmetics = useCosmetics();
const flags = useFeatureFlags() const flags = useFeatureFlags();
const tags = useTags() const tags = useTags();
const config = useRuntimeConfig() const config = useRuntimeConfig();
const route = useNativeRoute() const route = useNativeRoute();
const link = config.public.siteUrl + route.path.replace(/\/+$/, '') const link = config.public.siteUrl + route.path.replace(/\/+$/, "");
const verifyEmailBannerMessages = defineMessages({ const verifyEmailBannerMessages = defineMessages({
title: { title: {
id: 'layout.banner.verify-email.title', id: "layout.banner.verify-email.title",
defaultMessage: 'For security purposes, please verify your email address on Modrinth.', defaultMessage: "For security purposes, please verify your email address on Modrinth.",
}, },
action: { action: {
id: 'layout.banner.verify-email.action', id: "layout.banner.verify-email.action",
defaultMessage: 'Re-send verification email', defaultMessage: "Re-send verification email",
}, },
}) });
const addEmailBannerMessages = defineMessages({ const addEmailBannerMessages = defineMessages({
title: { title: {
id: 'layout.banner.add-email.title', id: "layout.banner.add-email.title",
defaultMessage: 'For security purposes, please enter your email on Modrinth.', defaultMessage: "For security purposes, please enter your email on Modrinth.",
}, },
action: { action: {
id: 'layout.banner.add-email.button', id: "layout.banner.add-email.button",
defaultMessage: 'Visit account settings', defaultMessage: "Visit account settings",
}, },
}) });
const stagingBannerMessages = defineMessages({ const stagingBannerMessages = defineMessages({
title: { title: {
id: 'layout.banner.staging.title', id: "layout.banner.staging.title",
defaultMessage: 'Youre viewing Modrinths staging environment.', defaultMessage: "Youre viewing Modrinths staging environment.",
}, },
description: { description: {
id: 'layout.banner.staging.description', id: "layout.banner.staging.description",
defaultMessage: defaultMessage:
'The staging environment is running on a copy of the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance.', "The staging environment is running on a copy of the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance.",
}, },
}) });
const navMenuMessages = defineMessages({ const navMenuMessages = defineMessages({
home: { home: {
id: 'layout.nav.home', id: "layout.nav.home",
defaultMessage: 'Home', defaultMessage: "Home",
}, },
search: { search: {
id: 'layout.nav.search', id: "layout.nav.search",
defaultMessage: 'Search', defaultMessage: "Search",
}, },
}) });
const messages = defineMessages({ const messages = defineMessages({
toggleMenu: { toggleMenu: {
id: 'layout.menu-toggle.action', id: "layout.menu-toggle.action",
defaultMessage: 'Toggle menu', defaultMessage: "Toggle menu",
}, },
yourAvatarAlt: { yourAvatarAlt: {
id: 'layout.avatar.alt', id: "layout.avatar.alt",
defaultMessage: 'Your avatar', defaultMessage: "Your avatar",
}, },
getModrinthApp: { getModrinthApp: {
id: 'layout.action.get-modrinth-app', id: "layout.action.get-modrinth-app",
defaultMessage: 'Get Modrinth App', defaultMessage: "Get Modrinth App",
}, },
changeTheme: { changeTheme: {
id: 'layout.action.change-theme', id: "layout.action.change-theme",
defaultMessage: 'Change theme', defaultMessage: "Change theme",
}, },
}) });
const footerMessages = defineMessages({ const footerMessages = defineMessages({
openSource: { openSource: {
id: 'layout.footer.open-source', id: "layout.footer.open-source",
defaultMessage: 'Modrinth is <github-link>open source</github-link>.', defaultMessage: "Modrinth is <github-link>open source</github-link>.",
}, },
companyTitle: { companyTitle: {
id: 'layout.footer.company.title', id: "layout.footer.company.title",
defaultMessage: 'Company', defaultMessage: "Company",
}, },
terms: { terms: {
id: 'layout.footer.company.terms', id: "layout.footer.company.terms",
defaultMessage: 'Terms', defaultMessage: "Terms",
}, },
privacy: { privacy: {
id: 'layout.footer.company.privacy', id: "layout.footer.company.privacy",
defaultMessage: 'Privacy', defaultMessage: "Privacy",
}, },
rules: { rules: {
id: 'layout.footer.company.rules', id: "layout.footer.company.rules",
defaultMessage: 'Rules', defaultMessage: "Rules",
}, },
careers: { careers: {
id: 'layout.footer.company.careers', id: "layout.footer.company.careers",
defaultMessage: 'Careers', defaultMessage: "Careers",
}, },
resourcesTitle: { resourcesTitle: {
id: 'layout.footer.resources.title', id: "layout.footer.resources.title",
defaultMessage: 'Resources', defaultMessage: "Resources",
}, },
support: { support: {
id: 'layout.footer.resources.support', id: "layout.footer.resources.support",
defaultMessage: 'Support', defaultMessage: "Support",
}, },
blog: { blog: {
id: 'layout.footer.resources.blog', id: "layout.footer.resources.blog",
defaultMessage: 'Blog', defaultMessage: "Blog",
}, },
docs: { docs: {
id: 'layout.footer.resources.docs', id: "layout.footer.resources.docs",
defaultMessage: 'Docs', defaultMessage: "Docs",
}, },
status: { status: {
id: 'layout.footer.resources.status', id: "layout.footer.resources.status",
defaultMessage: 'Status', defaultMessage: "Status",
}, },
interactTitle: { interactTitle: {
id: 'layout.footer.interact.title', id: "layout.footer.interact.title",
defaultMessage: 'Interact', defaultMessage: "Interact",
}, },
legalDisclaimer: { legalDisclaimer: {
id: 'layout.footer.legal-disclaimer', id: "layout.footer.legal-disclaimer",
defaultMessage: defaultMessage:
'NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.', "NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.",
}, },
}) });
useHead({ useHead({
link: [ link: [
{ {
rel: 'canonical', rel: "canonical",
href: link, href: link,
}, },
], ],
}) });
useSeoMeta({ useSeoMeta({
title: 'Modrinth', title: "Modrinth",
description: () => description: () =>
formatMessage({ formatMessage({
id: 'layout.meta.description', id: "layout.meta.description",
defaultMessage: defaultMessage:
'Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. ' + "Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. " +
'Discover and publish projects on Modrinth with a modern, easy to use interface and API.', "Discover and publish projects on Modrinth with a modern, easy to use interface and API.",
}), }),
publisher: 'Modrinth', publisher: "Modrinth",
themeColor: '#1bd96a', themeColor: "#1bd96a",
colorScheme: 'dark light', colorScheme: "dark light",
// OpenGraph // OpenGraph
ogTitle: 'Modrinth', ogTitle: "Modrinth",
ogSiteName: 'Modrinth', ogSiteName: "Modrinth",
ogDescription: () => ogDescription: () =>
formatMessage({ formatMessage({
id: 'layout.meta.og-description', id: "layout.meta.og-description",
defaultMessage: 'Discover and publish Minecraft content!', defaultMessage: "Discover and publish Minecraft content!",
}), }),
ogType: 'website', ogType: "website",
ogImage: 'https://cdn.modrinth.com/modrinth-new.png', ogImage: "https://cdn.modrinth.com/modrinth-new.png",
ogUrl: link, ogUrl: link,
// Twitter // Twitter
twitterCard: 'summary', twitterCard: "summary",
twitterSite: '@modrinth', twitterSite: "@modrinth",
}) });
const developerModeCounter = ref(0) const developerModeCounter = ref(0);
const isDropdownOpen = ref(false) const isDropdownOpen = ref(false);
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false);
const isBrowseMenuOpen = ref(false) const isBrowseMenuOpen = ref(false);
const navRoutes = computed(() => [ const navRoutes = computed(() => [
{ {
label: formatMessage(getProjectTypeMessage('mod', true)), label: formatMessage(getProjectTypeMessage("mod", true)),
href: '/mods', href: "/mods",
}, },
{ {
label: formatMessage(getProjectTypeMessage('plugin', true)), label: formatMessage(getProjectTypeMessage("plugin", true)),
href: '/plugins', href: "/plugins",
}, },
{ {
label: formatMessage(getProjectTypeMessage('datapack', true)), label: formatMessage(getProjectTypeMessage("datapack", true)),
href: '/datapacks', href: "/datapacks",
}, },
{ {
label: formatMessage(getProjectTypeMessage('shader', true)), label: formatMessage(getProjectTypeMessage("shader", true)),
href: '/shaders', href: "/shaders",
}, },
{ {
label: formatMessage(getProjectTypeMessage('resourcepack', true)), label: formatMessage(getProjectTypeMessage("resourcepack", true)),
href: '/resourcepacks', href: "/resourcepacks",
}, },
{ {
label: formatMessage(getProjectTypeMessage('modpack', true)), label: formatMessage(getProjectTypeMessage("modpack", true)),
href: '/modpacks', href: "/modpacks",
}, },
]) ]);
onMounted(() => { onMounted(() => {
if (window && process.client) { if (window && process.client) {
window.history.scrollRestoration = 'auto' window.history.scrollRestoration = "auto";
} }
runAnalytics() runAnalytics();
}) });
watch( watch(
() => route.path, () => route.path,
() => { () => {
isMobileMenuOpen.value = false isMobileMenuOpen.value = false;
isBrowseMenuOpen.value = false isBrowseMenuOpen.value = false;
if (process.client) { if (process.client) {
document.body.style.overflowY = 'scroll' document.body.style.overflowY = "scroll";
document.body.setAttribute('tabindex', '-1') document.body.setAttribute("tabindex", "-1");
document.body.removeAttribute('tabindex') document.body.removeAttribute("tabindex");
} }
updateCurrentDate() updateCurrentDate();
runAnalytics() runAnalytics();
} },
) );
function developerModeIncrement() { function developerModeIncrement() {
if (developerModeCounter.value >= 5) { if (developerModeCounter.value >= 5) {
flags.value.developerMode = !flags.value.developerMode flags.value.developerMode = !flags.value.developerMode;
developerModeCounter.value = 0 developerModeCounter.value = 0;
saveFeatureFlags() saveFeatureFlags();
if (flags.value.developerMode) { if (flags.value.developerMode) {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Developer mode activated', title: "Developer mode activated",
text: 'Developer mode has been enabled', text: "Developer mode has been enabled",
type: 'success', type: "success",
}) });
} else { } else {
app.$notify({ app.$notify({
group: 'main', group: "main",
title: 'Developer mode deactivated', title: "Developer mode deactivated",
text: 'Developer mode has been disabled', text: "Developer mode has been disabled",
type: 'success', type: "success",
}) });
} }
} else { } else {
developerModeCounter.value++ developerModeCounter.value++;
} }
} }
async function logoutUser() { async function logoutUser() {
await logout() await logout();
} }
function runAnalytics() { function runAnalytics() {
const config = useRuntimeConfig() const config = useRuntimeConfig();
const replacedUrl = config.public.apiBaseUrl.replace('v2/', '') const replacedUrl = config.public.apiBaseUrl.replace("v2/", "");
setTimeout(() => { setTimeout(() => {
$fetch(`${replacedUrl}analytics/view`, { $fetch(`${replacedUrl}analytics/view`, {
method: 'POST', method: "POST",
body: { body: {
url: window.location.href, url: window.location.href,
}, },
@ -722,39 +722,39 @@ function runAnalytics() {
}, },
}) })
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {});
}) });
} }
function toggleMobileMenu() { function toggleMobileMenu() {
isMobileMenuOpen.value = !isMobileMenuOpen.value isMobileMenuOpen.value = !isMobileMenuOpen.value;
if (isMobileMenuOpen.value) { if (isMobileMenuOpen.value) {
isBrowseMenuOpen.value = false isBrowseMenuOpen.value = false;
} }
} }
function toggleBrowseMenu() { function toggleBrowseMenu() {
isBrowseMenuOpen.value = !isBrowseMenuOpen.value isBrowseMenuOpen.value = !isBrowseMenuOpen.value;
if (isBrowseMenuOpen.value) { if (isBrowseMenuOpen.value) {
isMobileMenuOpen.value = false isMobileMenuOpen.value = false;
} }
} }
function changeTheme() { function changeTheme() {
updateTheme( updateTheme(
DARK_THEMES.includes(app.$colorMode.value) DARK_THEMES.includes(app.$colorMode.value)
? 'light' ? "light"
: cosmetics.value.preferredDarkTheme ?? 'dark', : cosmetics.value.preferredDarkTheme ?? "dark",
true true,
) );
} }
function hideStagingBanner() { function hideStagingBanner() {
cosmetics.value.hideStagingBanner = true cosmetics.value.hideStagingBanner = true;
saveCosmetics() saveCosmetics();
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import '~/assets/styles/global.scss'; @import "~/assets/styles/global.scss";
// @import '@modrinth/assets'; // @import '@modrinth/assets';
.layout { .layout {
@ -863,7 +863,7 @@ function hideStagingBanner() {
align-items: flex-start; align-items: flex-start;
&--alpha::after { &--alpha::after {
content: 'Alpha'; content: "Alpha";
background-color: var(--color-warning-bg); background-color: var(--color-warning-bg);
color: var(--color-warning-text); color: var(--color-warning-text);
border-radius: 1rem; border-radius: 1rem;
@ -909,7 +909,7 @@ function hideStagingBanner() {
&::after { &::after {
background-color: var(--color-brand); background-color: var(--color-brand);
border-radius: var(--size-rounded-max); border-radius: var(--size-rounded-max);
content: ''; content: "";
height: 0.5rem; height: 0.5rem;
position: absolute; position: absolute;
right: 0.25rem; right: 0.25rem;
@ -941,8 +941,10 @@ function hideStagingBanner() {
outline: none; outline: none;
.user-icon { .user-icon {
height: 2rem; min-height: unset;
width: 2rem; min-width: unset;
height: 2rem !important;
width: 2rem !important;
outline: 2px solid var(--color-raised-bg); outline: 2px solid var(--color-raised-bg);
transition: outline-color 0.1s ease-in-out; transition: outline-color 0.1s ease-in-out;
} }
@ -966,8 +968,11 @@ function hideStagingBanner() {
right: -1rem; right: -1rem;
transform: scaleY(0.9); transform: scaleY(0.9);
transform-origin: top; transform-origin: top;
transition: all 0.1s ease-in-out 0.05s, color 0s ease-in-out 0s, transition:
background-color 0s ease-in-out 0s, border-color 0s ease-in-out 0s; all 0.1s ease-in-out 0.05s,
color 0s ease-in-out 0s,
background-color 0s ease-in-out 0s,
border-color 0s ease-in-out 0s;
visibility: hidden; visibility: hidden;
width: max-content; width: max-content;
z-index: 1; z-index: 1;
@ -1192,7 +1197,7 @@ function hideStagingBanner() {
&::after { &::after {
background-color: var(--color-brand); background-color: var(--color-brand);
border-radius: var(--size-rounded-max); border-radius: var(--size-rounded-max);
content: ''; content: "";
height: 0.5rem; height: 0.5rem;
position: absolute; position: absolute;
left: 1.5rem; left: 1.5rem;
@ -1276,10 +1281,10 @@ function hideStagingBanner() {
text-align: center; text-align: center;
display: grid; display: grid;
grid-template: grid-template:
'logo-info logo-info logo-info' auto "logo-info logo-info logo-info" auto
'links-1 links-2 links-3' auto "links-1 links-2 links-3" auto
'buttons buttons buttons' auto "buttons buttons buttons" auto
'notice notice notice' auto "notice notice notice" auto
/ 1fr 1fr 1fr; / 1fr 1fr 1fr;
max-width: 1280px; max-width: 1280px;
@ -1357,8 +1362,8 @@ function hideStagingBanner() {
display: grid; display: grid;
margin-inline: auto; margin-inline: auto;
grid-template: grid-template:
'logo-info links-1 links-2 links-3 buttons' auto "logo-info links-1 links-2 links-3 buttons" auto
'notice notice notice notice notice' auto; "notice notice notice notice notice" auto;
text-align: unset; text-align: unset;
.logo-info { .logo-info {
@ -1415,7 +1420,7 @@ function hideStagingBanner() {
border-bottom: 2px solid var(--color-red); border-bottom: 2px solid var(--color-red);
display: grid; display: grid;
gap: 0.5rem; gap: 0.5rem;
grid-template: 'title actions' 'description actions'; grid-template: "title actions" "description actions";
padding-block: var(--gap-xl); padding-block: var(--gap-xl);
padding-inline: max(calc((100% - 80rem) / 2 + var(--gap-md)), var(--gap-xl)); padding-inline: max(calc((100% - 80rem) / 2 + var(--gap-md)), var(--gap-xl));

View File

@ -1,27 +1,28 @@
const whitelistedParams = ['flow', 'error'] /* eslint-disable no-undef */
const whitelistedParams = ["flow", "error"];
export default defineNuxtRouteMiddleware(async (_to, from) => { export default defineNuxtRouteMiddleware(async (_to, from) => {
const config = useRuntimeConfig() const config = useRuntimeConfig();
const auth = await useAuth() const auth = await useAuth();
if (!auth.value.user) { if (!auth.value.user) {
const fullPath = from.fullPath const fullPath = from.fullPath;
const url = new URL(fullPath, config.public.apiBaseUrl) const url = new URL(fullPath, config.public.apiBaseUrl);
const extractedParams = whitelistedParams.reduce((acc, param) => { const extractedParams = whitelistedParams.reduce((acc, param) => {
if (url.searchParams.has(param)) { if (url.searchParams.has(param)) {
acc[param] = url.searchParams.get(param) acc[param] = url.searchParams.get(param);
url.searchParams.delete(param) url.searchParams.delete(param);
} }
return acc return acc;
}, {}) }, {});
const redirectPath = encodeURIComponent(url.pathname + url.search) const redirectPath = encodeURIComponent(url.pathname + url.search);
return await navigateTo( return await navigateTo(
{ {
path: '/auth/sign-in', path: "/auth/sign-in",
query: { query: {
redirect: redirectPath, redirect: redirectPath,
...extractedParams, ...extractedParams,
@ -29,7 +30,7 @@ export default defineNuxtRouteMiddleware(async (_to, from) => {
}, },
{ {
replace: true, replace: true,
},
);
} }
) });
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More