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"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"extends": ["@nuxt/eslint-config", "plugin:prettier/recommended", "prettier"],
"env": {
"browser": true,
"node": true
},
"ignorePatterns": [".nuxt/**", ".output/**", "node_modules"],
"overrides": [
{
"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/logo.svg
- components/brand/*
- components/brand/\*
- static/favicon.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:
- assets/images/external/*
- assets/images/external/\*

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{
"name": "@modrinth/frontend",
"private": true,
"type": "module",
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
@ -9,31 +10,36 @@
"postinstall": "nuxi prepare",
"lint:js": "eslint ./src --ext .js,.vue,.ts",
"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"
},
"devDependencies": {
"@formatjs/cli": "^6.1.2",
"@nuxt/devtools": "^1.3.3",
"@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",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vintl/compact-number": "^2.0.5",
"@vintl/how-ago": "^3.0.1",
"@vintl/nuxt": "^1.8.0",
"autoprefixer": "^10.4.19",
"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-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.14.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.27.0",
"glob": "^10.2.7",
"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",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
"vite-plugin-eslint": "^1.8.1",
"vite-svg-loader": "^5.1.0",

View File

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

View File

@ -456,7 +456,7 @@
}
&:disabled,
&[disabled='true'] {
&[disabled="true"] {
cursor: not-allowed;
filter: grayscale(50%);
opacity: 0.5;
@ -502,7 +502,7 @@ tr.button-transparent {
}
&:disabled > *,
&[disabled='true'] > * {
&[disabled="true"] > * {
cursor: not-allowed;
filter: grayscale(50%);
opacity: 0.5;
@ -511,7 +511,10 @@ tr.button-transparent {
}
.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;
&:focus-visible:not(&.disabled),
@ -530,7 +533,7 @@ tr.button-transparent {
box-shadow: none;
&disabled,
&[disabled='true'] {
&[disabled="true"] {
cursor: not-allowed;
box-shadow: none;
}
@ -544,7 +547,9 @@ tr.button-transparent {
color: var(--text-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);
}
@ -560,7 +565,10 @@ tr.button-transparent {
cursor: pointer;
width: 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;
text-decoration: none;
@ -603,7 +611,9 @@ tr.button-transparent {
border-radius: var(--size-rounded-sm);
color: var(--text-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 {
min-width: 1.25rem;
@ -826,7 +836,7 @@ tr.button-transparent {
background: var(--color-button-bg);
&:after {
content: '';
content: "";
position: absolute;
top: 7px;
left: 7px;
@ -1065,7 +1075,9 @@ button {
background: var(--color-button-bg);
width: fit-content;
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;
overflow: hidden;
max-width: 100%;
@ -1099,7 +1111,9 @@ button {
&:focus,
&:focus-visible,
&: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);
}
}
@ -1436,15 +1450,15 @@ svg.inline-svg {
height: var(--_size, var(--icon-16)) !important;
border: 1px solid var(--color-button-border);
&[data-size='32'] {
&[data-size="32"] {
--_size: var(--icon-32);
}
&[data-shape='circle'] {
&[data-shape="circle"] {
border-radius: var(--radius-max) !important;
}
&[data-shape='square'] {
&[data-shape="square"] {
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;
--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-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-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
--landing-color-heading: #000;
@ -341,9 +341,9 @@ html {
--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%),
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-color-heading: #fff;
@ -537,18 +537,22 @@ textarea {
font-weight: var(--font-weight-medium);
border: none;
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;
min-height: 40px;
&:focus,
&: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);
}
&:disabled,
&[disabled='true'] {
&[disabled="true"] {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
@ -565,7 +569,7 @@ textarea {
}
button,
input[type='button'] {
input[type="button"] {
cursor: pointer;
border: none;
outline: 2px solid transparent;
@ -581,13 +585,13 @@ kbd {
font-size: 0.85em !important;
}
@import '~/assets/styles/layout.scss';
@import '~/assets/styles/utils.scss';
@import '~/assets/styles/components.scss';
@import "~/assets/styles/layout.scss";
@import "~/assets/styles/utils.scss";
@import "~/assets/styles/components.scss";
button:focus-visible,
a:focus-visible,
[tabindex='0']:focus-visible {
[tabindex="0"]:focus-visible {
outline: 0.25rem solid #ea80ff;
border-radius: 0.25rem;
}
@ -602,7 +606,10 @@ input {
}
.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;
}

View File

@ -42,9 +42,9 @@
padding: 0 0.75rem;
grid-template:
'sidebar'
'content'
'info'
"sidebar"
"content"
"info"
/ 100%;
@media screen and (max-width: 1024px) {
@ -81,25 +81,25 @@
column-gap: 0.75rem;
grid-template:
'sidebar content' auto
'info content' auto
'dummy content' 1fr
"sidebar content" auto
"info content" auto
"dummy content" 1fr
/ 20rem 1fr;
&.alt-layout {
grid-template:
'content sidebar' auto
'content info' auto
'content dummy' 1fr
"content sidebar" auto
"content info" auto
"content dummy" 1fr
/ 1fr 20rem;
}
&.no-sidebar {
grid-template:
'header header' auto
'content content' auto
'info info' auto
'dummy dummy' 1fr
"header header" auto
"content content" auto
"info info" auto
"dummy dummy" 1fr
/ 1fr 1fr;
.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
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
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>
</svg>
</template>
<script setup>
const loading = useLoading()
const loading = useLoading();
const config = useRuntimeConfig()
const config = useRuntimeConfig();
const api = computed(() => {
const apiUrl = config.public.apiBaseUrl
if (apiUrl.startsWith('https://api.modrinth.com')) {
return 'prod'
} else if (apiUrl.startsWith('https://staging-api.modrinth.com')) {
return 'staging'
} else if (apiUrl.startsWith('localhost') || apiUrl.startsWith('127.0.0.1')) {
return 'localhost'
const apiUrl = config.public.apiBaseUrl;
if (apiUrl.startsWith("https://api.modrinth.com")) {
return "prod";
} else if (apiUrl.startsWith("https://staging-api.modrinth.com")) {
return "staging";
} else if (apiUrl.startsWith("localhost") || apiUrl.startsWith("127.0.0.1")) {
return "localhost";
}
return 'foreign'
})
return "foreign";
});
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,8 +7,8 @@
</template>
<script>
import CheckIcon from '~/assets/images/utils/check.svg?component'
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?component'
import CheckIcon from "~/assets/images/utils/check.svg?component";
import ClipboardCopyIcon from "~/assets/images/utils/clipboard-copy.svg?component";
export default {
components: {
@ -24,15 +24,15 @@ export default {
data() {
return {
copied: false,
}
};
},
methods: {
async copyText() {
await navigator.clipboard.writeText(this.text)
this.copied = true
await navigator.clipboard.writeText(this.text);
this.copied = true;
},
},
}
};
</script>
<style lang="scss" scoped>
@ -48,7 +48,10 @@ export default {
width: min-content;
border-radius: 10px;
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;
span {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -253,7 +253,7 @@
>
<div
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"
>
@ -339,9 +339,9 @@ import {
XIcon as CrossIcon,
EyeOffIcon,
ExitIcon,
} from '@modrinth/assets'
import { MarkdownEditor, OverflowMenu } from '@modrinth/ui'
import Categories from '~/components/ui/search/Categories.vue'
} from "@modrinth/assets";
import { MarkdownEditor, OverflowMenu } from "@modrinth/ui";
import Categories from "~/components/ui/search/Categories.vue";
const props = defineProps({
project: {
@ -357,72 +357,72 @@ const props = defineProps({
required: true,
default: () => {},
},
})
});
const steps = computed(() =>
[
{
id: 'title',
question: 'Is this title free of useless information?',
id: "title",
question: "Is this title free of useless information?",
shown: true,
rules: [
'No unnecessary data (mod loaders, game versions, etc)',
'No emojis / useless text decorators',
"No unnecessary data (mod loaders, game versions, etc)",
"No emojis / useless text decorators",
],
examples: [
'✅ NoobMod [1.8+] • Kill all noobs in your world!',
'[FABRIC] My Optimization Pack',
'[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)',
"✅ NoobMod [1.8+] • Kill all noobs in your world!",
"[FABRIC] My Optimization Pack",
"[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)",
],
exceptions: [
'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 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)",
],
options: [
{
name: 'Contains useless info',
name: "Contains useless info",
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.`,
},
],
},
{
id: 'slug',
question: 'Is the slug accurate and appropriate?',
id: "slug",
question: "Is the slug accurate and appropriate?",
shown: true,
rules: ['Matches title / not misleading (acronyms are OK)'],
rules: ["Matches title / not misleading (acronyms are OK)"],
options: [
{
name: 'Misused',
name: "Misused",
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. `,
},
],
},
{
id: 'summary',
id: "summary",
question: `Is the project's summary sufficient?`,
shown: true,
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 include any markdown formatting.',
"Should not include any markdown formatting.",
],
options: [
{
name: 'Insufficient',
name: "Insufficient",
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.
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
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.`,
},
{
name: 'Formatting',
name: "Formatting",
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.
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?`,
navigate: `/${props.project.project_type}/${props.project.slug}`,
shown: true,
rules: [
'Should answer what the project specifically does or adds ',
'Should answer why someone should want to download the project ',
'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 answer what the project specifically does or adds ",
"Should answer why someone should want to download the project ",
"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)",
],
options: [
{
name: 'Insufficient',
name: "Insufficient",
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.
Currently, it looks like there are some missing details.
%EXPLAINER%`,
fillers: [
{
id: 'EXPLAINER',
question: 'Please elaborate on how the author can improve their description.',
id: "EXPLAINER",
question: "Please elaborate on how the author can improve their description.",
large: true,
},
],
},
{
name: 'Insufficient (default packs)',
name: "Insufficient (default packs)",
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.
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
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.
@ -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
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
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
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
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.`,
},
{
name: 'Non-standard text',
name: "Non-standard text",
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.`,
},
],
},
{
id: 'links',
id: "links",
question: `Are the project's links accessible and not misleading?`,
shown:
props.project.issues_url ||
@ -516,49 +516,49 @@ Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#cle
],
options: [
{
name: 'Links are misused',
name: "Links are misused",
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.`,
},
{
name: 'Not accessible (source)',
name: "Not accessible (source)",
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.
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
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!`,
fillers: [
{
id: 'LINK',
question: 'Please specify the link type that is inaccessible.',
id: "LINK",
question: "Please specify the link type that is inaccessible.",
},
],
},
],
},
{
id: 'categories',
id: "categories",
question: `Are the project's tags/categories accurate?`,
shown: props.project.categories.length > 0 || props.project.additional_categories.length > 0,
options: [
{
name: 'Inaccurate',
name: "Inaccurate",
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.`,
},
],
},
{
id: 'side-types',
id: "side-types",
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: [
{
name: 'Inaccurate (modpack)',
name: "Inaccurate (modpack)",
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.
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.`,
},
{
name: 'Inaccurate (mod)',
name: "Inaccurate (mod)",
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.
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`,
question: `Are the project's gallery images relevant?`,
shown: props.project.gallery.length > 0,
options: [
{
name: 'Not relevant',
name: "Not relevant",
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.`,
},
],
},
{
id: 'versions',
id: "versions",
navigate: `/${props.project.project_type}/${props.project.slug}/versions`,
question: `Are these project's files correct?`,
shown: !['modpack'].includes(props.project.project_type),
shown: !["modpack"].includes(props.project.project_type),
rules: [
'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)',
"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)",
],
options: [
{
name: 'Incorrect additional files',
name: "Incorrect 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\`.
Please upload each version of your mod separately, thank you.`,
},
{
name: 'Invalid file type (modpacks)',
name: "Invalid file type (modpacks)",
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!`,
},
{
name: 'Invalid file type (resourcepacks)',
name: "Invalid file type (resourcepacks)",
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.`,
},
],
},
{
id: 'copyright',
id: "copyright",
question: `Does the author have proper permissions to post this project?`,
shown: true,
rules: [
@ -628,20 +628,20 @@ It looks like you've selected loaders for your Resource Pack that are causing it
],
options: [
{
name: 'Re-upload',
name: "Re-upload",
resultingMessage: `## Reuploads are forbidden
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.
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: [
{
id: 'ORIGINAL_PROJECT',
question: 'What is the title of the original project?',
id: "ORIGINAL_PROJECT",
question: "What is the title of the original project?",
required: true,
},
{
id: 'ORIGINAL_AUTHOR',
question: 'What is the author of the original project?',
id: "ORIGINAL_AUTHOR",
question: "What is the author of the original project?",
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?`,
navigate: `/${props.project.project_type}/${props.project.slug}`,
shown: true,
rules: [
'Should not be a cheat/hack (without a server-side opt-out)',
'Should not contain sexually explicit / inappropriate content',
'Should not be excessively profane',
'Should not promote any illegal activity (including illicit drugs + substances)',
'Anything else infringing of our content rules (see 1.1-12, 3.1-3)',
"Should not be a cheat/hack (without a server-side opt-out)",
"Should not contain sexually explicit / inappropriate content",
"Should not be excessively profane",
"Should not promote any illegal activity (including illicit drugs + substances)",
"Anything else infringing of our content rules (see 1.1-12, 3.1-3)",
],
options: [
{
name: 'No',
name: "No",
resultingMessage: `%MESSAGE%`,
fillers: [
{
id: 'MESSAGE',
question: 'Please explain to the user how it infringes on our content rules.',
id: "MESSAGE",
question: "Please explain to the user how it infringes on our content rules.",
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',
question: 'Modpack permissions',
shown: ['modpack'].includes(props.project.project_type),
id: "modpack-permissions",
question: "Modpack permissions",
shown: ["modpack"].includes(props.project.project_type),
options: [],
},
{
id: 'private-server',
id: "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: [
'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: [
{
name: 'Private server (withhold)',
name: "Private server (withhold)",
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. `,
},
],
},
].filter((x) => x.shown)
)
].filter((x) => x.shown),
);
const currentStepIndex = ref(0)
const selectedOptions = ref({})
const currentStepIndex = ref(0);
const selectedOptions = ref({});
function toggleOption(stepId, option) {
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) {
selectedOptions.value[stepId].push(option)
selectedOptions.value[stepId].push(option);
} else {
selectedOptions.value[stepId].splice(index, 1)
selectedOptions.value[stepId].splice(index, 1);
}
const instance = getCurrentInstance()
instance?.proxy?.$forceUpdate()
const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate();
}
function previousPage() {
currentStepIndex.value -= 1
generatedMessage.value = false
currentStepIndex.value -= 1;
generatedMessage.value = false;
if (steps.value[currentStepIndex.value].navigate) {
navigateTo(steps.value[currentStepIndex.value].navigate)
navigateTo(steps.value[currentStepIndex.value].navigate);
}
}
async function nextPage() {
currentStepIndex.value += 1
currentStepIndex.value += 1;
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') {
await initializeModPackData()
if (steps.value[currentStepIndex.value].id === "modpack-permissions") {
await initializeModPackData();
}
}
async function initializeModPackData() {
startLoading()
startLoading();
try {
const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true })
const projects = []
const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true });
const projects = [];
for (const [hash, fileName] of Object.entries(raw.unknown_files)) {
projects.push({
type: 'unknown',
type: "unknown",
hash,
file_name: fileName,
status: null,
approved: null,
})
});
}
for (const [hash, file] of Object.entries(raw.flame_files)) {
projects.push({
type: 'flame',
type: "flame",
hash,
file_name: file.file_name,
status: null,
@ -764,130 +764,130 @@ async function initializeModPackData() {
id: file.id,
url: file.url,
approved: null,
})
});
}
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({
type: 'identified',
type: "identified",
hash,
file_name: file.file_name,
status: file.status,
approved: null,
})
});
}
}
modPackData.value = projects
modPackData.value = projects;
} catch (err) {
const app = useNuxtApp()
const app = useNuxtApp();
app.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const modPackData = ref(null)
const modPackIndex = ref(0)
const modPackData = ref(null);
const modPackIndex = ref(0);
const fileApprovalTypes = ref([
{
id: 'yes',
name: 'Yes',
id: "yes",
name: "Yes",
},
{
id: 'with-attribution-and-source',
name: 'With attribution and source',
id: "with-attribution-and-source",
name: "With attribution and source",
},
{
id: 'with-attribution',
name: 'With attribution',
id: "with-attribution",
name: "With attribution",
},
{
id: 'no',
name: 'No',
id: "no",
name: "No",
},
{
id: 'permanent-no',
name: 'Permanent no',
id: "permanent-no",
name: "Permanent no",
},
{
id: 'unidentified',
name: 'Unidentified',
id: "unidentified",
name: "Unidentified",
},
])
]);
const filePermissionTypes = ref([
{
id: true,
name: 'Yes',
name: "Yes",
},
{
id: false,
name: 'No',
name: "No",
},
])
]);
const message = ref('')
const generatedMessage = ref(false)
const loadingMessage = ref(false)
const message = ref("");
const generatedMessage = ref(false);
const loadingMessage = ref(false);
async function generateMessage() {
message.value = ''
loadingMessage.value = true
message.value = "";
loadingMessage.value = true;
function printMods(mods, msg) {
if (mods.length === 0) {
return
return;
}
message.value += msg
message.value += '\n\n'
message.value += msg;
message.value += "\n\n";
for (const mod of mods) {
message.value += `- ${mod}\n`
message.value += `- ${mod}\n`;
}
}
if (modPackData.value && modPackData.value.length > 0) {
const updateProjects = {}
const updateProjects = {};
const attributeMods = []
const noMods = []
const permanentNoMods = []
const unidentifiedMods = []
const attributeMods = [];
const noMods = [];
const permanentNoMods = [];
const unidentifiedMods = [];
for (const project of modPackData.value) {
if (project.type === 'unknown') {
if (project.type === "unknown") {
updateProjects[project.hash] = {
type: 'unknown',
type: "unknown",
status: project.status,
proof: project.proof,
title: project.title,
link: project.url,
}
};
}
if (project.type === 'flame') {
if (project.type === "flame") {
updateProjects[project.hash] = {
type: 'flame',
type: "flame",
status: project.status,
id: project.id,
link: project.url,
title: project.title,
}
};
}
if (project.status === 'with-attribution' && !project.approved) {
attributeMods.push(project.file_name)
} else if (project.status === 'unidentified' && !project.approved) {
unidentifiedMods.push(project.file_name)
} else if (project.status === 'no' && !project.approved) {
noMods.push(project.file_name)
} else if (project.status === 'permanent-no') {
permanentNoMods.push(project.file_name)
if (project.status === "with-attribution" && !project.approved) {
attributeMods.push(project.file_name);
} else if (project.status === "unidentified" && !project.approved) {
unidentifiedMods.push(project.file_name);
} else if (project.status === "no" && !project.approved) {
noMods.push(project.file_name);
} else if (project.status === "permanent-no") {
permanentNoMods.push(project.file_name);
}
}
@ -895,17 +895,17 @@ async function generateMessage() {
try {
await useBaseFetch(`moderation/project`, {
internal: true,
method: 'POST',
method: "POST",
body: updateProjects,
})
});
} catch (err) {
const app = useNuxtApp()
const app = useNuxtApp();
app.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
}
@ -915,106 +915,109 @@ async function generateMessage() {
permanentNoMods.length > 0 ||
unidentifiedMods.length > 0
) {
message.value += '## Copyrighted Content \n'
message.value += "## Copyrighted Content \n";
printMods(
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(
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(
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(
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 option of options) {
let addonMessage = option.resultingMessage
let addonMessage = option.resultingMessage;
if (option.fillers && option.fillers.length > 0) {
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 += '\n\n'
message.value += addonMessage;
message.value += "\n\n";
}
}
generatedMessage.value = true
loadingMessage.value = false
currentStepIndex.value += 1
await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`)
generatedMessage.value = true;
loadingMessage.value = false;
currentStepIndex.value += 1;
await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`);
}
const done = ref(false)
const done = ref(false);
async function sendMessage(status) {
startLoading()
startLoading();
try {
await useBaseFetch(`project/${props.project.id}`, {
method: 'PATCH',
method: "PATCH",
body: {
status,
},
})
});
if (message.value) {
await useBaseFetch(`thread/${props.project.thread_id}`, {
method: 'POST',
method: "POST",
body: {
body: {
type: 'text',
type: "text",
body: message.value,
},
},
})
});
}
await props.resetProject()
done.value = true
await props.resetProject();
done.value = true;
} catch (err) {
const app = useNuxtApp()
const app = useNuxtApp();
app.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const router = useNativeRouter()
const router = useNativeRouter();
async function goToNextProject() {
const project = props.futureProjects[0]
const project = props.futureProjects[0];
if (!project) {
await navigateTo('/moderation/review')
await navigateTo("/moderation/review");
}
await router.push({
name: 'type-id',
name: "type-id",
params: {
type: 'project',
type: "project",
id: project,
},
state: {
showChecklist: true,
projects: props.futureProjects.slice(1),
},
})
});
}
</script>
@ -1042,7 +1045,9 @@ async function goToNextProject() {
.option-selected {
color: var(--color-contrast);
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/compact-number'
import type { IntlController } from '@vintl/vintl/controller'
import { createFormatter, type Formatter } from "@vintl/compact-number";
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 {
const vintl = useVIntl()
const vintl = useVIntl();
let formatter = formatters.get(vintl)
let formatter = formatters.get(vintl);
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter);
}
return formatter
return formatter;
}

View File

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

View File

@ -1,17 +1,18 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
/* eslint-disable no-undef */
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 = () => {
const currentDate = useCurrentDate()
const currentDate = useCurrentDate();
currentDate.value = Date.now()
}
currentDate.value = Date.now();
};
export const fromNow = (date) => {
const currentDate = useCurrentDate()
return dayjs(date).from(currentDate.value)
}
const currentDate = useCurrentDate();
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) {
let safeTag = safeTags.get(locale)
let safeTag = safeTags.get(locale);
if (safeTag == null) {
safeTag = new Intl.Locale(locale).baseName
safeTags.set(locale, safeTag)
safeTag = new Intl.Locale(locale).baseName;
safeTags.set(locale, safeTag);
}
return safeTag
return safeTag;
}
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) {
return JSON.stringify({ ...options, locale })
return JSON.stringify({ ...options, locale });
}
export function createDisplayNames(
locale: string,
options: Intl.DisplayNamesOptions = { type: 'language' }
options: Intl.DisplayNamesOptions = { type: "language" },
) {
const wrapperKey = getWrapperKey(locale, options)
let wrapper = displayNamesDicts.get(wrapperKey)
const wrapperKey = getWrapperKey(locale, options);
let wrapper = displayNamesDicts.get(wrapperKey);
if (wrapper == null) {
const dict = new Intl.DisplayNames(locale, options)
const dict = new Intl.DisplayNames(locale, options);
const badTags: string[] = []
const badTags: string[] = [];
wrapper = {
resolvedOptions() {
return dict.resolvedOptions()
return dict.resolvedOptions();
},
of(tag: string) {
let attempt = 0
let attempt = 0;
// eslint-disable-next-line no-labels
lookupLoop: do {
let lookup: string
let lookup: string;
switch (attempt) {
case 0:
lookup = tag
break
lookup = tag;
break;
case 1:
lookup = safeTagFor(tag)
break
lookup = safeTagFor(tag);
break;
default:
// eslint-disable-next-line no-labels
break lookupLoop
break lookupLoop;
}
if (badTags.includes(lookup)) continue
if (badTags.includes(lookup)) continue;
try {
return dict.of(lookup)
return dict.of(lookup);
} catch (err) {
console.warn(
`Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale
}`
)
badTags.push(lookup)
continue
}`,
);
badTags.push(lookup);
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(
locale: AutoRef<string>,
options?: AutoRef<Intl.DisplayNamesOptions | undefined>
options?: AutoRef<Intl.DisplayNamesOptions | undefined>,
) {
const $locale = useAutoRef(locale)
const $options = useAutoRef(options)
const $locale = useAutoRef(locale);
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 DarkColorTheme = 'dark' | 'oled' | 'retro'
export type ProjectDisplayMode = "list" | "grid" | "gallery";
export type DarkColorTheme = "dark" | "oled" | "retro";
export interface NumberFlag {
min: number
max: number
min: 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({
// Developer flags
@ -48,58 +48,58 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
// dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: '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 = {
[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 = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
sameSite: "lax",
secure: true,
httpOnly: false,
path: '/',
} satisfies CookieOptions<PartialFeatureFlags>
path: "/",
} satisfies CookieOptions<PartialFeatureFlags>;
export const useFeatureFlags = () =>
useState<AllFeatureFlags>('featureFlags', () => {
const config = useRuntimeConfig()
useState<AllFeatureFlags>("featureFlags", () => {
const config = useRuntimeConfig();
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
const savedFlags = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
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) {
if (key in flags) {
const flag = key as FeatureFlag
const value = overrides[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
const flag = key as FeatureFlag;
const value = overrides[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value;
}
}
for (const key in savedFlags.value) {
if (key in flags) {
const flag = key as FeatureFlag
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
const flag = key as FeatureFlag;
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value;
}
}
return flags
})
return flags;
});
export const saveFeatureFlags = () => {
const flags = useFeatureFlags()
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
cookie.value = flags.value
}
const flags = useFeatureFlags();
const cookie = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
cookie.value = flags.value;
};

View File

@ -1,36 +1,37 @@
/* eslint-disable no-undef */
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig()
let base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
const config = useRuntimeConfig();
let base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl;
if (!options.headers) {
options.headers = {}
options.headers = {};
}
if (process.server) {
options.headers['x-ratelimit-key'] = config.rateLimitKey
options.headers["x-ratelimit-key"] = config.rateLimitKey;
}
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) {
// 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
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) {
base = base.replace(baseVersion[0], replaceStr)
base = base.replace(baseVersion[0], replaceStr);
} 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 type { IntlController } from '@vintl/vintl/controller'
import { createFormatter, type Formatter } from "@vintl/how-ago";
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 {
const vintl = useVIntl()
const vintl = useVIntl();
let formatter = formatters.get(vintl)
let formatter = formatters.get(vintl);
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter);
}
return formatter
return formatter;
}

View File

@ -1,46 +1,46 @@
type ImageUploadContext = {
projectID?: string
context: 'project' | 'version' | 'thread_message' | 'report'
}
projectID?: string;
context: "project" | "version" | "thread_message" | "report";
};
interface ImageUploadResponse {
id: string
url: string
id: string;
url: string;
}
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
if (
!file.type.startsWith('image/') ||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1])
!file.type.startsWith("image/") ||
!["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
if (file.size > 1024 * 1024) {
throw new Error('File is too large')
throw new Error("File is too large");
}
const qs = new URLSearchParams()
if (ctx.projectID) qs.set('project_id', ctx.projectID)
qs.set('context', ctx.context)
qs.set('ext', file.type.split('/')[1])
const url = `image?${qs.toString()}`
const qs = new URLSearchParams();
if (ctx.projectID) qs.set("project_id", ctx.projectID);
qs.set("context", ctx.context);
qs.set("ext", file.type.split("/")[1]);
const url = `image?${qs.toString()}`;
const response = (await useBaseFetch(url, {
method: 'POST',
method: "POST",
body: file,
apiVersion: 3,
})) as ImageUploadResponse
})) as ImageUploadResponse;
// Type check to see if response has a url property and an id property
if (!response?.id || typeof response.id !== 'string') {
throw new Error('Unexpected response from server')
if (!response?.id || typeof response.id !== "string") {
throw new Error("Unexpected response from server");
}
if (!response?.url || typeof response.url !== 'string') {
throw new Error('Unexpected response from server')
if (!response?.url || typeof response.url !== "string") {
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 = () => {
const loading = useLoading()
const loading = useLoading();
loading.value = true
}
loading.value = true;
};
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) => {
const notifications = useNotifications()
const notifications = useNotifications();
const existingNotif = notifications.value.find(
(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) {
setNotificationTimer(existingNotif)
setNotificationTimer(existingNotif);
return
return;
}
notification.id = new Date()
notification.id = new Date();
setNotificationTimer(notification)
notifications.value.push(notification)
}
setNotificationTimer(notification);
notifications.value.push(notification);
};
export const setNotificationTimer = (notification) => {
if (!notification) return
if (!notification) return;
const notifications = useNotifications()
const notifications = useNotifications();
if (notification.timer) {
clearTimeout(notification.timer)
clearTimeout(notification.timer);
}
notification.timer = setTimeout(() => {
notifications.value.splice(notifications.value.indexOf(notification), 1)
}, 30000)
}
notifications.value.splice(notifications.value.indexOf(notification), 1);
}, 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) => {
if (typeof x === 'string' || x instanceof String) {
return [x]
if (typeof x === "string" || x instanceof String) {
return [x];
} 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.
*
* @param {string?} key The key of the route param to extract.
* @returns {import('vue').Ref<string | string[] | undefined>}
*/
export const useRouteId = (key = 'id') => {
const route = useNativeRoute()
return route.params?.[key] || undefined
}
export const useRouteId = (key = "id") => {
const route = useNativeRoute();
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 = () =>
useState('tags', () => ({
useState("tags", () => ({
categories: tags.categories,
loaders: tags.loaders,
gameVersions: tags.gameVersions,
@ -9,56 +10,56 @@ export const useTags = () =>
reportTypes: tags.reportTypes,
projectTypes: [
{
actual: 'mod',
id: 'mod',
display: 'mod',
actual: "mod",
id: "mod",
display: "mod",
},
{
actual: 'mod',
id: 'plugin',
display: 'plugin',
actual: "mod",
id: "plugin",
display: "plugin",
},
{
actual: 'mod',
id: 'datapack',
display: 'data pack',
actual: "mod",
id: "datapack",
display: "data pack",
},
{
actual: 'shader',
id: 'shader',
display: 'shader',
actual: "shader",
id: "shader",
display: "shader",
},
{
actual: 'resourcepack',
id: 'resourcepack',
display: 'resource pack',
actual: "resourcepack",
id: "resourcepack",
display: "resource pack",
},
{
actual: 'modpack',
id: 'modpack',
display: 'modpack',
actual: "modpack",
id: "modpack",
display: "modpack",
},
],
loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
pluginLoaders: ["bukkit", "spigot", "paper", "purpur", "sponge", "folia"],
pluginPlatformLoaders: ["bungeecord", "waterfall", "velocity"],
allPluginLoaders: [
'bukkit',
'spigot',
'paper',
'purpur',
'sponge',
'bungeecord',
'waterfall',
'velocity',
'folia',
"bukkit",
"spigot",
"paper",
"purpur",
"sponge",
"bungeecord",
"waterfall",
"velocity",
"folia",
],
dataPackLoaders: ['datapack'],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'],
hiddenModLoaders: ['liteloader', 'modloader', 'rift'],
dataPackLoaders: ["datapack"],
modLoaders: ["forge", "fabric", "quilt", "liteloader", "modloader", "rift", "neoforge"],
hiddenModLoaders: ["liteloader", "modloader", "rift"],
},
projectViewModes: ['list', 'grid', 'gallery'],
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
rejectedStatuses: ['rejected', 'withheld'],
staffRoles: ['moderator', 'admin'],
}))
projectViewModes: ["list", "grid", "gallery"],
approvedStatuses: ["approved", "archived", "unlisted", "private"],
rejectedStatuses: ["rejected", "withheld"],
staffRoles: ["moderator", "admin"],
}));

View File

@ -1,58 +1,59 @@
/* eslint-disable no-undef */
export const useTheme = () =>
useState('theme', () => {
const colorMode = useCookie('color-mode', {
useState("theme", () => {
const colorMode = useCookie("color-mode", {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
sameSite: "lax",
secure: true,
httpOnly: false,
path: '/',
})
path: "/",
});
if (!colorMode.value) {
colorMode.value = {
value: 'dark',
preference: 'system',
}
value: "dark",
preference: "system",
};
}
if (colorMode.value.preference !== 'system') {
colorMode.value.value = colorMode.value.preference
if (colorMode.value.preference !== "system") {
colorMode.value.value = colorMode.value.preference;
}
return colorMode.value
})
return colorMode.value;
});
export const updateTheme = (value, updatePreference = false) => {
const theme = useTheme()
const cosmetics = useCosmetics()
const theme = useTheme();
const cosmetics = useCosmetics();
const themeCookie = useCookie('color-mode', {
const themeCookie = useCookie("color-mode", {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
sameSite: "lax",
secure: true,
httpOnly: false,
path: '/',
})
path: "/",
});
if (value === 'system') {
theme.value.preference = 'system'
if (value === "system") {
theme.value.preference = "system";
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: light)')
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: light)");
if (colorSchemeQueryList.matches) {
theme.value.value = 'light'
theme.value.value = "light";
} else {
theme.value.value = cosmetics.value.preferredDarkTheme
theme.value.value = cosmetics.value.preferredDarkTheme;
}
} else {
theme.value.value = value
if (updatePreference) theme.value.preference = value
theme.value.value = value;
if (updatePreference) theme.value.preference = value;
}
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 ErrorFunction = (err: any) => void | Promise<void>
type VoidFunction = () => void | Promise<void>
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
type ErrorFunction = (err: any) => void | Promise<void>;
type VoidFunction = () => void | Promise<void>;
type useClientTry = <TArgs extends any[], TResult>(
fn: AsyncFunction<TArgs, TResult>,
onFail?: ErrorFunction,
onFinish?: VoidFunction
) => (...args: TArgs) => Promise<TResult | undefined>
onFinish?: VoidFunction,
) => (...args: TArgs) => Promise<TResult | undefined>;
const defaultOnError: ErrorFunction = (error) => {
addNotification({
group: 'main',
title: 'An error occurred',
text: error?.data?.description || error.message || error || 'Unknown error',
type: 'error',
})
}
group: "main",
title: "An error occurred",
text: error?.data?.description || error.message || error || "Unknown error",
type: "error",
});
};
export const useClientTry: useClientTry =
(fn, onFail = defaultOnError, onFinish) =>
async (...args) => {
startLoading()
startLoading();
try {
return await fn(...args)
return await fn(...args);
} catch (err) {
if (onFail) {
await onFail(err)
await onFail(err);
} else {
console.error('[CLIENT TRY ERROR]', err)
console.error("[CLIENT TRY ERROR]", err);
}
} finally {
if (onFinish) await onFinish()
stopLoading()
}
if (onFinish) await onFinish();
stopLoading();
}
};

View File

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

View File

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

View File

@ -4,7 +4,7 @@
* @returns Whether any of the modifier keys is pressed.
*/
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) => {
const { maxSize, alertOnInvalid } = validationOptions
const { maxSize, alertOnInvalid } = validationOptions;
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
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) => {
switch (projectType) {
case 'mod':
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
case 'plugin':
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
case 'resourcepack':
return '.zip,application/zip'
case 'shader':
return '.zip,application/zip'
case 'datapack':
return '.zip,application/zip'
case 'modpack':
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
case "mod":
return ".jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip";
case "plugin":
return ".jar,.zip,application/java-archive,application/x-java-archive,application/zip";
case "resourcepack":
return ".zip,application/zip";
case "shader":
return ".zip,application/zip";
case "datapack":
return ".zip,application/zip";
case "modpack":
return ".mrpack,application/x-modrinth-modpack+zip,application/zip";
default:
return '*'
}
return "*";
}
};

View File

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

View File

@ -1,154 +1,154 @@
import TOML from '@ltd/j-toml'
import JSZip from 'jszip'
import yaml from 'js-yaml'
import { satisfies } from 'semver'
import TOML from "@ltd/j-toml";
import JSZip from "jszip";
import yaml from "js-yaml";
import { satisfies } from "semver";
export const inferVersionInfo = async function (rawFile, project, gameVersions) {
function versionType(number) {
if (number.includes('alpha')) {
return 'alpha'
if (number.includes("alpha")) {
return "alpha";
} else if (
number.includes('beta') ||
number.includes("beta") ||
number.match(/[^A-z](rc)[^A-z]/) || // includes `rc`
number.match(/[^A-z](pre)[^A-z]/) // includes `pre`
) {
return 'beta'
return "beta";
} else {
return 'release'
return "release";
}
}
function getGameVersionsMatchingSemverRange(range, gameVersions) {
if (!range) {
return []
return [];
}
const ranges = Array.isArray(range) ? range : [range]
const ranges = Array.isArray(range) ? range : [range];
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)
return ranges.some((v) => satisfies(semverVersion, v))
})
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));
});
}
function getGameVersionsMatchingMavenRange(range, gameVersions) {
if (!range) {
return []
return [];
}
const ranges = []
const ranges = [];
while (range.startsWith('[') || range.startsWith('(')) {
let index = range.indexOf(')')
const index2 = range.indexOf(']')
while (range.startsWith("[") || range.startsWith("(")) {
let index = range.indexOf(")");
const index2 = range.indexOf("]");
if (index === -1 || (index2 !== -1 && index2 < index)) {
index = index2
index = index2;
}
if (index === -1) break
ranges.push(range.substring(0, index + 1))
range = range.substring(index + 1).trim()
if (range.startsWith(',')) {
range = range.substring(1).trim()
if (index === -1) break;
ranges.push(range.substring(0, index + 1));
range = range.substring(index + 1).trim();
if (range.startsWith(",")) {
range = range.substring(1).trim();
}
}
if (range) {
ranges.push(range)
ranges.push(range);
}
const LESS_THAN_EQUAL = /^\(,(.*)]$/
const LESS_THAN = /^\(,(.*)\)$/
const EQUAL = /^\[(.*)]$/
const GREATER_THAN_EQUAL = /^\[(.*),\)$/
const GREATER_THAN = /^\((.*),\)$/
const BETWEEN = /^\((.*),(.*)\)$/
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/
const LESS_THAN_EQUAL = /^\(,(.*)]$/;
const LESS_THAN = /^\(,(.*)\)$/;
const EQUAL = /^\[(.*)]$/;
const GREATER_THAN_EQUAL = /^\[(.*),\)$/;
const GREATER_THAN = /^\((.*),\)$/;
const BETWEEN = /^\((.*),(.*)\)$/;
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/;
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/;
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/;
const semverRanges = []
const semverRanges = [];
for (const range of ranges) {
let result
let result;
if ((result = range.match(LESS_THAN_EQUAL))) {
semverRanges.push(`<=${result[1]}`)
semverRanges.push(`<=${result[1]}`);
} else if ((result = range.match(LESS_THAN))) {
semverRanges.push(`<${result[1]}`)
semverRanges.push(`<${result[1]}`);
} else if ((result = range.match(EQUAL))) {
semverRanges.push(`${result[1]}`)
semverRanges.push(`${result[1]}`);
} else if ((result = range.match(GREATER_THAN_EQUAL))) {
semverRanges.push(`>=${result[1]}`)
semverRanges.push(`>=${result[1]}`);
} else if ((result = range.match(GREATER_THAN))) {
semverRanges.push(`>${result[1]}`)
semverRanges.push(`>${result[1]}`);
} 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))) {
semverRanges.push(`>=${result[1]} <=${result[2]}`)
semverRanges.push(`>=${result[1]} <=${result[2]}`);
} 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))) {
semverRanges.push(`>=${result[1]} <${result[2]}`)
semverRanges.push(`>=${result[1]} <${result[2]}`);
}
}
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions)
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions);
}
const simplifiedGameVersions = gameVersions
.filter((it) => it.version_type === 'release')
.map((it) => it.version)
.filter((it) => it.version_type === "release")
.map((it) => it.version);
const inferFunctions = {
// Forge 1.13+ and NeoForge
'META-INF/mods.toml': async (file, zip) => {
const metadata = TOML.parse(file, { joiner: '\n' })
"META-INF/mods.toml": async (file, zip) => {
const metadata = TOML.parse(file, { joiner: "\n" });
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
const manifestFile = zip.file('META-INF/MANIFEST.MF')
const manifestFile = zip.file("META-INF/MANIFEST.MF");
if (
// 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
) {
const manifestText = await manifestFile.async('text')
const regex = /Implementation-Version: (.*)$/m
const match = manifestText.match(regex)
const manifestText = await manifestFile.async("text");
const regex = /Implementation-Version: (.*)$/m;
const match = manifestText.match(regex);
if (match) {
// 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)
.flat()
.filter((dependency) => dependency.modId === 'minecraft')
.filter((dependency) => dependency.modId === "minecraft");
if (mcDependencies.length > 0) {
gameVersions = getGameVersionsMatchingMavenRange(
mcDependencies[0].versionRange,
simplifiedGameVersions
)
simplifiedGameVersions,
);
}
const hasNeoForge =
Object.values(metadata.dependencies)
.flat()
.filter((dependency) => dependency.modId === 'neoforge').length > 0
.filter((dependency) => dependency.modId === "neoforge").length > 0;
const hasForge =
Object.values(metadata.dependencies)
.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
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 (hasForge || isOlderThan1202) loaders.push('forge')
if (hasNeoForge) loaders.push("neoforge");
if (hasForge || isOlderThan1202) loaders.push("forge");
return {
name: `${project.title} ${versionNum}`,
@ -156,61 +156,61 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
version_type: versionType(versionNum),
loaders,
game_versions: gameVersions,
}
};
} else {
return {}
return {};
}
},
// Old Forge
'mcmod.info': (file) => {
const metadata = JSON.parse(file)
"mcmod.info": (file) => {
const metadata = JSON.parse(file);
return {
name: metadata.version ? `${project.title} ${metadata.version}` : '',
name: metadata.version ? `${project.title} ${metadata.version}` : "",
version_number: metadata.version,
version_type: versionType(metadata.version),
loaders: ['forge'],
loaders: ["forge"],
game_versions: simplifiedGameVersions.filter((version) =>
version.startsWith(metadata.mcversion)
version.startsWith(metadata.mcversion),
),
}
};
},
// Fabric
'fabric.mod.json': (file) => {
const metadata = JSON.parse(file)
"fabric.mod.json": (file) => {
const metadata = JSON.parse(file);
return {
name: `${project.title} ${metadata.version}`,
version_number: metadata.version,
loaders: ['fabric'],
loaders: ["fabric"],
version_type: versionType(metadata.version),
game_versions: metadata.depends
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
: [],
}
};
},
// Quilt
'quilt.mod.json': (file) => {
const metadata = JSON.parse(file)
"quilt.mod.json": (file) => {
const metadata = JSON.parse(file);
return {
name: `${project.title} ${metadata.quilt_loader.version}`,
version_number: metadata.quilt_loader.version,
loaders: ['quilt'],
loaders: ["quilt"],
version_type: versionType(metadata.quilt_loader.version),
game_versions: metadata.quilt_loader.depends
? getGameVersionsMatchingSemverRange(
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")
? metadata.quilt_loader.depends.find((x) => x.id === "minecraft").versions
: [],
simplifiedGameVersions
simplifiedGameVersions,
)
: [],
}
};
},
// Bukkit + Other Forks
'plugin.yml': (file) => {
const metadata = yaml.load(file)
"plugin.yml": (file) => {
const metadata = yaml.load(file);
return {
name: `${project.title} ${metadata.version}`,
@ -220,65 +220,65 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
loaders: [],
game_versions: gameVersions
.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),
}
};
},
// Paper 1.19.3+
'paper-plugin.yml': (file) => {
const metadata = yaml.load(file)
"paper-plugin.yml": (file) => {
const metadata = yaml.load(file);
return {
name: `${project.title} ${metadata.version}`,
version_number: metadata.version,
version_type: versionType(metadata.version),
loaders: ['paper'],
loaders: ["paper"],
game_versions: gameVersions
.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),
}
};
},
// Bungeecord + Waterfall
'bungee.yml': (file) => {
const metadata = yaml.load(file)
"bungee.yml": (file) => {
const metadata = yaml.load(file);
return {
name: `${project.title} ${metadata.version}`,
version_number: metadata.version,
version_type: versionType(metadata.version),
loaders: ['bungeecord'],
}
loaders: ["bungeecord"],
};
},
// Velocity
'velocity-plugin.json': (file) => {
const metadata = JSON.parse(file)
"velocity-plugin.json": (file) => {
const metadata = JSON.parse(file);
return {
name: `${project.title} ${metadata.version}`,
version_number: metadata.version,
version_type: versionType(metadata.version),
loaders: ['velocity'],
}
loaders: ["velocity"],
};
},
// Modpacks
'modrinth.index.json': (file) => {
const metadata = JSON.parse(file)
"modrinth.index.json": (file) => {
const metadata = JSON.parse(file);
const loaders = []
if ('forge' in metadata.dependencies) {
loaders.push('forge')
const loaders = [];
if ("forge" in metadata.dependencies) {
loaders.push("forge");
}
if ('neoforge' in metadata.dependencies) {
loaders.push('neoforge')
if ("neoforge" in metadata.dependencies) {
loaders.push("neoforge");
}
if ('fabric-loader' in metadata.dependencies) {
loaders.push('fabric')
if ("fabric-loader" in metadata.dependencies) {
loaders.push("fabric");
}
if ('quilt-loader' in metadata.dependencies) {
loaders.push('quilt')
if ("quilt-loader" in metadata.dependencies) {
loaders.push("quilt");
}
return {
@ -289,106 +289,106 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
game_versions: gameVersions
.filter((x) => x.version === metadata.dependencies.minecraft)
.map((x) => x.version),
}
};
},
// Resource Packs + Data Packs
'pack.mcmeta': (file) => {
const metadata = JSON.parse(file)
"pack.mcmeta": (file) => {
const metadata = JSON.parse(file);
function getRange(versionA, versionB) {
const startingIndex = gameVersions.findIndex((x) => x.version === versionA)
const endingIndex = gameVersions.findIndex((x) => x.version === versionB)
const startingIndex = gameVersions.findIndex((x) => x.version === versionA);
const endingIndex = gameVersions.findIndex((x) => x.version === versionB);
const final = []
const filterOnlyRelease = gameVersions[startingIndex].version_type === 'release'
const final = [];
const filterOnlyRelease = gameVersions[startingIndex].version_type === "release";
for (let i = startingIndex; i >= endingIndex; i--) {
if (gameVersions[i].version_type === 'release' || !filterOnlyRelease) {
final.push(gameVersions[i].version)
if (gameVersions[i].version_type === "release" || !filterOnlyRelease) {
final.push(gameVersions[i].version);
}
}
return final
return final;
}
const loaders = []
let newGameVersions = []
const loaders = [];
let newGameVersions = [];
if (project.actualProjectType === 'mod') {
loaders.push('datapack')
if (project.actualProjectType === "mod") {
loaders.push("datapack");
switch (metadata.pack.pack_format) {
case 4:
newGameVersions = getRange('1.13', '1.14.4')
break
newGameVersions = getRange("1.13", "1.14.4");
break;
case 5:
newGameVersions = getRange('1.15', '1.16.1')
break
newGameVersions = getRange("1.15", "1.16.1");
break;
case 6:
newGameVersions = getRange('1.16.2', '1.16.5')
break
newGameVersions = getRange("1.16.2", "1.16.5");
break;
case 7:
newGameVersions = getRange('1.17', '1.17.1')
break
newGameVersions = getRange("1.17", "1.17.1");
break;
case 8:
newGameVersions = getRange('1.18', '1.18.1')
break
newGameVersions = getRange("1.18", "1.18.1");
break;
case 9:
newGameVersions.push('1.18.2')
break
newGameVersions.push("1.18.2");
break;
case 10:
newGameVersions = getRange('1.19', '1.19.3')
break
newGameVersions = getRange("1.19", "1.19.3");
break;
case 11:
newGameVersions = getRange('23w03a', '23w05a')
break
newGameVersions = getRange("23w03a", "23w05a");
break;
case 12:
newGameVersions.push('1.19.4')
break
newGameVersions.push("1.19.4");
break;
default:
}
}
if (project.actualProjectType === 'resourcepack') {
loaders.push('minecraft')
if (project.actualProjectType === "resourcepack") {
loaders.push("minecraft");
switch (metadata.pack.pack_format) {
case 1:
newGameVersions = getRange('1.6.1', '1.8.9')
break
newGameVersions = getRange("1.6.1", "1.8.9");
break;
case 2:
newGameVersions = getRange('1.9', '1.10.2')
break
newGameVersions = getRange("1.9", "1.10.2");
break;
case 3:
newGameVersions = getRange('1.11', '1.12.2')
break
newGameVersions = getRange("1.11", "1.12.2");
break;
case 4:
newGameVersions = getRange('1.13', '1.14.4')
break
newGameVersions = getRange("1.13", "1.14.4");
break;
case 5:
newGameVersions = getRange('1.15', '1.16.1')
break
newGameVersions = getRange("1.15", "1.16.1");
break;
case 6:
newGameVersions = getRange('1.16.2', '1.16.5')
break
newGameVersions = getRange("1.16.2", "1.16.5");
break;
case 7:
newGameVersions = getRange('1.17', '1.17.1')
break
newGameVersions = getRange("1.17", "1.17.1");
break;
case 8:
newGameVersions = getRange('1.18', '1.18.2')
break
newGameVersions = getRange("1.18", "1.18.2");
break;
case 9:
newGameVersions = getRange('1.19', '1.19.2')
break
newGameVersions = getRange("1.19", "1.19.2");
break;
case 11:
newGameVersions = getRange('22w42a', '22w44a')
break
newGameVersions = getRange("22w42a", "22w44a");
break;
case 12:
newGameVersions.push('1.19.3')
break
newGameVersions.push("1.19.3");
break;
case 13:
newGameVersions.push('1.19.4')
break
newGameVersions.push("1.19.4");
break;
default:
}
}
@ -396,20 +396,20 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
return {
loaders,
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) {
const file = zip.file(fileName)
const file = zip.file(fileName);
if (file !== null) {
const text = await file.async('text')
return inferFunctions[fileName](text, zip)
}
const text = await file.async("text");
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) {
if (ids.length === 0) {
return []
return [];
}
const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}`
return await useBaseFetch(url, { apiVersion })
const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}`;
return await useBaseFetch(url, { apiVersion });
}
export async function fetchExtraNotificationData(notifications) {
@ -17,154 +18,154 @@ export async function fetchExtraNotificationData(notifications) {
users: [],
versions: [],
organizations: [],
}
};
for (const notification of notifications) {
if (notification.body) {
if (notification.body.project_id) {
bulk.projects.push(notification.body.project_id)
bulk.projects.push(notification.body.project_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) {
bulk.reports.push(notification.body.report_id)
bulk.reports.push(notification.body.report_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) {
bulk.users.push(notification.body.invited_by)
bulk.users.push(notification.body.invited_by);
}
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) {
if (report.item_type === 'project') {
bulk.projects.push(report.item_id)
} else if (report.item_type === 'user') {
bulk.users.push(report.item_id)
} else if (report.item_type === 'version') {
bulk.versions.push(report.item_id)
if (report.item_type === "project") {
bulk.projects.push(report.item_id);
} else if (report.item_type === "user") {
bulk.users.push(report.item_id);
} else if (report.item_type === "version") {
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) {
bulk.projects.push(version.project_id)
bulk.projects.push(version.project_id);
}
const [projects, threads, users, organizations] = await Promise.all([
getBulk('projects', bulk.projects),
getBulk('threads', bulk.threads),
getBulk('users', bulk.users),
getBulk('organizations', bulk.organizations, 3),
])
getBulk("projects", bulk.projects),
getBulk("threads", bulk.threads),
getBulk("users", bulk.users),
getBulk("organizations", bulk.organizations, 3),
]);
for (const notification of notifications) {
notification.extra_data = {}
notification.extra_data = {};
if (notification.body) {
if (notification.body.project_id) {
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) {
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) {
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
if (type === 'project') {
const type = notification.extra_data.report.item_type;
if (type === "project") {
notification.extra_data.project = projects.find(
(x) => x.id === notification.extra_data.report.item_id
)
} else if (type === 'user') {
(x) => x.id === notification.extra_data.report.item_id,
);
} else if (type === "user") {
notification.extra_data.user = users.find(
(x) => x.id === notification.extra_data.report.item_id
)
} else if (type === 'version') {
(x) => x.id === notification.extra_data.report.item_id,
);
} else if (type === "version") {
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(
(x) => x.id === notification.extra_data.version.project_id
)
(x) => x.id === notification.extra_data.version.project_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) {
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) {
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) {
const grouped = []
const grouped = [];
for (let i = 0; i < notifications.length; i++) {
const current = notifications[i]
const next = notifications[i + 1]
const current = notifications[i];
const next = notifications[i + 1];
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])) {
current.grouped_notifs.push(notifications[j])
j++
current.grouped_notifs.push(notifications[j]);
j++;
}
grouped.push(current)
i = j - 1 // skip i to the last ungrouped
grouped.push(current);
i = j - 1; // skip i to the last ungrouped
} else {
grouped.push(current)
grouped.push(current);
}
}
return grouped
return grouped;
}
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) {
try {
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
method: 'PATCH',
})
method: "PATCH",
});
return (notifications) => {
const newNotifs = notifications
const newNotifs = notifications;
newNotifs.forEach((notif) => {
if (ids.includes(notif.id)) {
notif.read = true
}
})
return newNotifs
notif.read = true;
}
});
return newNotifs;
};
} catch (err) {
const app = useNuxtApp()
const app = useNuxtApp();
app.$notify({
group: 'main',
title: 'Error marking notification as read',
group: "main",
title: "Error marking notification as read",
text: err.data ? err.data.description : err,
type: 'error',
})
return () => {}
type: "error",
});
return () => {};
}
}

View File

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

View File

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

View File

@ -1,26 +1,26 @@
export function addReportMessage(thread, report) {
if (!thread || !report) {
return thread
return thread;
}
if (
!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({
id: 'original',
id: "original",
author_id: report.reporterUser.id,
body: {
type: 'text',
type: "text",
body: report.body,
private: false,
replying_to: null,
},
created: report.created,
})
});
}
return thread
return thread;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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