chore: lint frontend

This commit is contained in:
Calum H. (IMB11) 2025-08-01 15:03:08 +01:00 committed by Calum H.
parent 5b97f1e9b8
commit f7fc208b15
176 changed files with 3105 additions and 2995 deletions

View File

@ -1,2 +1,8 @@
import config from '@modrinth/tooling-config/eslint/nuxt.mjs' import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
export default config export default config.append([{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
"import/no-unresolved": "off",
'no-undef': 'off'
}
}])

View File

@ -1,27 +1,28 @@
import { promises as fs } from "fs"; import { pathToFileURL } from 'node:url'
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/"; import { match as matchLocale } from '@formatjs/intl-localematcher'
import { consola } from 'consola'
import { promises as fs } from 'fs'
import { globIterate } from 'glob'
import { defineNuxtConfig } from 'nuxt/config'
import { $fetch } from 'ofetch'
import { basename, relative, resolve } from 'pathe'
import svgLoader from 'vite-svg-loader'
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
const preloadedFonts = [ const preloadedFonts = [
"inter/Inter-Regular.woff2", 'inter/Inter-Regular.woff2',
"inter/Inter-Medium.woff2", 'inter/Inter-Medium.woff2',
"inter/Inter-SemiBold.woff2", 'inter/Inter-SemiBold.woff2',
"inter/Inter-Bold.woff2", 'inter/Inter-Bold.woff2',
]; ]
const favicons = { const favicons = {
"(prefers-color-scheme:no-preference)": "/favicon-light.ico", '(prefers-color-scheme:no-preference)': '/favicon-light.ico',
"(prefers-color-scheme:light)": "/favicon-light.ico", '(prefers-color-scheme:light)': '/favicon-light.ico',
"(prefers-color-scheme:dark)": "/favicon.ico", '(prefers-color-scheme:dark)': '/favicon.ico',
}; }
/** /**
* Tags of locales that are auto-discovered besides the default locale. * Tags of locales that are auto-discovered besides the default locale.
@ -29,52 +30,52 @@ const favicons = {
* Preferably only the locales that reach a certain threshold of complete * Preferably only the locales that reach a certain threshold of complete
* translations would be included in this array. * translations would be included in this array.
*/ */
const enabledLocales: string[] = []; const enabledLocales: string[] = []
/** /**
* Overrides for the categories of the certain locales. * Overrides for the categories of the certain locales.
*/ */
const localesCategoriesOverrides: Partial<Record<string, "fun" | "experimental">> = { const localesCategoriesOverrides: Partial<Record<string, 'fun' | 'experimental'>> = {
"en-x-pirate": "fun", 'en-x-pirate': 'fun',
"en-x-updown": "fun", 'en-x-updown': 'fun',
"en-x-lolcat": "fun", 'en-x-lolcat': 'fun',
"en-x-uwu": "fun", 'en-x-uwu': 'fun',
"ru-x-bandit": "fun", 'ru-x-bandit': 'fun',
ar: "experimental", ar: 'experimental',
he: "experimental", he: 'experimental',
pes: "experimental", pes: 'experimental',
}; }
export default defineNuxtConfig({ export default defineNuxtConfig({
srcDir: "src/", srcDir: 'src/',
app: { app: {
head: { head: {
htmlAttrs: { htmlAttrs: {
lang: "en", lang: 'en',
}, },
title: "Modrinth", title: 'Modrinth',
link: [ link: [
// The type is necessary because the linter can't always compare this very nested/complex type on itself // The type is necessary because the linter can't always compare this very nested/complex type on itself
...preloadedFonts.map((font): object => { ...preloadedFonts.map((font): object => {
return { return {
rel: "preload", rel: 'preload',
href: `https://cdn-raw.modrinth.com/fonts/${font}?v=3.19`, href: `https://cdn-raw.modrinth.com/fonts/${font}?v=3.19`,
as: "font", as: 'font',
type: "font/woff2", type: 'font/woff2',
crossorigin: "anonymous", crossorigin: 'anonymous',
}; }
}), }),
...Object.entries(favicons).map(([media, href]): object => { ...Object.entries(favicons).map(([media, href]): object => {
return { rel: "icon", type: "image/x-icon", href, media }; return { rel: 'icon', type: 'image/x-icon', href, media }
}), }),
...Object.entries(favicons).map(([media, href]): object => { ...Object.entries(favicons).map(([media, href]): object => {
return { rel: "apple-touch-icon", type: "image/x-icon", href, media, sizes: "64x64" }; return { rel: 'apple-touch-icon', type: 'image/x-icon', href, media, sizes: '64x64' }
}), }),
{ {
rel: "search", rel: 'search',
type: "application/opensearchdescription+xml", type: 'application/opensearchdescription+xml',
href: "/opensearch.xml", href: '/opensearch.xml',
title: "Modrinth mods", title: 'Modrinth mods',
}, },
], ],
}, },
@ -85,19 +86,19 @@ export default defineNuxtConfig({
}, },
esbuild: { esbuild: {
define: { define: {
global: "globalThis", global: 'globalThis',
}, },
}, },
cacheDir: "../../node_modules/.vite/apps/knossos", cacheDir: '../../node_modules/.vite/apps/knossos',
resolve: { resolve: {
dedupe: ["vue"], dedupe: ['vue'],
}, },
plugins: [ plugins: [
svgLoader({ svgLoader({
svgoConfig: { svgoConfig: {
plugins: [ plugins: [
{ {
name: "preset-default", name: 'preset-default',
params: { params: {
overrides: { overrides: {
removeViewBox: false, removeViewBox: false,
@ -110,33 +111,33 @@ export default defineNuxtConfig({
], ],
}, },
hooks: { hooks: {
async "build:before"() { async 'build:before'() {
// 30 minutes // 30 minutes
const TTL = 30 * 60 * 1000; const TTL = 30 * 60 * 1000
let state: { let state: {
lastGenerated?: string; lastGenerated?: string
apiUrl?: string; apiUrl?: string
categories?: any[]; categories?: any[]
loaders?: any[]; loaders?: any[]
gameVersions?: any[]; gameVersions?: any[]
donationPlatforms?: any[]; donationPlatforms?: any[]
reportTypes?: any[]; reportTypes?: any[]
homePageProjects?: any[]; homePageProjects?: any[]
homePageSearch?: any[]; homePageSearch?: any[]
homePageNotifs?: any[]; homePageNotifs?: any[]
products?: any[]; products?: any[]
errors?: number[]; errors?: number[]
} = {}; } = {}
try { try {
state = JSON.parse(await fs.readFile("./src/generated/state.json", "utf8")); state = JSON.parse(await fs.readFile('./src/generated/state.json', 'utf8'))
} catch { } catch {
// File doesn't exist, create folder // File doesn't exist, create folder
await fs.mkdir("./src/generated", { recursive: true }); await fs.mkdir('./src/generated', { recursive: true })
} }
const API_URL = getApiUrl(); const API_URL = getApiUrl()
if ( if (
// Skip regeneration if within TTL... // Skip regeneration if within TTL...
@ -145,25 +146,25 @@ export default defineNuxtConfig({
// ...but only if the API URL is the same // ...but only if the API URL is the same
state.apiUrl === API_URL state.apiUrl === API_URL
) { ) {
return; return
} }
state.lastGenerated = new Date().toISOString(); state.lastGenerated = new Date().toISOString()
state.apiUrl = API_URL; state.apiUrl = API_URL
const headers = { const headers = {
headers: { headers: {
"user-agent": "Knossos generator (support@modrinth.com)", 'user-agent': 'Knossos generator (support@modrinth.com)',
}, },
}; }
const caughtErrorCodes = new Set<number>(); const caughtErrorCodes = new Set<number>()
function handleFetchError(err: any, defaultValue: any) { function handleFetchError(err: any, defaultValue: any) {
console.error("Error generating state: ", err); console.error('Error generating state: ', err)
caughtErrorCodes.add(err.status); caughtErrorCodes.add(err.status)
return defaultValue; return defaultValue
} }
const [ const [
@ -193,152 +194,152 @@ export default defineNuxtConfig({
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) => $fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) =>
handleFetchError(err, {}), handleFetchError(err, {}),
), ),
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers).catch((err) => $fetch(`${API_URL.replace('/v2/', '/_internal/')}billing/products`, headers).catch((err) =>
handleFetchError(err, []), handleFetchError(err, []),
), ),
]); ])
state.categories = categories; state.categories = categories
state.loaders = loaders; state.loaders = loaders
state.gameVersions = gameVersions; state.gameVersions = gameVersions
state.donationPlatforms = donationPlatforms; state.donationPlatforms = donationPlatforms
state.reportTypes = reportTypes; state.reportTypes = reportTypes
state.homePageProjects = homePageProjects; state.homePageProjects = homePageProjects
state.homePageSearch = homePageSearch; state.homePageSearch = homePageSearch
state.homePageNotifs = homePageNotifs; state.homePageNotifs = homePageNotifs
state.products = products; state.products = products
state.errors = [...caughtErrorCodes]; state.errors = [...caughtErrorCodes]
await fs.writeFile("./src/generated/state.json", JSON.stringify(state)); await fs.writeFile('./src/generated/state.json', JSON.stringify(state))
console.log("Tags generated!"); console.log('Tags generated!')
}, },
"pages:extend"(routes) { 'pages:extend'(routes) {
routes.splice( routes.splice(
routes.findIndex((x) => x.name === "search-searchProjectType"), routes.findIndex((x) => x.name === 'search-searchProjectType'),
1, 1,
); )
const types = ["mods", "modpacks", "plugins", "resourcepacks", "shaders", "datapacks"]; const types = ['mods', 'modpacks', 'plugins', 'resourcepacks', 'shaders', 'datapacks']
types.forEach((type) => types.forEach((type) =>
routes.push({ routes.push({
name: `search-${type}`, name: `search-${type}`,
path: `/${type}`, path: `/${type}`,
file: resolve(__dirname, "src/pages/search/[searchProjectType].vue"), file: resolve(__dirname, 'src/pages/search/[searchProjectType].vue'),
children: [], children: [],
}), }),
); )
}, },
async "vintl:extendOptions"(opts) { async 'vintl:extendOptions'(opts) {
opts.locales ??= []; opts.locales ??= []
const isProduction = getDomain() === "https://modrinth.com"; const isProduction = getDomain() === 'https://modrinth.com'
const resolveCompactNumberDataImport = await (async () => { const resolveCompactNumberDataImport = await (async () => {
const compactNumberLocales: string[] = []; const compactNumberLocales: string[] = []
for await (const localeFile of globIterate( for await (const localeFile of globIterate(
"node_modules/@vintl/compact-number/dist/locale-data/*.mjs", 'node_modules/@vintl/compact-number/dist/locale-data/*.mjs',
{ ignore: "**/*.data.mjs" }, { ignore: '**/*.data.mjs' },
)) { )) {
const tag = basename(localeFile, ".mjs"); const tag = basename(localeFile, '.mjs')
compactNumberLocales.push(tag); compactNumberLocales.push(tag)
} }
function resolveImport(tag: string) { function resolveImport(tag: string) {
const matchedTag = matchLocale([tag], compactNumberLocales, "en-x-placeholder"); const matchedTag = matchLocale([tag], compactNumberLocales, 'en-x-placeholder')
return matchedTag === "en-x-placeholder" return matchedTag === 'en-x-placeholder'
? undefined ? undefined
: `@vintl/compact-number/locale-data/${matchedTag}`; : `@vintl/compact-number/locale-data/${matchedTag}`
} }
return resolveImport; return resolveImport
})(); })()
const resolveOmorphiaLocaleImport = await (async () => { const resolveOmorphiaLocaleImport = await (async () => {
const omorphiaLocales: string[] = []; const omorphiaLocales: string[] = []
const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>(); const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>()
for await (const localeDir of globIterate("node_modules/@modrinth/ui/src/locales/*", { for await (const localeDir of globIterate('node_modules/@modrinth/ui/src/locales/*', {
posix: true, posix: true,
})) { })) {
const tag = basename(localeDir); const tag = basename(localeDir)
omorphiaLocales.push(tag); omorphiaLocales.push(tag)
const localeFiles: { from: string; format?: string }[] = []; const localeFiles: { from: string; format?: string }[] = []
omorphiaLocaleSets.set(tag, { files: localeFiles }); omorphiaLocaleSets.set(tag, { files: localeFiles })
for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) { for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) {
localeFiles.push({ localeFiles.push({
from: pathToFileURL(localeFile).toString(), from: pathToFileURL(localeFile).toString(),
format: "default", format: 'default',
}); })
} }
} }
return function resolveLocaleImport(tag: string) { return function resolveLocaleImport(tag: string) {
return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, "en-x-placeholder")); return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, 'en-x-placeholder'))
}; }
})(); })()
for await (const localeDir of globIterate("src/locales/*/", { posix: true })) { for await (const localeDir of globIterate('src/locales/*/', { posix: true })) {
const tag = basename(localeDir); const tag = basename(localeDir)
if (isProduction && !enabledLocales.includes(tag) && opts.defaultLocale !== tag) continue; if (isProduction && !enabledLocales.includes(tag) && opts.defaultLocale !== tag) continue
const locale = const locale =
opts.locales.find((locale) => locale.tag === tag) ?? opts.locales.find((locale) => locale.tag === tag) ??
opts.locales[opts.locales.push({ tag }) - 1]!; opts.locales[opts.locales.push({ tag }) - 1]!
const localeFiles = (locale.files ??= []); const localeFiles = (locale.files ??= [])
for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) { for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) {
const fileName = basename(localeFile); const fileName = basename(localeFile)
if (fileName === "index.json") { if (fileName === 'index.json') {
localeFiles.push({ localeFiles.push({
from: `./${relative("./src", localeFile)}`, from: `./${relative('./src', localeFile)}`,
format: "crowdin", format: 'crowdin',
}); })
} else if (fileName === "meta.json") { } else if (fileName === 'meta.json') {
const meta: Record<string, { message: string }> = await fs const meta: Record<string, { message: string }> = await fs
.readFile(localeFile, "utf8") .readFile(localeFile, 'utf8')
.then((date) => JSON.parse(date)); .then((date) => JSON.parse(date))
const localeMeta = (locale.meta ??= {}); const localeMeta = (locale.meta ??= {})
for (const key in meta) { for (const key in meta) {
const value = meta[key]; const value = meta[key]
if (value === undefined) continue; if (value === undefined) continue
localeMeta[key] = value.message; localeMeta[key] = value.message
} }
} else { } else {
(locale.resources ??= {})[fileName] = `./${relative("./src", localeFile)}`; ;(locale.resources ??= {})[fileName] = `./${relative('./src', localeFile)}`
} }
} }
const categoryOverride = localesCategoriesOverrides[tag]; const categoryOverride = localesCategoriesOverrides[tag]
if (categoryOverride != null) { if (categoryOverride != null) {
(locale.meta ??= {}).category = categoryOverride; ;(locale.meta ??= {}).category = categoryOverride
} }
const omorphiaLocaleData = resolveOmorphiaLocaleImport(tag); const omorphiaLocaleData = resolveOmorphiaLocaleImport(tag)
if (omorphiaLocaleData != null) { if (omorphiaLocaleData != null) {
localeFiles.push(...omorphiaLocaleData.files); localeFiles.push(...omorphiaLocaleData.files)
} }
const cnDataImport = resolveCompactNumberDataImport(tag); const cnDataImport = resolveCompactNumberDataImport(tag)
if (cnDataImport != null) { if (cnDataImport != null) {
(locale.additionalImports ??= []).push({ ;(locale.additionalImports ??= []).push({
from: cnDataImport, from: cnDataImport,
resolve: false, resolve: false,
}); })
} }
} }
}, },
}, },
runtimeConfig: { runtimeConfig: {
// @ts-ignore // @ts-expect-error
apiBaseUrl: process.env.BASE_URL ?? globalThis.BASE_URL ?? getApiUrl(), apiBaseUrl: process.env.BASE_URL ?? globalThis.BASE_URL ?? getApiUrl(),
// @ts-ignore // @ts-expect-error
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY, rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
pyroBaseUrl: process.env.PYRO_BASE_URL, pyroBaseUrl: process.env.PYRO_BASE_URL,
public: { public: {
@ -348,25 +349,26 @@ export default defineNuxtConfig({
production: isProduction(), production: isProduction(),
featureFlagOverrides: getFeatureFlagOverrides(), featureFlagOverrides: getFeatureFlagOverrides(),
owner: process.env.VERCEL_GIT_REPO_OWNER || "modrinth", owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
slug: process.env.VERCEL_GIT_REPO_SLUG || "code", slug: process.env.VERCEL_GIT_REPO_SLUG || 'code',
branch: branch:
process.env.VERCEL_GIT_COMMIT_REF || process.env.VERCEL_GIT_COMMIT_REF ||
process.env.CF_PAGES_BRANCH || process.env.CF_PAGES_BRANCH ||
// @ts-ignore // @ts-expect-error
globalThis.CF_PAGES_BRANCH || globalThis.CF_PAGES_BRANCH ||
"master", 'master',
hash: hash:
process.env.VERCEL_GIT_COMMIT_SHA || process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.CF_PAGES_COMMIT_SHA || process.env.CF_PAGES_COMMIT_SHA ||
// @ts-ignore // @ts-expect-error
globalThis.CF_PAGES_COMMIT_SHA || globalThis.CF_PAGES_COMMIT_SHA ||
"unknown", 'unknown',
stripePublishableKey: stripePublishableKey:
process.env.STRIPE_PUBLISHABLE_KEY || process.env.STRIPE_PUBLISHABLE_KEY ||
// @ts-expect-error
globalThis.STRIPE_PUBLISHABLE_KEY || globalThis.STRIPE_PUBLISHABLE_KEY ||
"pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b", 'pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b',
}, },
}, },
typescript: { typescript: {
@ -375,62 +377,62 @@ export default defineNuxtConfig({
typeCheck: false, typeCheck: false,
tsConfig: { tsConfig: {
compilerOptions: { compilerOptions: {
moduleResolution: "bundler", moduleResolution: 'bundler',
allowImportingTsExtensions: true, allowImportingTsExtensions: true,
}, },
}, },
}, },
modules: ["@vintl/nuxt", "@pinia/nuxt"], modules: ['@vintl/nuxt', '@pinia/nuxt'],
vintl: { vintl: {
defaultLocale: "en-US", defaultLocale: 'en-US',
locales: [ locales: [
{ {
tag: "en-US", tag: 'en-US',
meta: { meta: {
static: { static: {
iso: "en", iso: 'en',
}, },
}, },
}, },
], ],
storage: "cookie", storage: 'cookie',
parserless: "only-prod", parserless: 'only-prod',
seo: { seo: {
defaultLocaleHasParameter: false, defaultLocaleHasParameter: false,
}, },
onParseError({ error, message, messageId, moduleId, parseMessage, parserOptions }) { onParseError({ error, message, messageId, moduleId, parseMessage, parserOptions }) {
const errorMessage = String(error); const errorMessage = String(error)
const modulePath = relative(__dirname, moduleId); const modulePath = relative(__dirname, moduleId)
try { try {
const fallback = parseMessage(message, { ...parserOptions, ignoreTag: true }); const fallback = parseMessage(message, { ...parserOptions, ignoreTag: true })
consola.warn( consola.warn(
`[i18n] ${messageId} in ${modulePath} cannot be parsed normally due to ${errorMessage}. The tags will will not be parsed.`, `[i18n] ${messageId} in ${modulePath} cannot be parsed normally due to ${errorMessage}. The tags will will not be parsed.`,
); )
return fallback; return fallback
} catch (err) { } catch (err) {
const secondaryErrorMessage = String(err); const secondaryErrorMessage = String(err)
const reason = const reason =
errorMessage === secondaryErrorMessage errorMessage === secondaryErrorMessage
? errorMessage ? errorMessage
: `${errorMessage} and ${secondaryErrorMessage}`; : `${errorMessage} and ${secondaryErrorMessage}`
consola.warn( consola.warn(
`[i18n] ${messageId} in ${modulePath} cannot be parsed due to ${reason}. It will be skipped.`, `[i18n] ${messageId} in ${modulePath} cannot be parsed due to ${reason}. It will be skipped.`,
); )
} }
}, },
}, },
nitro: { nitro: {
moduleSideEffects: ["@vintl/compact-number/locale-data"], moduleSideEffects: ['@vintl/compact-number/locale-data'],
}, },
devtools: { devtools: {
enabled: true, enabled: true,
}, },
css: ["~/assets/styles/tailwind.css"], css: ['~/assets/styles/tailwind.css'],
postcss: { postcss: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
@ -438,50 +440,50 @@ export default defineNuxtConfig({
}, },
}, },
routeRules: { routeRules: {
"/**": { '/**': {
headers: { headers: {
"Accept-CH": "Sec-CH-Prefers-Color-Scheme", 'Accept-CH': 'Sec-CH-Prefers-Color-Scheme',
"Critical-CH": "Sec-CH-Prefers-Color-Scheme", 'Critical-CH': 'Sec-CH-Prefers-Color-Scheme',
}, },
}, },
}, },
compatibilityDate: "2024-07-03", compatibilityDate: '2024-07-03',
telemetry: false, telemetry: false,
}); })
function getApiUrl() { function getApiUrl() {
// @ts-ignore // @ts-expect-error
return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL; return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL
} }
function isProduction() { function isProduction() {
return process.env.NODE_ENV === "production"; return process.env.NODE_ENV === 'production'
} }
function getFeatureFlagOverrides() { function getFeatureFlagOverrides() {
return JSON.parse(process.env.FLAG_OVERRIDES ?? "{}"); return JSON.parse(process.env.FLAG_OVERRIDES ?? '{}')
} }
function getDomain() { function getDomain() {
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === 'production') {
if (process.env.SITE_URL) { if (process.env.SITE_URL) {
return process.env.SITE_URL; return process.env.SITE_URL
} }
// @ts-ignore // @ts-expect-error
else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) { else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
// @ts-ignore // @ts-expect-error
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL; return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL
} else if (process.env.HEROKU_APP_NAME) { } else if (process.env.HEROKU_APP_NAME) {
return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`; return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
} else if (process.env.VERCEL_URL) { } else if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`; return `https://${process.env.VERCEL_URL}`
} else if (getApiUrl() === STAGING_API_URL) { } else if (getApiUrl() === STAGING_API_URL) {
return "https://staging.modrinth.com"; return 'https://staging.modrinth.com'
} else { } else {
return "https://modrinth.com"; return 'https://modrinth.com'
} }
} else { } else {
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000
return `http://localhost:${port}`; return `http://localhost:${port}`
} }
} }

View File

@ -49,7 +49,7 @@
</template> </template>
<script setup> <script setup>
import { InfoIcon, ClientIcon, GlobeIcon, ServerIcon } from "@modrinth/assets"; import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from "@modrinth/assets";
defineProps({ defineProps({
type: { type: {

View File

@ -84,8 +84,8 @@
</template> </template>
<script setup> <script setup>
import { NewModal, ButtonStyled, DropdownSelect } from "@modrinth/ui"; import { PlusIcon,XIcon } from "@modrinth/assets";
import { XIcon, PlusIcon } from "@modrinth/assets"; import { ButtonStyled, DropdownSelect,NewModal } from "@modrinth/ui";
const router = useRouter(); const router = useRouter();
const app = useNuxtApp(); const app = useNuxtApp();

View File

@ -35,7 +35,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue"; import { computed, onMounted,ref, watch } from "vue";
const route = useNativeRoute(); const route = useNativeRoute();

View File

@ -1,42 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui"; import { CheckIcon, MailIcon } from '@modrinth/assets'
import { MailIcon, CheckIcon } from "@modrinth/assets"; import { ButtonStyled } from '@modrinth/ui'
import { ref } from "vue"; import { ref } from 'vue'
import { useBaseFetch } from "~/composables/fetch.js";
const auth = await useAuth(); import { useBaseFetch } from '~/composables/fetch.js'
const showSubscriptionConfirmation = ref(false);
const auth = await useAuth()
const showSubscriptionConfirmation = ref(false)
const showSubscribeButton = useAsyncData( const showSubscribeButton = useAsyncData(
async () => { async () => {
if (auth.value?.user) { if (auth.value?.user) {
try { try {
const { subscribed } = await useBaseFetch("auth/email/subscribe", { const { subscribed } = await useBaseFetch('auth/email/subscribe', {
method: "GET", method: 'GET',
}); })
return !subscribed; return !subscribed
} catch { } catch {
return true; return true
} }
} else { } else {
return false; return false
} }
}, },
{ watch: [auth], server: false }, { watch: [auth], server: false },
); )
async function subscribe() { async function subscribe() {
try { try {
await useBaseFetch("auth/email/subscribe", { await useBaseFetch('auth/email/subscribe', {
method: "POST", method: 'POST',
}); })
showSubscriptionConfirmation.value = true; showSubscriptionConfirmation.value = true
} catch { } catch {
// TODO: Use addNotification when DI pr is merged.
} finally { } finally {
setTimeout(() => { setTimeout(() => {
showSubscriptionConfirmation.value = false; showSubscriptionConfirmation.value = false
showSubscribeButton.status.value = "success"; showSubscribeButton.status.value = 'success'
showSubscribeButton.data.value = false; showSubscribeButton.data.value = false
}, 2500); }, 2500)
} }
} }
</script> </script>

View File

@ -319,26 +319,27 @@
</template> </template>
<script setup> <script setup>
import { renderString } from "@modrinth/utils";
import { import {
UserPlusIcon,
ScaleIcon,
BellIcon, BellIcon,
CheckCircleIcon,
CalendarIcon, CalendarIcon,
VersionIcon, CheckCircleIcon,
CheckIcon, CheckIcon,
XIcon,
ExternalIcon, ExternalIcon,
ScaleIcon,
UserPlusIcon,
VersionIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui"; import { Avatar, CopyCode, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue"; import { renderString } from "@modrinth/utils";
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.ts";
import DoubleIcon from "~/components/ui/DoubleIcon.vue"; import DoubleIcon from "~/components/ui/DoubleIcon.vue";
import Categories from "~/components/ui/search/Categories.vue"; import Categories from "~/components/ui/search/Categories.vue";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { markAsRead } from "~/helpers/notifications.ts";
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
import { getUserLink } from "~/helpers/users.js";
const app = useNuxtApp(); const app = useNuxtApp();
const emit = defineEmits(["update:notifications"]); const emit = defineEmits(["update:notifications"]);

View File

@ -74,76 +74,77 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ButtonStyled } from "@modrinth/ui";
import { import {
XCircleIcon,
CheckCircleIcon, CheckCircleIcon,
CheckIcon, CheckIcon,
CopyIcon,
InfoIcon, InfoIcon,
IssuesIcon, IssuesIcon,
XCircleIcon,
XIcon, XIcon,
CopyIcon, } from '@modrinth/assets'
} from "@modrinth/assets"; import { ButtonStyled } from '@modrinth/ui'
const notifications = useNotifications(); const notifications = useNotifications()
const { isVisible: moveNotificationsRight } = useNotificationRightwards(); const { isVisible: moveNotificationsRight } = useNotificationRightwards()
const isIntercomPresent = ref(false); const isIntercomPresent = ref(false)
function stopTimer(notif) { function stopTimer(notif) {
clearTimeout(notif.timer); clearTimeout(notif.timer)
} }
const copied = ref({}); const copied = ref({})
const createNotifText = (notif) => { const createNotifText = (notif) => {
let text = ""; let text = ''
if (notif.title) { if (notif.title) {
text += notif.title; text += notif.title
} }
if (notif.text) { if (notif.text) {
if (text.length > 0) { if (text.length > 0) {
text += "\n"; text += '\n'
} }
text += notif.text; text += notif.text
} }
if (notif.errorCode) { if (notif.errorCode) {
if (text.length > 0) { if (text.length > 0) {
text += "\n"; text += '\n'
} }
text += notif.errorCode; text += notif.errorCode
} }
return text; return text
}; }
function checkIntercomPresence() { function checkIntercomPresence() {
isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app"); isIntercomPresent.value = !!document.querySelector('.intercom-lightweight-app')
} }
onMounted(() => { onMounted(() => {
checkIntercomPresence(); checkIntercomPresence()
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
checkIntercomPresence(); checkIntercomPresence()
}); })
observer.observe(document.body, { observer.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true,
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer.disconnect(); observer.disconnect()
}); })
}); })
function copyToClipboard(notif) { function copyToClipboard(notif) {
const text = createNotifText(notif); const text = createNotifText(notif)
copied.value[text] = true; copied.value[text] = true
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text)
setTimeout(() => { setTimeout(() => {
delete copied.value[text]; const { [text]: _, ...newCopied } = copied.value
}, 2000); copied.value = newCopied
}, 2000)
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -31,7 +31,7 @@
</template> </template>
<script setup lang="ts" generic="T"> <script setup lang="ts" generic="T">
import { ref, computed, onMounted } from "vue"; import { computed, onMounted,ref } from "vue";
const modelValue = defineModel<T>({ required: true }); const modelValue = defineModel<T>({ required: true });

View File

@ -71,7 +71,7 @@
</NewModal> </NewModal>
</template> </template>
<script setup> <script setup>
import { XIcon, PlusIcon } from "@modrinth/assets"; import { PlusIcon,XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
const router = useNativeRouter(); const router = useNativeRouter();

View File

@ -110,7 +110,7 @@
<script setup> <script setup>
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets"; import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui"; import { Avatar,Button, Checkbox, CopyCode, Modal } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils"; import { formatProjectType } from "@modrinth/utils";
const modalOpen = ref(null); const modalOpen = ref(null);

View File

@ -90,10 +90,11 @@
</template> </template>
<script> <script>
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets"; import { CalendarIcon, DownloadIcon, HeartIcon,UpdatedIcon } from "@modrinth/assets";
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui"; import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
import Categories from "~/components/ui/search/Categories.vue";
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue"; import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import Categories from "~/components/ui/search/Categories.vue";
export default { export default {
components: { components: {

View File

@ -109,16 +109,17 @@
<script setup> <script setup>
import { import {
ChevronRightIcon,
CheckIcon,
XIcon,
AsteriskIcon, AsteriskIcon,
LightBulbIcon, CheckIcon,
SendIcon, ChevronRightIcon,
ScaleIcon,
DropdownIcon, DropdownIcon,
LightBulbIcon,
ScaleIcon,
SendIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { formatProjectType } from "@modrinth/utils"; import { formatProjectType } from "@modrinth/utils";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
const props = defineProps({ const props = defineProps({
@ -244,7 +245,7 @@ const nags = computed(() => [
}, },
}, },
{ {
condition: props.project.gallery.length === 0 || !featuredGalleryImage, condition: props.project.gallery.length === 0 || !featuredGalleryImage.value,
title: "Feature a gallery image", title: "Feature a gallery image",
id: "feature-gallery-image", id: "feature-gallery-image",
description: "Featured gallery images may be the first impression of many users.", description: "Featured gallery images may be the first impression of many users.",

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { formatMoney,formatNumber } from "@modrinth/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { formatNumber, formatMoney } from "@modrinth/utils";
import VueApexCharts from "vue3-apexcharts"; import VueApexCharts from "vue3-apexcharts";
const props = defineProps({ const props = defineProps({

View File

@ -304,17 +304,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DownloadIcon,UpdatedIcon } from "@modrinth/assets";
import { Button, Card, DropdownSelect } from "@modrinth/ui"; import { Button, Card, DropdownSelect } from "@modrinth/ui";
import { formatMoney, formatNumber, formatCategoryHeader } from "@modrinth/utils"; import { formatCategoryHeader,formatMoney, formatNumber } from "@modrinth/utils";
import { UpdatedIcon, DownloadIcon } from "@modrinth/assets";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { computed } from "vue"; import { computed } from "vue";
import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js"; import { UiChartsChart as Chart,UiChartsCompactChart as CompactChart } 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";
import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js";
const router = useNativeRouter(); const router = useNativeRouter();
const theme = useTheme(); const theme = useTheme();

View File

@ -118,22 +118,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from "dayjs";
import { import {
Avatar,
useRelativeTime,
OverflowMenu,
type OverflowMenuOption,
ButtonStyled,
} from "@modrinth/ui";
import {
EllipsisVerticalIcon,
OrganizationIcon,
EyeIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
EllipsisVerticalIcon,
EyeIcon,
LinkIcon, LinkIcon,
OrganizationIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import type { ExtendedDelphiReport } from "@modrinth/moderation"; import type { ExtendedDelphiReport } from "@modrinth/moderation";
import {
Avatar,
ButtonStyled,
OverflowMenu,
type OverflowMenuOption,
useRelativeTime,
} from "@modrinth/ui";
import dayjs from "dayjs";
const props = defineProps<{ const props = defineProps<{
report: ExtendedDelphiReport; report: ExtendedDelphiReport;

View File

@ -75,10 +75,10 @@
aria-hidden="true" aria-hidden="true"
/> />
<span class="hidden sm:inline">{{ <span class="hidden sm:inline">{{
props.queueEntry.project.project_types.map(formatProjectType).join(", ") props.queueEntry.project.project_types.map(formatProjectType).join(', ')
}}</span> }}</span>
<span class="sm:hidden">{{ <span class="sm:hidden">{{
formatProjectType(props.queueEntry.project.project_type ?? "project").substring(0, 3) formatProjectType(props.queueEntry.project.project_type ?? 'project').substring(0, 3)
}}</span> }}</span>
</span> </span>
@ -105,7 +105,7 @@
> >
<span class="hidden sm:inline">{{ getSubmittedTime(queueEntry) }}</span> <span class="hidden sm:inline">{{ getSubmittedTime(queueEntry) }}</span>
<span class="sm:hidden">{{ <span class="sm:hidden">{{
getSubmittedTime(queueEntry).replace("Submitted ", "") getSubmittedTime(queueEntry).replace('Submitted ', '')
}}</span> }}</span>
</span> </span>
</div> </div>
@ -127,39 +127,35 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from "dayjs";
import { import {
EyeIcon,
PaintbrushIcon,
ScaleIcon,
BoxIcon, BoxIcon,
GlassesIcon,
PlugIcon,
PackageOpenIcon,
BracesIcon, BracesIcon,
} from "@modrinth/assets"; EyeIcon,
import { useRelativeTime, Avatar, ButtonStyled, Badge } from "@modrinth/ui"; GlassesIcon,
import { PackageOpenIcon,
formatProjectType, PaintbrushIcon,
type Organization, PlugIcon,
type Project, ScaleIcon,
type TeamMember, } from '@modrinth/assets'
} from "@modrinth/utils"; import { Avatar, Badge, ButtonStyled, useRelativeTime } from '@modrinth/ui'
import { computed } from "vue"; import { formatProjectType } from '@modrinth/utils'
import { useModerationStore } from "~/store/moderation.ts"; import dayjs from 'dayjs'
import type { ModerationProject } from "~/helpers/moderation"; import { computed } from 'vue'
const formatRelativeTime = useRelativeTime(); import type { ModerationProject } from '~/helpers/moderation'
const moderationStore = useModerationStore(); import { useModerationStore } from '~/store/moderation.ts'
const formatRelativeTime = useRelativeTime()
const moderationStore = useModerationStore()
const props = defineProps<{ const props = defineProps<{
queueEntry: ModerationProject; queueEntry: ModerationProject
}>(); }>()
function getDaysQueued(date: Date): number { function getDaysQueued(date: Date): number {
const now = new Date(); const now = new Date()
const diff = now.getTime() - date.getTime(); const diff = now.getTime() - date.getTime()
return Math.floor(diff / (1000 * 60 * 60 * 24)); return Math.floor(diff / (1000 * 60 * 60 * 24))
} }
const queuedDate = computed(() => { const queuedDate = computed(() => {
@ -167,38 +163,38 @@ const queuedDate = computed(() => {
props.queueEntry.project.queued || props.queueEntry.project.queued ||
props.queueEntry.project.created || props.queueEntry.project.created ||
props.queueEntry.project.updated, props.queueEntry.project.updated,
); )
}); })
const daysInQueue = computed(() => { const daysInQueue = computed(() => {
return getDaysQueued(queuedDate.value.toDate()); return getDaysQueued(queuedDate.value.toDate())
}); })
function openProjectForReview() { function openProjectForReview() {
moderationStore.setSingleProject(props.queueEntry.project.id); moderationStore.setSingleProject(props.queueEntry.project.id)
navigateTo({ navigateTo({
name: "type-id", name: 'type-id',
params: { params: {
type: "project", type: 'project',
id: props.queueEntry.project.id, id: props.queueEntry.project.id,
}, },
state: { state: {
showChecklist: true, showChecklist: true,
}, },
}); })
} }
function getSubmittedTime(project: any): string { function getSubmittedTime(): string {
const date = const date =
props.queueEntry.project.queued || props.queueEntry.project.queued ||
props.queueEntry.project.created || props.queueEntry.project.created ||
props.queueEntry.project.updated; props.queueEntry.project.updated
if (!date) return "Unknown"; if (!date) return 'Unknown'
try { try {
return `Submitted ${formatRelativeTime(dayjs(date).toISOString())}`; return `Submitted ${formatRelativeTime(dayjs(date).toISOString())}`
} catch { } catch {
return "Unknown"; return 'Unknown'
} }
} }
</script> </script>

View File

@ -87,7 +87,7 @@
v-if="report.target.type === 'organization'" v-if="report.target.type === 'organization'"
class="align-middle" class="align-middle"
/> />
{{ report.target.name || "Unknown User" }} {{ report.target.name || 'Unknown User' }}
</span> </span>
</nuxt-link> </nuxt-link>
@ -102,7 +102,7 @@
class="max-w-[200px] truncate font-mono text-xs sm:max-w-none" class="max-w-[200px] truncate font-mono text-xs sm:max-w-none"
> >
{{ {{
report.version.files.find((file) => file.primary)?.filename || "Unknown Version" report.version.files.find((file) => file.primary)?.filename || 'Unknown Version'
}} }}
</span> </span>
</div> </div>
@ -120,7 +120,7 @@
</div> </div>
</div> </div>
<CollapsibleRegion class="my-4" ref="collapsibleRegion"> <CollapsibleRegion ref="collapsibleRegion" class="my-4">
<ReportThread <ReportThread
v-if="report.thread" v-if="report.thread"
ref="reportThread" ref="reportThread"
@ -135,81 +135,82 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
Avatar,
useRelativeTime,
OverflowMenu,
type OverflowMenuOption,
CollapsibleRegion,
ButtonStyled,
} from "@modrinth/ui";
import {
EllipsisVerticalIcon,
OrganizationIcon,
EyeIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
EllipsisVerticalIcon,
EyeIcon,
LinkIcon, LinkIcon,
} from "@modrinth/assets"; OrganizationIcon,
} from '@modrinth/assets'
import { import {
type ExtendedReport, type ExtendedReport,
reportQuickReplies, reportQuickReplies,
type ReportQuickReply, type ReportQuickReply,
} from "@modrinth/moderation"; } from '@modrinth/moderation'
import ChevronDownIcon from "../servers/icons/ChevronDownIcon.vue"; import {
import ReportThread from "../thread/ReportThread.vue"; Avatar,
ButtonStyled,
CollapsibleRegion,
OverflowMenu,
type OverflowMenuOption,
useRelativeTime,
} from '@modrinth/ui'
import ChevronDownIcon from '../servers/icons/ChevronDownIcon.vue'
import ReportThread from '../thread/ReportThread.vue'
const props = defineProps<{ const props = defineProps<{
report: ExtendedReport; report: ExtendedReport
}>(); }>()
const reportThread = ref<InstanceType<typeof ReportThread> | null>(null); const reportThread = ref<InstanceType<typeof ReportThread> | null>(null)
const collapsibleRegion = ref<InstanceType<typeof CollapsibleRegion> | null>(null); const collapsibleRegion = ref<InstanceType<typeof CollapsibleRegion> | null>(null)
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime()
function updateThread(newThread: any) { function updateThread(newThread: any) {
if (props.report.thread) { if (props.report.thread) {
Object.assign(props.report.thread, newThread); Object.assign(props.report.thread, newThread)
} }
} }
const quickActions: OverflowMenuOption[] = [ const quickActions: OverflowMenuOption[] = [
{ {
id: "copy-link", id: 'copy-link',
action: () => { action: () => {
const base = window.location.origin; const base = window.location.origin
const reportUrl = `${base}/moderation/reports/${props.report.id}`; const reportUrl = `${base}/moderation/reports/${props.report.id}`
navigator.clipboard.writeText(reportUrl).then(() => { navigator.clipboard.writeText(reportUrl).then(() => {
addNotification({ addNotification({
type: "success", type: 'success',
title: "Report link copied", title: 'Report link copied',
text: "The link to this report has been copied to your clipboard.", text: 'The link to this report has been copied to your clipboard.',
}); })
}); })
}, },
}, },
{ {
id: "copy-id", id: 'copy-id',
action: () => { action: () => {
navigator.clipboard.writeText(props.report.id).then(() => { navigator.clipboard.writeText(props.report.id).then(() => {
addNotification({ addNotification({
type: "success", type: 'success',
title: "Report ID copied", title: 'Report ID copied',
text: "The ID of this report has been copied to your clipboard.", text: 'The ID of this report has been copied to your clipboard.',
}); })
}); })
}, },
}, },
]; ]
const visibleQuickReplies = computed<OverflowMenuOption[]>(() => { const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
return reportQuickReplies return reportQuickReplies
.filter((reply) => { .filter((reply) => {
if (reply.shouldShow === undefined) return true; if (reply.shouldShow === undefined) return true
if (typeof reply.shouldShow === "function") { if (typeof reply.shouldShow === 'function') {
return reply.shouldShow(props.report); return reply.shouldShow(props.report)
} }
return reply.shouldShow; return reply.shouldShow
}) })
.map( .map(
(reply) => (reply) =>
@ -217,59 +218,61 @@ const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
id: reply.label, id: reply.label,
action: () => handleQuickReply(reply), action: () => handleQuickReply(reply),
}) as OverflowMenuOption, }) as OverflowMenuOption,
); )
}); })
async function handleQuickReply(reply: ReportQuickReply) { async function handleQuickReply(reply: ReportQuickReply) {
const message = const message =
typeof reply.message === "function" ? await reply.message(props.report) : reply.message; typeof reply.message === 'function' ? await reply.message(props.report) : reply.message
collapsibleRegion.value?.setCollapsed(false); collapsibleRegion.value?.setCollapsed(false)
await nextTick(); await nextTick()
reportThread.value?.setReplyContent(message); reportThread.value?.setReplyContent(message)
} }
const reportItemAvatarUrl = computed(() => { const reportItemAvatarUrl = computed(() => {
switch (props.report.item_type) { switch (props.report.item_type) {
case "project": case 'project':
case "version": case 'version':
return props.report.project?.icon_url || ""; return props.report.project?.icon_url || ''
case "user": case 'user':
return props.report.user?.avatar_url || ""; return props.report.user?.avatar_url || ''
default: default:
return undefined; return undefined
} }
}); })
const reportItemTitle = computed(() => { const reportItemTitle = computed(() => {
if (props.report.item_type === "user") return props.report.user?.username || "Unknown User"; if (props.report.item_type === 'user') return props.report.user?.username || 'Unknown User'
return props.report.project?.title || "Unknown Project"; return props.report.project?.title || 'Unknown Project'
}); })
const reportItemUrl = computed(() => { const reportItemUrl = computed(() => {
switch (props.report.item_type) { switch (props.report.item_type) {
case "user": case 'user':
return `/user/${props.report.user?.username}`; return `/user/${props.report.user?.username}`
case "project": case 'project':
return `/${props.report.project?.project_type}/${props.report.project?.slug}`; return `/${props.report.project?.project_type}/${props.report.project?.slug}`
case "version": case 'version':
return `/${props.report.project?.project_type}/${props.report.project?.slug}/versions/${props.report.version?.id}`; return `/${props.report.project?.project_type}/${props.report.project?.slug}/versions/${props.report.version?.id}`
default:
return ''
} }
}); })
const formattedItemType = computed(() => { const formattedItemType = computed(() => {
const itemType = props.report.item_type; const itemType = props.report.item_type
return itemType.charAt(0).toUpperCase() + itemType.slice(1); return itemType.charAt(0).toUpperCase() + itemType.slice(1)
}); })
const formattedReportType = computed(() => { const formattedReportType = computed(() => {
const reportType = props.report.report_type; const reportType = props.report.report_type
// some are split by -, some are split by " " // some are split by -, some are split by " "
const words = reportType.includes("-") ? reportType.split("-") : reportType.split(" "); const words = reportType.includes('-') ? reportType.split('-') : reportType.split(' ')
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}); })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -29,9 +29,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { type KeybindListener, keybinds, normalizeKeybind } from "@modrinth/moderation";
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue"; import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
import { keybinds, type KeybindListener, normalizeKeybind } from "@modrinth/moderation"; import { ref } from "vue";
const modal = ref<InstanceType<typeof NewModal>>(); const modal = ref<InstanceType<typeof NewModal>>();

View File

@ -146,18 +146,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets"; import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { import type {
ModerationFlameModpackItem,
ModerationJudgements, ModerationJudgements,
ModerationModpackItem, ModerationModpackItem,
ModerationModpackResponse,
ModerationUnknownModpackItem,
ModerationFlameModpackItem,
ModerationModpackPermissionApprovalType, ModerationModpackPermissionApprovalType,
ModerationModpackResponse,
ModerationPermissionType, ModerationPermissionType,
ModerationUnknownModpackItem,
} from "@modrinth/utils"; } from "@modrinth/utils";
import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, onMounted } from "vue";
import { useLocalStorage, useSessionStorage } from "@vueuse/core"; import { useLocalStorage, useSessionStorage } from "@vueuse/core";
import { computed, onMounted,ref, watch } from "vue";
const props = defineProps<{ const props = defineProps<{
projectId: string; projectId: string;

View File

@ -1,4 +1,5 @@
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue"; import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
import { startLoading, stopLoading, useNuxtApp } from "#imports"; import { startLoading, stopLoading, useNuxtApp } from "#imports";
export default defineComponent({ export default defineComponent({

View File

@ -28,7 +28,7 @@
import { NewspaperIcon } from "@modrinth/assets"; import { NewspaperIcon } from "@modrinth/assets";
import { articles as rawArticles } from "@modrinth/blog"; import { articles as rawArticles } from "@modrinth/blog";
import { ButtonStyled, NewsArticleCard } from "@modrinth/ui"; import { ButtonStyled, NewsArticleCard } from "@modrinth/ui";
import { ref, computed } from "vue"; import { computed,ref } from "vue";
const articles = ref( const articles = ref(
rawArticles rawArticles

View File

@ -106,8 +106,9 @@
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets"; import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui"; import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils"; import { formatProjectType } from "@modrinth/utils";
import { renderHighlightedString } from "~/helpers/highlight.js";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue"; import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { getProjectTypeForUrl } from "~/helpers/projects.js"; import { getProjectTypeForUrl } from "~/helpers/projects.js";
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime();

View File

@ -22,8 +22,8 @@
</template> </template>
<script setup> <script setup>
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import ReportInfo from "~/components/ui/report/ReportInfo.vue"; import ReportInfo from "~/components/ui/report/ReportInfo.vue";
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import { addReportMessage } from "~/helpers/threads.js"; import { addReportMessage } from "~/helpers/threads.js";
const props = defineProps({ const props = defineProps({

View File

@ -25,6 +25,7 @@
</template> </template>
<script setup> <script setup>
import { Chips } from "@modrinth/ui"; import { Chips } from "@modrinth/ui";
import ReportInfo from "~/components/ui/report/ReportInfo.vue"; import ReportInfo from "~/components/ui/report/ReportInfo.vue";
import { addReportMessage } from "~/helpers/threads.js"; import { addReportMessage } from "~/helpers/threads.js";
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts"; import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";

View File

@ -42,11 +42,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, computed } from "vue";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets"; import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils"; import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { computed,nextTick, ref } from "vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;

View File

@ -18,9 +18,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import { ConfirmModal } from "@modrinth/ui"; import { ConfirmModal } from "@modrinth/ui";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
import { ref } from "vue";
import BackupItem from "~/components/ui/servers/BackupItem.vue"; import BackupItem from "~/components/ui/servers/BackupItem.vue";
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -1,23 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from "dayjs";
import { import {
MoreVerticalIcon,
HistoryIcon,
DownloadIcon,
SpinnerIcon,
EditIcon,
LockIcon,
TrashIcon,
FolderArchiveIcon,
BotIcon, BotIcon,
XIcon, DownloadIcon,
EditIcon,
FolderArchiveIcon,
HistoryIcon,
LockIcon,
LockOpenIcon, LockOpenIcon,
MoreVerticalIcon,
RotateCounterClockwiseIcon, RotateCounterClockwiseIcon,
SpinnerIcon,
TrashIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui"; import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
import { defineMessages, useVIntl } from "@vintl/vintl";
import { ref, computed } from "vue";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
import { defineMessages, useVIntl } from "@vintl/vintl";
import dayjs from "dayjs";
import { computed,ref } from "vue";
const flags = useFeatureFlags(); const flags = useFeatureFlags();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();

View File

@ -45,11 +45,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, computed } from "vue"; import { IssuesIcon,SaveIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { computed,nextTick, ref } from "vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;

View File

@ -17,11 +17,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import type { NewModal } from "@modrinth/ui";
import { ConfirmModal, NewModal } from "@modrinth/ui"; import { ConfirmModal } from "@modrinth/ui";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
import { ref } from "vue";
import BackupItem from "~/components/ui/servers/BackupItem.vue"; import BackupItem from "~/components/ui/servers/BackupItem.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;

View File

@ -56,10 +56,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon,XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { XIcon, SaveIcon } from "@modrinth/assets"; import { computed,ref } from "vue";
import { ref, computed } from "vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;

View File

@ -229,17 +229,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
DropdownIcon,
XIcon,
CheckIcon, CheckIcon,
LockOpenIcon, DropdownIcon,
GameIcon,
ExternalIcon, ExternalIcon,
GameIcon,
LockOpenIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui"; import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui";
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue"; import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
import { ref, computed } from "vue";
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils"; import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
import { computed,ref } from "vue";
import Accordion from "~/components/ui/Accordion.vue"; import Accordion from "~/components/ui/Accordion.vue";
import Checkbox from "~/components/ui/Checkbox.vue"; import Checkbox from "~/components/ui/Checkbox.vue";
import ContentVersionFilter, { import ContentVersionFilter, {

View File

@ -58,11 +58,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { FilterIcon } from "@modrinth/assets"; import { FilterIcon } from "@modrinth/assets";
import { type Version, formatCategory, type GameVersionTag } from "@modrinth/utils";
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
import ManySelect from "@modrinth/ui/src/components/base/ManySelect.vue";
import Checkbox from "@modrinth/ui/src/components/base/Checkbox.vue"; import Checkbox from "@modrinth/ui/src/components/base/Checkbox.vue";
import ManySelect from "@modrinth/ui/src/components/base/ManySelect.vue";
import { formatCategory, type GameVersionTag,type Version } from "@modrinth/utils";
import { computed,ref } from "vue";
import { useRoute } from "vue-router";
export type ListedGameVersion = { export type ListedGameVersion = {
name: string; name: string;

View File

@ -65,27 +65,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { import {
MoreHorizontalIcon,
EditIcon,
DownloadIcon, DownloadIcon,
TrashIcon, EditIcon,
FolderOpenIcon,
FileIcon,
RightArrowIcon,
PackageOpenIcon,
FileArchiveIcon, FileArchiveIcon,
FileIcon,
FolderOpenIcon,
MoreHorizontalIcon,
PackageOpenIcon,
RightArrowIcon,
TrashIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { computed, shallowRef, ref } from "vue"; import { ButtonStyled } from "@modrinth/ui";
import { computed, ref,shallowRef } from "vue";
import { renderToString } from "vue/server-renderer"; import { renderToString } from "vue/server-renderer";
import { useRouter, useRoute } from "vue-router"; import { useRoute,useRouter } from "vue-router";
import { import {
UiServersIconsCodeFileIcon,
UiServersIconsCogFolderIcon, UiServersIconsCogFolderIcon,
UiServersIconsEarthIcon, UiServersIconsEarthIcon,
UiServersIconsCodeFileIcon,
UiServersIconsTextFileIcon,
UiServersIconsImageFileIcon, UiServersIconsImageFileIcon,
UiServersIconsTextFileIcon,
} from "#components"; } from "#components";
import PaletteIcon from "~/assets/icons/palette.svg?component"; import PaletteIcon from "~/assets/icons/palette.svg?component";

View File

@ -25,16 +25,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FileIcon, HomeIcon } from "@modrinth/assets"; import { FileIcon, HomeIcon } from '@modrinth/assets'
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from '@modrinth/ui'
defineProps<{ defineProps<{
title: string; title: string
message: string; message: string
}>(); }>()
defineEmits<{ defineEmits<{
(e: "refetch"): void; (e: 'refetch' | 'home'): void
(e: "home"): void; }>()
}>();
</script> </script>

View File

@ -43,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue"; import { computed, onMounted, onUnmounted,ref } from "vue";
const props = defineProps<{ const props = defineProps<{
items: any[]; items: any[];

View File

@ -54,7 +54,7 @@
}" }"
@click="$emit('navigate', index)" @click="$emit('navigate', index)"
> >
{{ segment || "" }} {{ segment || '' }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ChevronRightIcon <ChevronRightIcon
@ -154,60 +154,59 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
LinkIcon,
CurseForgeIcon,
FileArchiveIcon,
BoxIcon, BoxIcon,
PlusIcon,
UploadIcon,
DropdownIcon,
FolderOpenIcon,
SearchIcon,
HomeIcon,
ChevronRightIcon, ChevronRightIcon,
CurseForgeIcon,
DropdownIcon,
FileArchiveIcon,
FilterIcon, FilterIcon,
} from "@modrinth/assets"; FolderOpenIcon,
import { ButtonStyled, OverflowMenu } from "@modrinth/ui"; HomeIcon,
import { ref, computed } from "vue"; LinkIcon,
import { useIntersectionObserver } from "@vueuse/core"; PlusIcon,
SearchIcon,
UploadIcon,
} from '@modrinth/assets'
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { useIntersectionObserver } from '@vueuse/core'
import { computed, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
breadcrumbSegments: string[]; breadcrumbSegments: string[]
searchQuery: string; searchQuery: string
currentFilter: string; currentFilter: string
baseId: string; baseId: string
}>(); }>()
defineEmits<{ defineEmits<{
(e: "navigate", index: number): void; (e: 'navigate', index: number): void
(e: "create", type: "file" | "directory"): void; (e: 'create', type: 'file' | 'directory'): void
(e: "upload" | "upload-zip"): void; (e: 'upload' | 'upload-zip'): void
(e: "unzip-from-url", cf: boolean): void; (e: 'unzip-from-url', cf: boolean): void
(e: "update:searchQuery", value: string): void; (e: 'update:searchQuery' | 'filter', value: string): void
(e: "filter", type: string): void; }>()
}>();
const pyroFilesSentinel = ref<HTMLElement | null>(null); const pyroFilesSentinel = ref<HTMLElement | null>(null)
const isStuck = ref(false); const isStuck = ref(false)
useIntersectionObserver( useIntersectionObserver(
pyroFilesSentinel, pyroFilesSentinel,
([{ isIntersecting }]) => { ([{ isIntersecting }]) => {
isStuck.value = !isIntersecting; isStuck.value = !isIntersecting
}, },
{ threshold: [0, 1] }, { threshold: [0, 1] },
); )
const filterLabel = computed(() => { const filterLabel = computed(() => {
switch (props.currentFilter) { switch (props.currentFilter) {
case "filesOnly": case 'filesOnly':
return "Files only"; return 'Files only'
case "foldersOnly": case 'foldersOnly':
return "Folders only"; return 'Folders only'
default: default:
return "Show all"; return 'Show all'
} }
}); })
</script> </script>
<style scoped> <style scoped>

View File

@ -55,33 +55,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { EditIcon, DownloadIcon, TrashIcon, RightArrowIcon } from "@modrinth/assets"; import { DownloadIcon, EditIcon, RightArrowIcon, TrashIcon } from '@modrinth/assets'
interface FileItem { interface FileItem {
type: string; type: string
name: string; name: string
[key: string]: any; [key: string]: any
} }
defineProps<{ defineProps<{
item: FileItem | null; item: FileItem | null
x: number; x: number
y: number; y: number
isAtBottom: boolean; isAtBottom: boolean
}>(); }>()
const ctxRef = ref<HTMLElement | null>(null); const ctxRef = ref<HTMLElement | null>(null)
defineEmits<{ defineEmits<{
(e: "rename", item: FileItem): void; (e: 'rename' | 'move' | 'download' | 'delete', item: FileItem): void
(e: "move", item: FileItem): void; }>()
(e: "download", item: FileItem): void;
(e: "delete", item: FileItem): void;
}>();
defineExpose({ defineExpose({
ctxRef, ctxRef,
}); })
</script> </script>
<style scoped> <style scoped>

View File

@ -35,7 +35,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { PlusIcon, XIcon } from "@modrinth/assets"; import { PlusIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ref, computed, nextTick } from "vue"; import { computed, nextTick,ref } from "vue";
const props = defineProps<{ const props = defineProps<{
type: "file" | "directory"; type: "file" | "directory";

View File

@ -42,8 +42,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from "@modrinth/assets"; import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
defineProps<{ defineProps<{
item: { item: {

View File

@ -39,7 +39,7 @@
:class="{ '!text-contrast': index === breadcrumbSegments.length - 1 }" :class="{ '!text-contrast': index === breadcrumbSegments.length - 1 }"
@click="$emit('navigate', index)" @click="$emit('navigate', index)"
> >
{{ segment || "" }} {{ segment || '' }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ChevronRightIcon <ChevronRightIcon
@ -105,36 +105,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DropdownIcon, SaveIcon, ShareIcon, HomeIcon, ChevronRightIcon } from "@modrinth/assets"; import { ChevronRightIcon, DropdownIcon, HomeIcon, SaveIcon, ShareIcon } from '@modrinth/assets'
import { Button, ButtonStyled } from "@modrinth/ui"; import { Button, ButtonStyled } from '@modrinth/ui'
import { computed } from "vue"; import { computed } from 'vue'
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from 'vue-router'
const props = defineProps<{ const props = defineProps<{
breadcrumbSegments: string[]; breadcrumbSegments: string[]
fileName?: string; fileName?: string
isImage: boolean; isImage: boolean
filePath?: string; filePath?: string
}>(); }>()
const isLogFile = computed(() => { const isLogFile = computed(() => {
return props.filePath?.startsWith("logs") || props.filePath?.endsWith(".log"); return props.filePath?.startsWith('logs') || props.filePath?.endsWith('.log')
}); })
const route = useRoute(); const route = useRoute()
const router = useRouter(); const router = useRouter()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "cancel"): void; (e: 'cancel' | 'save' | 'save-as' | 'save-restart' | 'share'): void
(e: "save"): void; (e: 'navigate', index: number): void
(e: "save-as"): void; }>()
(e: "save-restart"): void;
(e: "share"): void;
(e: "navigate", index: number): void;
}>();
const goHome = () => { const goHome = () => {
emit("cancel"); emit('cancel')
router.push({ path: "/servers/manage/" + route.params.id + "/files" }); router.push({ path: '/servers/manage/' + route.params.id + '/files' })
}; }
</script> </script>

View File

@ -53,9 +53,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { ZoomInIcon, ZoomOutIcon } from "@modrinth/assets"; import { ZoomInIcon, ZoomOutIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
const ZOOM_MIN = 0.1; const ZOOM_MIN = 0.1;
const ZOOM_MAX = 5; const ZOOM_MAX = 5;

View File

@ -39,7 +39,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowBigUpDashIcon, XIcon } from "@modrinth/assets"; import { ArrowBigUpDashIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ref, nextTick, computed } from "vue"; import { computed,nextTick, ref } from "vue";
const destinationInput = ref<HTMLInputElement | null>(null); const destinationInput = ref<HTMLInputElement | null>(null);

View File

@ -34,7 +34,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditIcon, XIcon } from "@modrinth/assets"; import { EditIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ref, computed, nextTick } from "vue"; import { computed, nextTick,ref } from "vue";
const props = defineProps<{ const props = defineProps<{
item: { name: string; type: string } | null; item: { name: string; type: string } | null;

View File

@ -27,9 +27,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckIcon,XIcon } from "@modrinth/assets";
import { ConfirmModal } from "@modrinth/ui"; import { ConfirmModal } from "@modrinth/ui";
import { ref } from "vue"; import { ref } from "vue";
import { XIcon, CheckIcon } from "@modrinth/assets";
const path = ref(""); const path = ref("");
const files = ref<string[]>([]); const files = ref<string[]>([]);

View File

@ -101,10 +101,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets"; import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, nextTick } from "vue"; import { computed, nextTick,ref, watch } from "vue";
import { FSModule } from "~/composables/servers/modules/fs.ts";
import type { FSModule } from "~/composables/servers/modules/fs.ts";
interface UploadItem { interface UploadItem {
file: File; file: File;
@ -152,7 +153,7 @@ const activeUploads = computed(() =>
const onUploadStatusEnter = (el: Element) => { const onUploadStatusEnter = (el: Element) => {
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0); const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
(el as HTMLElement).style.height = "0"; (el as HTMLElement).style.height = "0";
// eslint-disable-next-line no-void
void (el as HTMLElement).offsetHeight; void (el as HTMLElement).offsetHeight;
(el as HTMLElement).style.height = `${height}px`; (el as HTMLElement).style.height = `${height}px`;
}; };
@ -160,7 +161,7 @@ const onUploadStatusEnter = (el: Element) => {
const onUploadStatusLeave = (el: Element) => { const onUploadStatusLeave = (el: Element) => {
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0); const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
(el as HTMLElement).style.height = `${height}px`; (el as HTMLElement).style.height = `${height}px`;
// eslint-disable-next-line no-void
void (el as HTMLElement).offsetHeight; void (el as HTMLElement).offsetHeight;
(el as HTMLElement).style.height = "0"; (el as HTMLElement).style.height = "0";
}; };

View File

@ -73,11 +73,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets"; import { DownloadIcon, ExternalIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui"; import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError } from "@modrinth/utils"; import { ModrinthServersFetchError } from "@modrinth/utils";
import { ref, computed, nextTick } from "vue"; import { computed, nextTick,ref } from "vue";
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import { handleError } from "~/composables/servers/modrinth-servers.ts";
const cf = ref(false); const cf = ref(false);

View File

@ -44,7 +44,7 @@
<script setup> <script setup>
import * as THREE from "three"; import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { ref, onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted,ref } from "vue";
const container = ref(null); const container = ref(null);
const showLabels = ref(false); const showLabels = ref(false);

View File

@ -14,7 +14,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted,ref } from "vue";
const msgs = [ const msgs = [
"Organizing files...", "Organizing files...",

View File

@ -19,9 +19,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { computed, onMounted, onUnmounted,ref } from "vue";
const props = defineProps<{ const props = defineProps<{
log: string; log: string;

View File

@ -104,23 +104,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue";
import { import {
PlayIcon,
UpdatedIcon,
StopCircleIcon,
SlashIcon,
XIcon,
CheckIcon, CheckIcon,
ServerIcon, ClipboardCopyIcon,
InfoIcon, InfoIcon,
MoreVerticalIcon, MoreVerticalIcon,
ClipboardCopyIcon, PlayIcon,
ServerIcon,
SlashIcon,
StopCircleIcon,
UpdatedIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { useRouter } from "vue-router";
import { useStorage } from "@vueuse/core";
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils"; import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
import { useStorage } from "@vueuse/core";
import { computed,ref } from "vue";
import { useRouter } from "vue-router";
const flags = useFeatureFlags(); const flags = useFeatureFlags();

View File

@ -39,8 +39,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import type { ServerState } from "@modrinth/utils"; import type { ServerState } from "@modrinth/utils";
import { ref } from "vue";
const STATUS_CLASSES = { const STATUS_CLASSES = {
running: { main: "bg-brand", bg: "bg-bg-green" }, running: { main: "bg-brand", bg: "bg-bg-green" },

View File

@ -295,12 +295,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RightArrowIcon, CopyIcon, XIcon, SearchIcon, EyeIcon } from "@modrinth/assets"; import { CopyIcon, EyeIcon,RightArrowIcon, SearchIcon, XIcon } from "@modrinth/assets";
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { NewModal } from "@modrinth/ui"; import { NewModal } from "@modrinth/ui";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue"; import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import { useDebounceFn } from "@vueuse/core";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { computed, nextTick,onMounted, onUnmounted, ref, watch } from "vue";
import { useModrinthServersConsole } from "~/store/console.ts"; import { useModrinthServersConsole } from "~/store/console.ts";
const { $cosmetics } = useNuxtApp(); const { $cosmetics } = useNuxtApp();

View File

@ -67,10 +67,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { DownloadIcon, XIcon } from "@modrinth/assets"; import { DownloadIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError } from "@modrinth/utils"; import { ModrinthServersFetchError } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;

View File

@ -144,18 +144,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { import {
UploadIcon,
RightArrowIcon,
XIcon,
ServerIcon,
ArrowBigRightDashIcon, ArrowBigRightDashIcon,
RightArrowIcon,
ServerIcon,
UploadIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils"; import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers"; import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
const handleBeforeUnload = (event: BeforeUnloadEvent) => { const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (isLoading.value) { if (isLoading.value) {

View File

@ -197,12 +197,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets"; import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
import { $fetch } from "ofetch"; import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils"; import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
import { $fetch } from "ofetch";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue"; import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
@ -337,7 +338,7 @@ const selectedLoaderVersions = computed<string[]>(() => {
} }
const backwardsCompatibleVersion = loaderVersions.value[apiLoader]?.find( const backwardsCompatibleVersion = loaderVersions.value[apiLoader]?.find(
// eslint-disable-next-line no-template-curly-in-string
(x) => x.id === "${modrinth.gameVersion}", (x) => x.id === "${modrinth.gameVersion}",
); );

View File

@ -31,7 +31,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{ const props = defineProps<{
isUpdating: boolean; isUpdating: boolean;

View File

@ -158,31 +158,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, NewProjectCard } from "@modrinth/ui"; import { CompassIcon, InfoIcon, SettingsIcon, TransferIcon, UploadIcon } from '@modrinth/assets'
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets"; import { ButtonStyled, NewProjectCard } from '@modrinth/ui'
import type { Loaders } from "@modrinth/utils"; import type { Loaders } from '@modrinth/utils'
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { formatMessage } = useVIntl(); import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
const { formatMessage } = useVIntl()
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer
ignoreCurrentInstallation?: boolean; ignoreCurrentInstallation?: boolean
backupInProgress?: BackupInProgressReason; backupInProgress?: BackupInProgressReason
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
reinstall: [any?]; reinstall: [any?]
}>(); }>()
const isInstalling = computed(() => props.server.general?.status === "installing"); const isInstalling = computed(() => props.server.general?.status === 'installing')
const versionSelectModal = ref(); const versionSelectModal = ref()
const mrpackModal = ref(); const mrpackModal = ref()
const modpackVersionModal = ref(); const modpackVersionModal = ref()
const data = computed(() => props.server.general); const data = computed(() => props.server.general)
const { const {
data: versions, data: versions,
@ -191,17 +192,17 @@ const {
} = await useAsyncData( } = await useAsyncData(
`content-loader-versions-${data.value?.upstream?.project_id}`, `content-loader-versions-${data.value?.upstream?.project_id}`,
async () => { async () => {
if (!data.value?.upstream?.project_id) return []; if (!data.value?.upstream?.project_id) return []
try { try {
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`); const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`)
return result || []; return result || []
} catch (e) { } catch (e) {
console.error("couldnt fetch all versions:", e); console.error('couldnt fetch all versions:', e)
throw new Error("Failed to load modpack versions."); throw new Error('Failed to load modpack versions.')
} }
}, },
{ default: () => [] }, { default: () => [] },
); )
const { const {
data: currentVersion, data: currentVersion,
@ -210,17 +211,17 @@ const {
} = await useAsyncData( } = await useAsyncData(
`content-loader-version-${data.value?.upstream?.version_id}`, `content-loader-version-${data.value?.upstream?.version_id}`,
async () => { async () => {
if (!data.value?.upstream?.version_id) return null; if (!data.value?.upstream?.version_id) return null
try { try {
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`); const result = await useBaseFetch(`version/${data.value.upstream.version_id}`)
return result || null; return result || null
} catch (e) { } catch (e) {
console.error("couldnt fetch version:", e); console.error('couldnt fetch version:', e)
throw new Error("Failed to load modpack version."); throw new Error('Failed to load modpack version.')
} }
}, },
{ default: () => null }, { default: () => null },
); )
const projectCardData = computed(() => ({ const projectCardData = computed(() => ({
icon_url: data.value?.project?.icon_url, icon_url: data.value?.project?.icon_url,
@ -228,43 +229,43 @@ const projectCardData = computed(() => ({
description: data.value?.project?.description, description: data.value?.project?.description,
downloads: data.value?.project?.downloads, downloads: data.value?.project?.downloads,
follows: data.value?.project?.followers, follows: data.value?.project?.followers,
// @ts-ignore // @ts-expect-error
date_modified: currentVersion.value?.date_published || data.value?.project?.updated, date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
})); }))
const selectLoader = (loader: string) => { const selectLoader = (loader: string) => {
versionSelectModal.value?.show(loader as Loaders); versionSelectModal.value?.show(loader as Loaders)
}; }
const refreshData = async () => { const refreshData = async () => {
await Promise.all([refreshVersions(), refreshCurrentVersion()]); await Promise.all([refreshVersions(), refreshCurrentVersion()])
}; }
const updateAvailable = computed(() => { const updateAvailable = computed(() => {
// so sorry // so sorry
// @ts-ignore // @ts-expect-error
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) { if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
return false; return false
} }
// @ts-ignore // @ts-expect-error
const latestVersion = versions.value[0]; const latestVersion = versions.value[0]
// @ts-ignore // @ts-expect-error
return latestVersion.id !== currentVersion.value.id; return latestVersion.id !== currentVersion.value.id
}); })
watch( watch(
() => props.server.general?.status, () => props.server.general?.status,
async (newStatus, oldStatus) => { async (newStatus, oldStatus) => {
if (oldStatus === "installing" && newStatus === "available") { if (oldStatus === 'installing' && newStatus === 'available') {
await Promise.all([ await Promise.all([
refreshVersions(), refreshVersions(),
refreshCurrentVersion(), refreshCurrentVersion(),
props.server.refresh(["general"]), props.server.refresh(['general']),
]); ])
} }
}, },
); )
</script> </script>
<style scoped> <style scoped>

View File

@ -102,9 +102,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets"; import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
import type { Project, Server } from "@modrinth/utils";
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import { Avatar, CopyCode } from "@modrinth/ui"; import { Avatar, CopyCode } from "@modrinth/ui";
import type { Project, Server } from "@modrinth/utils";
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<Partial<Server>>(); const props = defineProps<Partial<Server>>();

View File

@ -37,8 +37,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { RightArrowIcon } from "@modrinth/assets"; import { RightArrowIcon } from "@modrinth/assets";
import type { RouteLocationNormalized } from "vue-router"; import type { RouteLocationNormalized } from "vue-router";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue"; import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const emit = defineEmits(["reinstall"]); const emit = defineEmits(["reinstall"]);

View File

@ -67,10 +67,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, shallowRef } from "vue"; import { CpuIcon, DatabaseIcon, FolderOpenIcon, IssuesIcon } from "@modrinth/assets";
import { FolderOpenIcon, CpuIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
import { useStorage } from "@vueuse/core";
import type { Stats } from "@modrinth/utils"; import type { Stats } from "@modrinth/utils";
import { useStorage } from "@vueuse/core";
import { computed, ref, shallowRef } from "vue";
const flags = useFeatureFlags(); const flags = useFeatureFlags();
const route = useNativeRoute(); const route = useNativeRoute();

View File

@ -86,24 +86,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DropdownIcon } from "@modrinth/assets"; import { DropdownIcon } from '@modrinth/assets'
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from "vue"; import type { CSSProperties } from 'vue'
import type { CSSProperties } from "vue"; import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
const ITEM_HEIGHT = 44; const ITEM_HEIGHT = 44
const BUFFER_ITEMS = 5; const BUFFER_ITEMS = 5
type OptionValue = string | number | Record<string, any>; type OptionValue = string | number | Record<string, any>
interface Props { interface Props {
options: OptionValue[]; options: OptionValue[]
name: string; name: string
defaultValue?: OptionValue | null; defaultValue?: OptionValue | null
placeholder?: string | number | null; placeholder?: string | number | null
modelValue?: OptionValue | null; modelValue?: OptionValue | null
renderUp?: boolean; renderUp?: boolean
disabled?: boolean; disabled?: boolean
displayName?: (option: OptionValue) => string; displayName?: (option: OptionValue) => string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -113,347 +113,346 @@ const props = withDefaults(defineProps<Props>(), {
renderUp: false, renderUp: false,
disabled: false, disabled: false,
displayName: (option: OptionValue) => String(option), displayName: (option: OptionValue) => String(option),
}); })
const emit = defineEmits<{ const emit = defineEmits<{
(e: "input", value: OptionValue): void; (e: 'input' | 'update:modelValue', value: OptionValue): void
(e: "change", value: { option: OptionValue; index: number }): void; (e: 'change', value: { option: OptionValue; index: number }): void
(e: "update:modelValue", value: OptionValue): void; }>()
}>();
const dropdownVisible = ref(false); const dropdownVisible = ref(false)
const selectedValue = ref<OptionValue | null>(props.modelValue || props.defaultValue); const selectedValue = ref<OptionValue | null>(props.modelValue || props.defaultValue)
const focusedOptionIndex = ref<number | null>(null); const focusedOptionIndex = ref<number | null>(null)
const optionsContainer = ref<HTMLElement | null>(null); const optionsContainer = ref<HTMLElement | null>(null)
const scrollTop = ref(0); const scrollTop = ref(0)
const isRenderingUp = ref(false); const isRenderingUp = ref(false)
const virtualListHeight = ref(300); const virtualListHeight = ref(300)
const isOpen = ref(false); const isOpen = ref(false)
const openDropdownCount = ref(0); const openDropdownCount = ref(0)
const listboxId = `pyro-listbox-${Math.random().toString(36).substring(2, 11)}`; const listboxId = `pyro-listbox-${Math.random().toString(36).substring(2, 11)}`
const triggerRef = ref<HTMLButtonElement | null>(null); const triggerRef = ref<HTMLButtonElement | null>(null)
const positionStyle = ref<CSSProperties>({ const positionStyle = ref<CSSProperties>({
position: "fixed", position: 'fixed',
top: "0px", top: '0px',
left: "0px", left: '0px',
width: "0px", width: '0px',
zIndex: 999, zIndex: 999,
}); })
const totalHeight = computed(() => props.options.length * ITEM_HEIGHT); const totalHeight = computed(() => props.options.length * ITEM_HEIGHT)
const visibleOptions = computed(() => { const visibleOptions = computed(() => {
const startIndex = Math.floor(scrollTop.value / ITEM_HEIGHT) - BUFFER_ITEMS; const startIndex = Math.floor(scrollTop.value / ITEM_HEIGHT) - BUFFER_ITEMS
const visibleCount = Math.ceil(virtualListHeight.value / ITEM_HEIGHT) + 2 * BUFFER_ITEMS; const visibleCount = Math.ceil(virtualListHeight.value / ITEM_HEIGHT) + 2 * BUFFER_ITEMS
return Array.from({ length: visibleCount }, (_, i) => { return Array.from({ length: visibleCount }, (_, i) => {
const index = startIndex + i; const index = startIndex + i
if (index >= 0 && index < props.options.length) { if (index >= 0 && index < props.options.length) {
return { return {
index, index,
option: props.options[index], option: props.options[index],
}; }
} }
return null; return null
}).filter((item): item is { index: number; option: OptionValue } => item !== null); }).filter((item): item is { index: number; option: OptionValue } => item !== null)
}); })
const selectedOption = computed(() => { const selectedOption = computed(() => {
if (selectedValue.value !== null && selectedValue.value !== undefined) { if (selectedValue.value !== null && selectedValue.value !== undefined) {
return props.displayName(selectedValue.value as OptionValue); return props.displayName(selectedValue.value as OptionValue)
} }
return props.placeholder || "Select an option"; return props.placeholder || 'Select an option'
}); })
const radioValue = computed<OptionValue>({ const radioValue = computed<OptionValue>({
get() { get() {
return props.modelValue ?? selectedValue.value ?? ""; return props.modelValue ?? selectedValue.value ?? ''
}, },
set(newValue: OptionValue) { set(newValue: OptionValue) {
emit("update:modelValue", newValue); emit('update:modelValue', newValue)
selectedValue.value = newValue; selectedValue.value = newValue
}, },
}); })
const triggerClasses = computed(() => ({ const triggerClasses = computed(() => ({
"!cursor-not-allowed opacity-50 grayscale": props.disabled, '!cursor-not-allowed opacity-50 grayscale': props.disabled,
"rounded-b-none": dropdownVisible.value && !isRenderingUp.value && !props.disabled, 'rounded-b-none': dropdownVisible.value && !isRenderingUp.value && !props.disabled,
"rounded-t-none": dropdownVisible.value && isRenderingUp.value && !props.disabled, 'rounded-t-none': dropdownVisible.value && isRenderingUp.value && !props.disabled,
})); }))
const updatePosition = async () => { const updatePosition = async () => {
if (!triggerRef.value) return; if (!triggerRef.value) return
await nextTick(); await nextTick()
const triggerRect = triggerRef.value.getBoundingClientRect(); const triggerRect = triggerRef.value.getBoundingClientRect()
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight
const margin = 8; const margin = 8
const contentHeight = props.options.length * ITEM_HEIGHT; const contentHeight = props.options.length * ITEM_HEIGHT
const preferredHeight = Math.min(contentHeight, 300); const preferredHeight = Math.min(contentHeight, 300)
const spaceBelow = viewportHeight - triggerRect.bottom; const spaceBelow = viewportHeight - triggerRect.bottom
const spaceAbove = triggerRect.top; const spaceAbove = triggerRect.top
isRenderingUp.value = spaceBelow < preferredHeight && spaceAbove > spaceBelow; isRenderingUp.value = spaceBelow < preferredHeight && spaceAbove > spaceBelow
virtualListHeight.value = isRenderingUp.value virtualListHeight.value = isRenderingUp.value
? Math.min(spaceAbove - margin, preferredHeight) ? Math.min(spaceAbove - margin, preferredHeight)
: Math.min(spaceBelow - margin, preferredHeight); : Math.min(spaceBelow - margin, preferredHeight)
positionStyle.value = { positionStyle.value = {
position: "fixed", position: 'fixed',
left: `${triggerRect.left}px`, left: `${triggerRect.left}px`,
width: `${triggerRect.width}px`, width: `${triggerRect.width}px`,
zIndex: 999, zIndex: 999,
...(isRenderingUp.value ...(isRenderingUp.value
? { bottom: `${viewportHeight - triggerRect.top}px`, top: "auto" } ? { bottom: `${viewportHeight - triggerRect.top}px`, top: 'auto' }
: { top: `${triggerRect.bottom}px`, bottom: "auto" }), : { top: `${triggerRect.bottom}px`, bottom: 'auto' }),
}; }
}; }
const toggleDropdown = () => { const toggleDropdown = () => {
if (!props.disabled) { if (!props.disabled) {
if (dropdownVisible.value) { if (dropdownVisible.value) {
closeDropdown(); closeDropdown()
} else { } else {
openDropdown(); openDropdown()
} }
} }
}; }
const handleResize = () => { const handleResize = () => {
if (dropdownVisible.value) { if (dropdownVisible.value) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
updatePosition(); updatePosition()
}); })
} }
}; }
const handleScroll = (event: Event) => { const handleScroll = (event: Event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement
scrollTop.value = target.scrollTop; scrollTop.value = target.scrollTop
}; }
const closeAllDropdowns = () => { const closeAllDropdowns = () => {
const event = new CustomEvent("close-all-dropdowns"); const event = new CustomEvent('close-all-dropdowns')
window.dispatchEvent(event); window.dispatchEvent(event)
}; }
const selectOption = (option: OptionValue, index: number) => { const selectOption = (option: OptionValue, index: number) => {
radioValue.value = option; radioValue.value = option
emit("change", { option, index }); emit('change', { option, index })
closeDropdown(); closeDropdown()
}; }
const focusNextOption = () => { const focusNextOption = () => {
if (focusedOptionIndex.value === null) { if (focusedOptionIndex.value === null) {
focusedOptionIndex.value = 0; focusedOptionIndex.value = 0
} else { } else {
focusedOptionIndex.value = (focusedOptionIndex.value + 1) % props.options.length; focusedOptionIndex.value = (focusedOptionIndex.value + 1) % props.options.length
} }
scrollToFocused(); scrollToFocused()
}; }
const focusPreviousOption = () => { const focusPreviousOption = () => {
if (focusedOptionIndex.value === null) { if (focusedOptionIndex.value === null) {
focusedOptionIndex.value = props.options.length - 1; focusedOptionIndex.value = props.options.length - 1
} else { } else {
focusedOptionIndex.value = focusedOptionIndex.value =
(focusedOptionIndex.value - 1 + props.options.length) % props.options.length; (focusedOptionIndex.value - 1 + props.options.length) % props.options.length
} }
scrollToFocused(); scrollToFocused()
}; }
const scrollToFocused = () => { const scrollToFocused = () => {
if (focusedOptionIndex.value === null) return; if (focusedOptionIndex.value === null) return
const optionsElement = optionsContainer.value?.querySelector(".overflow-y-auto"); const optionsElement = optionsContainer.value?.querySelector('.overflow-y-auto')
if (!optionsElement) return; if (!optionsElement) return
const targetScrollTop = focusedOptionIndex.value * ITEM_HEIGHT; const targetScrollTop = focusedOptionIndex.value * ITEM_HEIGHT
const scrollBottom = optionsElement.clientHeight; const scrollBottom = optionsElement.clientHeight
if (targetScrollTop < optionsElement.scrollTop) { if (targetScrollTop < optionsElement.scrollTop) {
optionsElement.scrollTop = targetScrollTop; optionsElement.scrollTop = targetScrollTop
} else if (targetScrollTop + ITEM_HEIGHT > optionsElement.scrollTop + scrollBottom) { } else if (targetScrollTop + ITEM_HEIGHT > optionsElement.scrollTop + scrollBottom) {
optionsElement.scrollTop = targetScrollTop - scrollBottom + ITEM_HEIGHT; optionsElement.scrollTop = targetScrollTop - scrollBottom + ITEM_HEIGHT
} }
}; }
const openDropdown = async () => { const openDropdown = async () => {
if (!props.disabled) { if (!props.disabled) {
closeAllDropdowns(); closeAllDropdowns()
dropdownVisible.value = true; dropdownVisible.value = true
isOpen.value = true; isOpen.value = true
openDropdownCount.value++; openDropdownCount.value++
document.body.style.overflow = "hidden"; document.body.style.overflow = 'hidden'
await updatePosition(); await updatePosition()
nextTick(() => { nextTick(() => {
optionsContainer.value?.focus(); optionsContainer.value?.focus()
}); })
} }
}; }
const closeDropdown = () => { const closeDropdown = () => {
if (isOpen.value) { if (isOpen.value) {
dropdownVisible.value = false; dropdownVisible.value = false
isOpen.value = false; isOpen.value = false
openDropdownCount.value--; openDropdownCount.value--
if (openDropdownCount.value === 0) { if (openDropdownCount.value === 0) {
document.body.style.overflow = ""; document.body.style.overflow = ''
} }
focusedOptionIndex.value = null; focusedOptionIndex.value = null
triggerRef.value?.focus(); triggerRef.value?.focus()
} }
}; }
const handleTriggerKeyDown = (event: KeyboardEvent) => { const handleTriggerKeyDown = (event: KeyboardEvent) => {
switch (event.key) { switch (event.key) {
case "ArrowDown": case 'ArrowDown':
case "ArrowUp": case 'ArrowUp':
event.preventDefault(); event.preventDefault()
if (!dropdownVisible.value) { if (!dropdownVisible.value) {
openDropdown(); openDropdown()
focusedOptionIndex.value = event.key === "ArrowUp" ? props.options.length - 1 : 0; focusedOptionIndex.value = event.key === 'ArrowUp' ? props.options.length - 1 : 0
} else if (event.key === "ArrowDown") { } else if (event.key === 'ArrowDown') {
focusNextOption(); focusNextOption()
} else { } else {
focusPreviousOption(); focusPreviousOption()
} }
break; break
case "Enter": case 'Enter':
case " ": case ' ':
event.preventDefault(); event.preventDefault()
if (!dropdownVisible.value) { if (!dropdownVisible.value) {
openDropdown(); openDropdown()
focusedOptionIndex.value = 0; focusedOptionIndex.value = 0
} else if (focusedOptionIndex.value !== null) { } else if (focusedOptionIndex.value !== null) {
selectOption(props.options[focusedOptionIndex.value], focusedOptionIndex.value); selectOption(props.options[focusedOptionIndex.value], focusedOptionIndex.value)
} }
break; break
case "Escape": case 'Escape':
event.preventDefault(); event.preventDefault()
closeDropdown(); closeDropdown()
break; break
case "Tab": case 'Tab':
if (dropdownVisible.value) { if (dropdownVisible.value) {
event.preventDefault(); event.preventDefault()
} }
break; break
} }
}; }
const handleListboxKeyDown = (event: KeyboardEvent) => { const handleListboxKeyDown = (event: KeyboardEvent) => {
switch (event.key) { switch (event.key) {
case "Enter": case 'Enter':
case " ": case ' ':
event.preventDefault(); event.preventDefault()
if (focusedOptionIndex.value !== null) { if (focusedOptionIndex.value !== null) {
selectOption(props.options[focusedOptionIndex.value], focusedOptionIndex.value); selectOption(props.options[focusedOptionIndex.value], focusedOptionIndex.value)
} }
break; break
case "ArrowDown": case 'ArrowDown':
event.preventDefault(); event.preventDefault()
focusNextOption(); focusNextOption()
break; break
case "ArrowUp": case 'ArrowUp':
event.preventDefault(); event.preventDefault()
focusPreviousOption(); focusPreviousOption()
break; break
case "Escape": case 'Escape':
event.preventDefault(); event.preventDefault()
closeDropdown(); closeDropdown()
break; break
case "Tab": case 'Tab':
event.preventDefault(); event.preventDefault()
break; break
case "Home": case 'Home':
event.preventDefault(); event.preventDefault()
focusedOptionIndex.value = 0; focusedOptionIndex.value = 0
scrollToFocused(); scrollToFocused()
break; break
case "End": case 'End':
event.preventDefault(); event.preventDefault()
focusedOptionIndex.value = props.options.length - 1; focusedOptionIndex.value = props.options.length - 1
scrollToFocused(); scrollToFocused()
break; break
default: default:
if (event.key.length === 1) { if (event.key.length === 1) {
const char = event.key.toLowerCase(); const char = event.key.toLowerCase()
const index = props.options.findIndex((option) => const index = props.options.findIndex((option) =>
props.displayName(option).toLowerCase().startsWith(char), props.displayName(option).toLowerCase().startsWith(char),
); )
if (index !== -1) { if (index !== -1) {
focusedOptionIndex.value = index; focusedOptionIndex.value = index
scrollToFocused(); scrollToFocused()
} }
} }
break; break
} }
}; }
onMounted(() => { onMounted(() => {
window.addEventListener("resize", handleResize); window.addEventListener('resize', handleResize)
window.addEventListener("scroll", handleResize, true); window.addEventListener('scroll', handleResize, true)
window.addEventListener("click", (event) => { window.addEventListener('click', (event) => {
if (!isChildOfDropdown(event.target as HTMLElement)) { if (!isChildOfDropdown(event.target as HTMLElement)) {
closeDropdown(); closeDropdown()
} }
}); })
window.addEventListener("close-all-dropdowns", closeDropdown); window.addEventListener('close-all-dropdowns', closeDropdown)
if (selectedValue.value) { if (selectedValue.value) {
focusedOptionIndex.value = props.options.findIndex((option) => option === selectedValue.value); focusedOptionIndex.value = props.options.findIndex((option) => option === selectedValue.value)
} }
}); })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("resize", handleResize); window.removeEventListener('resize', handleResize)
window.removeEventListener("scroll", handleResize, true); window.removeEventListener('scroll', handleResize, true)
window.removeEventListener("click", (event) => { window.removeEventListener('click', (event) => {
if (!isChildOfDropdown(event.target as HTMLElement)) { if (!isChildOfDropdown(event.target as HTMLElement)) {
closeDropdown(); closeDropdown()
} }
}); })
window.removeEventListener("close-all-dropdowns", closeDropdown); window.removeEventListener('close-all-dropdowns', closeDropdown)
if (isOpen.value) { if (isOpen.value) {
openDropdownCount.value--; openDropdownCount.value--
if (openDropdownCount.value === 0) { if (openDropdownCount.value === 0) {
document.body.style.overflow = ""; document.body.style.overflow = ''
} }
} }
}); })
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue) => { (newValue) => {
selectedValue.value = newValue; selectedValue.value = newValue
}, },
); )
watch(dropdownVisible, async (newValue) => { watch(dropdownVisible, async (newValue) => {
if (newValue) { if (newValue) {
await updatePosition(); await updatePosition()
scrollTop.value = 0; scrollTop.value = 0
} }
}); })
const activeDescendant = computed(() => const activeDescendant = computed(() =>
focusedOptionIndex.value !== null ? `${listboxId}-option-${focusedOptionIndex.value}` : undefined, focusedOptionIndex.value !== null ? `${listboxId}-option-${focusedOptionIndex.value}` : undefined,
); )
const isChildOfDropdown = (element: HTMLElement | null): boolean => { const isChildOfDropdown = (element: HTMLElement | null): boolean => {
let currentNode: HTMLElement | null = element; let currentNode: HTMLElement | null = element
while (currentNode) { while (currentNode) {
if (currentNode === triggerRef.value || currentNode === optionsContainer.value) { if (currentNode === triggerRef.value || currentNode === optionsContainer.value) {
return true; return true
} }
currentNode = currentNode.parentElement; currentNode = currentNode.parentElement
} }
return false; return false
}; }
</script> </script>

View File

@ -102,8 +102,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from "vue";
import { onClickOutside, useElementHover } from "@vueuse/core"; import { onClickOutside, useElementHover } from "@vueuse/core";
import { computed,nextTick, onMounted, onUnmounted, ref, watch } from "vue";
interface Option { interface Option {
id: string; id: string;

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, ServersSpecs } from "@modrinth/ui"; import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
import type { MessageDescriptor } from "@vintl/vintl";
import { formatPrice } from "@modrinth/utils"; import { formatPrice } from "@modrinth/utils";
import type { MessageDescriptor } from "@vintl/vintl";
const { formatMessage, locale } = useVIntl(); const { formatMessage, locale } = useVIntl();

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
import { PlusIcon, XIcon } from "@modrinth/assets"; import { PlusIcon, XIcon } from "@modrinth/assets";
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils"; import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { ref } from "vue"; import { ref } from "vue";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
const app = useNuxtApp() as unknown as { $notify: any }; const app = useNuxtApp() as unknown as { $notify: any };

View File

@ -1,17 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from "dayjs"; import { EditIcon, SettingsIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui"; import {
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets"; ButtonStyled,
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils"; commonMessages,
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui"; CopyCode,
import { useVIntl } from "@vintl/vintl"; getDismissableMetadata,
NOTICE_LEVELS,
ServerNotice,
TagItem,
useRelativeTime,
} from '@modrinth/ui'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime()
const props = defineProps<{ defineProps<{
notice: ServerNoticeType; notice: ServerNoticeType
}>(); }>()
</script> </script>
<template> <template>
<div class="col-span-full grid grid-cols-subgrid gap-4 rounded-2xl bg-bg-raised p-4"> <div class="col-span-full grid grid-cols-subgrid gap-4 rounded-2xl bg-bg-raised p-4">
@ -21,7 +29,7 @@ const props = defineProps<{
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span v-if="notice.announce_at"> <span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{ {{ dayjs(notice.announce_at).format('MMM D, YYYY [at] h:mm A') }} ({{
formatRelativeTime(notice.announce_at) formatRelativeTime(notice.announce_at)
}}) }})
</span> </span>
@ -91,16 +99,16 @@ const props = defineProps<{
> >
<span v-else-if="!notice.assigned.some((n) => n.kind === 'server')"> <span v-else-if="!notice.assigned.some((n) => n.kind === 'server')">
Assigned to Assigned to
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes {{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span> </span>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'node')"> <span v-else-if="!notice.assigned.some((n) => n.kind === 'node')">
Assigned to Assigned to
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers {{ notice.assigned.filter((n) => n.kind === 'server').length }} servers
</span> </span>
<span v-else> <span v-else>
Assigned to Assigned to
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers and {{ notice.assigned.filter((n) => n.kind === 'server').length }} servers and
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes {{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span> </span>
<button <button

View File

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

View File

@ -110,55 +110,57 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CopyCode, MarkdownEditor, ButtonStyled } from "@modrinth/ui"; import { CheckCircleIcon, ReplyIcon, ScaleIcon, SendIcon } from '@modrinth/assets'
import { ReplyIcon, SendIcon, CheckCircleIcon, ScaleIcon } from "@modrinth/assets"; import { ButtonStyled, CopyCode, MarkdownEditor } from '@modrinth/ui'
import type { Thread, Report, User, ThreadMessage as TypeThreadMessage } from "@modrinth/utils"; import type { Report, Thread, ThreadMessage as TypeThreadMessage, User } from '@modrinth/utils'
import dayjs from "dayjs"; import dayjs from 'dayjs'
import ThreadMessage from "./ThreadMessage.vue";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from '~/composables/image-upload.ts'
import { isStaff } from "~/helpers/users.js"; import { isStaff } from '~/helpers/users.js'
import ThreadMessage from './ThreadMessage.vue'
const props = defineProps<{ const props = defineProps<{
thread: Thread; thread: Thread
reporter: User; reporter: User
report: Report; report: Report
}>(); }>()
const auth = await useAuth(); defineExpose({
setReplyContent,
})
const auth = await useAuth()
const emit = defineEmits<{ const emit = defineEmits<{
updateThread: [thread: Thread]; updateThread: [thread: Thread]
}>(); }>()
const flags = useFeatureFlags(); const flags = useFeatureFlags()
const members = computed(() => { const members = computed(() => {
const membersMap: Record<string, User> = { const membersMap: Record<string, User> = {
[props.reporter.id]: props.reporter, [props.reporter.id]: props.reporter,
};
for (const member of props.thread.members) {
membersMap[member.id] = member;
} }
return membersMap; for (const member of props.thread.members) {
}); membersMap[member.id] = member
}
return membersMap
})
const replyBody = ref(""); const replyBody = ref('')
function setReplyContent(content: string) { function setReplyContent(content: string) {
replyBody.value = content; replyBody.value = content
} }
defineExpose({
setReplyContent,
});
const sortedMessages = computed(() => { const sortedMessages = computed(() => {
const messages: TypeThreadMessage[] = [ const messages: TypeThreadMessage[] = [
{ {
id: null, id: null,
author_id: props.reporter.id, author_id: props.reporter.id,
body: { body: {
type: "text", type: 'text',
body: props.report.body || "Report opened.", body: props.report.body || 'Report opened.',
private: false, private: false,
replying_to: null, replying_to: null,
associated_images: [], associated_images: [],
@ -166,117 +168,117 @@ const sortedMessages = computed(() => {
created: props.report.created, created: props.report.created,
hide_identity: false, hide_identity: false,
}, },
]; ]
if (props.thread) { if (props.thread) {
messages.push( messages.push(
...[...props.thread.messages].sort( ...[...props.thread.messages].sort(
(a, b) => dayjs(a.created).toDate().getTime() - dayjs(b.created).toDate().getTime(), (a, b) => dayjs(a.created).toDate().getTime() - dayjs(b.created).toDate().getTime(),
), ),
); )
} }
return messages; return messages
}); })
async function updateThreadLocal() { async function updateThreadLocal() {
const threadId = props.report.thread_id; const threadId = props.report.thread_id
if (threadId) { if (threadId) {
try { try {
const thread = (await useBaseFetch(`thread/${threadId}`)) as Thread; const thread = (await useBaseFetch(`thread/${threadId}`)) as Thread
emit("updateThread", thread); emit('updateThread', thread)
} catch (error) { } catch (error) {
console.error("Failed to update thread:", error); console.error('Failed to update thread:', error)
} }
} }
} }
const imageIDs = ref<string[]>([]); const imageIDs = ref<string[]>([])
async function onUploadImage(file: File) { async function onUploadImage(file: 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)
imageIDs.value = imageIDs.value.slice(-10); imageIDs.value = imageIDs.value.slice(-10)
return response.url; return response.url
} }
async function sendReply(privateMessage = false) { async function sendReply(privateMessage = false) {
try { try {
const body: any = { const body: any = {
body: { body: {
type: "text", type: 'text',
body: replyBody.value, body: replyBody.value,
private: privateMessage, private: privateMessage,
}, },
}; }
if (imageIDs.value.length > 0) { if (imageIDs.value.length > 0) {
body.body = { body.body = {
...body.body, ...body.body,
uploaded_images: imageIDs.value, uploaded_images: imageIDs.value,
}; }
} }
await useBaseFetch(`thread/${props.thread.id}`, { await useBaseFetch(`thread/${props.thread.id}`, {
method: "POST", method: 'POST',
body, body,
}); })
replyBody.value = ""; replyBody.value = ''
await updateThreadLocal(); await updateThreadLocal()
} catch (err: any) { } catch (err: any) {
addNotification({ addNotification({
title: "Error sending message", title: 'Error sending message',
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: 'error',
}); })
} }
} }
const didCloseReport = ref(false); const didCloseReport = ref(false)
const reportClosed = computed(() => { const reportClosed = computed(() => {
return didCloseReport.value || (props.report && props.report.closed); return didCloseReport.value || (props.report && props.report.closed)
}); })
async function closeReport(reply = false) { async function closeReport(reply = false) {
if (reply) { if (reply) {
await sendReply(); await sendReply()
} }
try { try {
await useBaseFetch(`report/${props.report.id}`, { await useBaseFetch(`report/${props.report.id}`, {
method: "PATCH", method: 'PATCH',
body: { body: {
closed: true, closed: true,
}, },
}); })
await updateThreadLocal(); await updateThreadLocal()
didCloseReport.value = true; didCloseReport.value = true
} catch (err: any) { } catch (err: any) {
addNotification({ addNotification({
title: "Error closing report", title: 'Error closing report',
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: 'error',
}); })
} }
} }
async function reopenReport() { async function reopenReport() {
try { try {
await useBaseFetch(`report/${props.report.id}`, { await useBaseFetch(`report/${props.report.id}`, {
method: "PATCH", method: 'PATCH',
body: { body: {
closed: false, closed: false,
}, },
}); })
await updateThreadLocal(); await updateThreadLocal()
} catch (err: any) { } catch (err: any) {
addNotification({ addNotification({
title: "Error reopening report", title: 'Error reopening report',
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: 'error',
}); })
} }
} }
</script> </script>

View File

@ -96,15 +96,16 @@
<script setup> <script setup>
import { import {
MoreHorizontalIcon,
TrashIcon,
MicrophoneIcon,
LockIcon, LockIcon,
MicrophoneIcon,
ModrinthIcon, ModrinthIcon,
MoreHorizontalIcon,
ScaleIcon, ScaleIcon,
TrashIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui"; import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
import { renderString } from "@modrinth/utils"; import { renderString } from "@modrinth/utils";
import { isStaff } from "~/helpers/users.js"; import { isStaff } from "~/helpers/users.js";
const props = defineProps({ const props = defineProps({

View File

@ -25,6 +25,7 @@
<script setup> <script setup>
import { ChevronRightIcon } from "@modrinth/assets"; import { ChevronRightIcon } from "@modrinth/assets";
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue"; import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
const props = defineProps({ const props = defineProps({

View File

@ -1,4 +1,4 @@
import { useState, useRequestHeaders } from "#imports"; import { useRequestHeaders,useState } from "#imports";
export const useUserCountry = () => { export const useUserCountry = () => {
const country = useState<string>("userCountry", () => "US"); const country = useState<string>("userCountry", () => "US");

View File

@ -38,7 +38,7 @@ export function createDisplayNames(
of(tag: string) { of(tag: string) {
let attempt = 0; let attempt = 0;
// eslint-disable-next-line no-labels
lookupLoop: do { lookupLoop: do {
let lookup: string; let lookup: string;
switch (attempt) { switch (attempt) {
@ -49,7 +49,7 @@ export function createDisplayNames(
lookup = safeTagFor(tag); lookup = safeTagFor(tag);
break; break;
default: default:
// eslint-disable-next-line no-labels
break lookupLoop; break lookupLoop;
} }

View File

@ -1,16 +1,16 @@
import { ModrinthServerError } from "@modrinth/utils";
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils"; import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { useServersFetch } from "./servers-fetch.ts"; import { ModrinthServerError } from "@modrinth/utils";
import { import {
GeneralModule,
ContentModule,
BackupsModule, BackupsModule,
ContentModule,
FSModule,
GeneralModule,
NetworkModule, NetworkModule,
StartupModule, StartupModule,
WSModule, WSModule,
FSModule,
} from "./modules/index.ts"; } from "./modules/index.ts";
import { useServersFetch } from "./servers-fetch.ts";
export function handleError(err: any) { export function handleError(err: any) {
if (err instanceof ModrinthServerError && err.v1Error) { if (err instanceof ModrinthServerError && err.v1Error) {

View File

@ -1,4 +1,5 @@
import type { Backup, AutoBackupSettings } from "@modrinth/utils"; import type { AutoBackupSettings,Backup } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,4 +1,5 @@
import type { Mod, ContentType } from "@modrinth/utils"; import type { ContentType,Mod } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,11 +1,12 @@
import type { import type {
FileUploadQuery,
JWTAuth,
DirectoryResponse, DirectoryResponse,
FilesystemOp, FilesystemOp,
FileUploadQuery,
FSQueuedOp, FSQueuedOp,
JWTAuth,
} from "@modrinth/utils"; } from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils"; import { ModrinthServerError } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,5 +1,6 @@
import type { JWTAuth,PowerAction, Project, ServerGeneral } from "@modrinth/utils";
import { $fetch } from "ofetch"; import { $fetch } from "ofetch";
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,5 +1,5 @@
export * from "./base.ts";
export * from "./backups.ts"; export * from "./backups.ts";
export * from "./base.ts";
export * from "./content.ts"; export * from "./content.ts";
export * from "./fs.ts"; export * from "./fs.ts";
export * from "./general.ts"; export * from "./general.ts";

View File

@ -1,4 +1,5 @@
import type { Allocation } from "@modrinth/utils"; import type { Allocation } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,4 +1,5 @@
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils"; import type { JDKBuild,JDKVersion, Startup } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,4 +1,5 @@
import type { JWTAuth } from "@modrinth/utils"; import type { JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts"; import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts"; import { ServerModule } from "./base.ts";

View File

@ -1,6 +1,6 @@
import { $fetch, FetchError } from "ofetch";
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
import type { V1ErrorInfo } from "@modrinth/utils"; import type { V1ErrorInfo } from "@modrinth/utils";
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
import { $fetch, FetchError } from "ofetch";
export interface ServersFetchOptions { export interface ServersFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

View File

@ -49,8 +49,9 @@
</template> </template>
<script setup> <script setup>
import { defineMessage, useVIntl } from "@vintl/vintl";
import { SadRinthbot } from "@modrinth/assets"; import { SadRinthbot } from "@modrinth/assets";
import { defineMessage, useVIntl } from "@vintl/vintl";
import Logo404 from "~/assets/images/404.svg"; import Logo404 from "~/assets/images/404.svg";
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();

View File

@ -1,19 +1,19 @@
import hljs from "highlight.js/lib/core";
// Scripting
import javascript from "highlight.js/lib/languages/javascript";
import lua from "highlight.js/lib/languages/lua";
import python from "highlight.js/lib/languages/python";
// Coding
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";
// Configs // Configs
import { configuredXss, md } from "@modrinth/utils"; import { configuredXss, md } from "@modrinth/utils";
import hljs from "highlight.js/lib/core";
import gradle from "highlight.js/lib/languages/gradle"; import gradle from "highlight.js/lib/languages/gradle";
// Coding
import groovy from "highlight.js/lib/languages/groovy";
import ini from "highlight.js/lib/languages/ini"; import ini from "highlight.js/lib/languages/ini";
import java from "highlight.js/lib/languages/java";
// Scripting
import javascript from "highlight.js/lib/languages/javascript";
import json from "highlight.js/lib/languages/json"; import json from "highlight.js/lib/languages/json";
import kotlin from "highlight.js/lib/languages/kotlin";
import lua from "highlight.js/lib/languages/lua";
import properties from "highlight.js/lib/languages/properties"; import properties from "highlight.js/lib/languages/properties";
import python from "highlight.js/lib/languages/python";
import scala from "highlight.js/lib/languages/scala";
import xml from "highlight.js/lib/languages/xml"; import xml from "highlight.js/lib/languages/xml";
import yaml from "highlight.js/lib/languages/yaml"; import yaml from "highlight.js/lib/languages/yaml";

View File

@ -1,6 +1,6 @@
import { parse as parseTOML } from "@ltd/j-toml"; import { parse as parseTOML } from "@ltd/j-toml";
import JSZip from "jszip";
import yaml from "js-yaml"; import yaml from "js-yaml";
import JSZip from "jszip";
import { satisfies } from "semver"; import { satisfies } from "semver";
export const inferVersionInfo = async function (rawFile, project, gameVersions) { export const inferVersionInfo = async function (rawFile, project, gameVersions) {
@ -139,7 +139,7 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
// ${file.jarVersion} -> Implementation-Version from manifest // ${file.jarVersion} -> Implementation-Version from manifest
const manifestFile = zip.file("META-INF/MANIFEST.MF"); const manifestFile = zip.file("META-INF/MANIFEST.MF");
if ( if (
// eslint-disable-next-line no-template-curly-in-string
metadata.mods[0].version.includes("${file.jarVersion}") && metadata.mods[0].version.includes("${file.jarVersion}") &&
manifestFile !== null manifestFile !== null
) { ) {
@ -147,7 +147,7 @@ export const inferVersionInfo = async function (rawFile, project, gameVersions)
const regex = /Implementation-Version: (.*)$/m; const regex = /Implementation-Version: (.*)$/m;
const match = manifestText.match(regex); const match = manifestText.match(regex);
if (match) { if (match) {
// eslint-disable-next-line no-template-curly-in-string
versionNum = versionNum.replace("${file.jarVersion}", match[1]); versionNum = versionNum.replace("${file.jarVersion}", match[1]);
} }
} }

View File

@ -1,12 +1,12 @@
import type { ExtendedReport, OwnershipTarget } from "@modrinth/moderation"; import type { ExtendedReport, OwnershipTarget } from "@modrinth/moderation";
import type { import type {
Thread,
Version,
User,
Project,
TeamMember,
Organization, Organization,
Project,
Report, Report,
TeamMember,
Thread,
User,
Version,
} from "@modrinth/utils"; } from "@modrinth/utils";
export const useModerationCache = () => ({ export const useModerationCache = () => ({

View File

@ -1,5 +1,5 @@
import JSZip from "jszip";
import TOML from "@ltd/j-toml"; import TOML from "@ltd/j-toml";
import JSZip from "jszip";
export const createDataPackVersion = async function ( export const createDataPackVersion = async function (
project, project,
@ -141,12 +141,12 @@ export const createDataPackVersion = async function (
primaryZipReader.file("quilt.mod.json", JSON.stringify(quiltModJson)); primaryZipReader.file("quilt.mod.json", JSON.stringify(quiltModJson));
} }
if (loaders.includes("forge")) { if (loaders.includes("forge")) {
primaryZipReader.file("META-INF/mods.toml", TOML.stringify(forgeModsToml, { newline: "\n" })); // eslint-disable-line import/no-named-as-default-member primaryZipReader.file("META-INF/mods.toml", TOML.stringify(forgeModsToml, { newline: "\n" }));
} }
if (loaders.includes("neoforge")) { if (loaders.includes("neoforge")) {
primaryZipReader.file( primaryZipReader.file(
"META-INF/neoforge.mods.toml", "META-INF/neoforge.mods.toml",
TOML.stringify(neoModsToml, { newline: "\n" }), // eslint-disable-line import/no-named-as-default-member TOML.stringify(neoModsToml, { newline: "\n" }),
); );
} }

View File

@ -666,61 +666,61 @@
</template> </template>
<script setup> <script setup>
import { import {
ModrinthIcon,
ArrowBigUpDashIcon, ArrowBigUpDashIcon,
BookmarkIcon,
ServerIcon,
LogInIcon,
DownloadIcon,
LibraryIcon,
XIcon,
IssuesIcon,
ReportIcon,
CompassIcon,
HamburgerIcon,
SearchIcon,
BellIcon, BellIcon,
SettingsIcon, BlueskyIcon,
BookmarkIcon,
BoxIcon,
BracesIcon,
ChartIcon,
CollectionIcon,
CompassIcon,
CurrencyIcon,
DiscordIcon,
DownloadIcon,
DropdownIcon,
GithubIcon,
GlassesIcon,
HamburgerIcon,
HomeIcon, HomeIcon,
IssuesIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
MastodonIcon,
ModrinthIcon,
MoonIcon, MoonIcon,
SunIcon, OrganizationIcon,
PackageOpenIcon,
PaintbrushIcon,
PlugIcon, PlugIcon,
PlusIcon, PlusIcon,
DropdownIcon, ReportIcon,
LogOutIcon,
ChartIcon,
BoxIcon,
CollectionIcon,
OrganizationIcon,
UserIcon,
CurrencyIcon,
BracesIcon,
GlassesIcon,
PaintbrushIcon,
PackageOpenIcon,
DiscordIcon,
BlueskyIcon,
TwitterIcon,
MastodonIcon,
GithubIcon,
ScaleIcon, ScaleIcon,
SearchIcon,
ServerIcon,
SettingsIcon,
SunIcon,
TwitterIcon,
UserIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar,
Button, Button,
ButtonStyled, ButtonStyled,
commonMessages,
OverflowMenu, OverflowMenu,
PagewideBanner, PagewideBanner,
Avatar,
commonMessages,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { isAdmin, isStaff } from "@modrinth/utils"; import { isAdmin, isStaff } from "@modrinth/utils";
import { errors as generatedStateErrors } from "~/generated/state.json";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue"; import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue"; import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue";
import { errors as generatedStateErrors } from "~/generated/state.json";
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();

View File

@ -891,6 +891,7 @@
<script setup> <script setup>
import { import {
AlignLeftIcon as DescriptionIcon,
BookmarkIcon, BookmarkIcon,
BookTextIcon, BookTextIcon,
CalendarIcon, CalendarIcon,
@ -898,14 +899,14 @@ import {
CheckIcon, CheckIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
CopyrightIcon, CopyrightIcon,
AlignLeftIcon as DescriptionIcon,
DownloadIcon, DownloadIcon,
ExternalIcon, ExternalIcon,
ImageIcon as GalleryIcon,
GameIcon, GameIcon,
HeartIcon, HeartIcon,
ImageIcon as GalleryIcon,
InfoIcon, InfoIcon,
LinkIcon as LinksIcon, LinkIcon as LinksIcon,
ModrinthIcon,
MoreVerticalIcon, MoreVerticalIcon,
PlusIcon, PlusIcon,
ReportIcon, ReportIcon,
@ -917,7 +918,6 @@ import {
UsersIcon, UsersIcon,
VersionIcon, VersionIcon,
WrenchIcon, WrenchIcon,
ModrinthIcon,
XIcon, XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
@ -935,15 +935,16 @@ import {
ProjectSidebarLinks, ProjectSidebarLinks,
ProjectStatusBadge, ProjectStatusBadge,
ScrollablePanel, ScrollablePanel,
TagItem,
ServersPromo, ServersPromo,
TagItem,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue"; import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import { formatCategory, formatProjectType, renderString } from "@modrinth/utils"; import { formatCategory, formatProjectType, renderString } from "@modrinth/utils";
import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Tooltip } from "floating-vue"; import { Tooltip } from "floating-vue";
import { useLocalStorage } from "@vueuse/core";
import { navigateTo } from "#app"; import { navigateTo } from "#app";
import Accordion from "~/components/ui/Accordion.vue"; import Accordion from "~/components/ui/Accordion.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
@ -951,15 +952,15 @@ import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import MessageBanner from "~/components/ui/MessageBanner.vue"; import MessageBanner from "~/components/ui/MessageBanner.vue";
import ModerationChecklist from "~/components/ui/moderation/checklist/ModerationChecklist.vue";
import NavStack from "~/components/ui/NavStack.vue"; import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue"; import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavTabs from "~/components/ui/NavTabs.vue"; import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue"; import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
import { userCollectProject } from "~/composables/user.js";
import { reportProject } from "~/utils/report-helpers.ts";
import { saveFeatureFlags } from "~/composables/featureFlags.ts"; import { saveFeatureFlags } from "~/composables/featureFlags.ts";
import ModerationChecklist from "~/components/ui/moderation/checklist/ModerationChecklist.vue"; import { userCollectProject } from "~/composables/user.js";
import { useModerationStore } from "~/store/moderation.ts"; import { useModerationStore } from "~/store/moderation.ts";
import { reportProject } from "~/utils/report-helpers.ts";
const data = useNuxtApp(); const data = useNuxtApp();
const route = useNativeRoute(); const route = useNativeRoute();

View File

@ -73,10 +73,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Pagination } from "@modrinth/ui";
import { DownloadIcon } from "@modrinth/assets"; import { DownloadIcon } from "@modrinth/assets";
import { Pagination } from "@modrinth/ui";
import VersionFilterControl from "@modrinth/ui/src/components/version/VersionFilterControl.vue"; import VersionFilterControl from "@modrinth/ui/src/components/version/VersionFilterControl.vue";
import { renderHighlightedString } from "~/helpers/highlight.js"; import { renderHighlightedString } from "~/helpers/highlight.js";
const props = defineProps({ const props = defineProps({

View File

@ -278,28 +278,28 @@
<script setup> <script setup>
import { import {
PlusIcon,
CalendarIcon, CalendarIcon,
ContractIcon,
EditIcon, EditIcon,
TrashIcon, ExpandIcon,
ExternalIcon,
ImageIcon,
InfoIcon,
LeftArrowIcon,
PlusIcon,
RightArrowIcon,
SaveIcon, SaveIcon,
StarIcon, StarIcon,
XIcon,
RightArrowIcon,
LeftArrowIcon,
ExternalIcon,
ExpandIcon,
ContractIcon,
UploadIcon,
InfoIcon,
ImageIcon,
TransferIcon, TransferIcon,
TrashIcon,
UploadIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ConfirmModal } from "@modrinth/ui"; 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 DropArea from "~/components/ui/DropArea.vue";
import FileInput from "~/components/ui/FileInput.vue";
import Modal from "~/components/ui/Modal.vue";
import { isPermission } from "~/utils/permissions.ts"; import { isPermission } from "~/utils/permissions.ts";
const props = defineProps({ const props = defineProps({

View File

@ -99,8 +99,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { XIcon, CheckIcon, IssuesIcon } from "@modrinth/assets"; import { CheckIcon, IssuesIcon,XIcon } from "@modrinth/assets";
import { Badge } from "@modrinth/ui"; import { Badge } from "@modrinth/ui";
import ConversationThread from "~/components/ui/thread/ConversationThread.vue"; import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import { import {
getProjectLink, getProjectLink,

View File

@ -42,6 +42,7 @@ import { SaveIcon } from "@modrinth/assets";
import { MarkdownEditor } from "@modrinth/ui"; import { MarkdownEditor } from "@modrinth/ui";
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils"; import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from "~/composables/image-upload.ts";
const props = defineProps<{ const props = defineProps<{

View File

@ -239,10 +239,11 @@
</template> </template>
<script setup> <script setup>
import { CheckIcon,IssuesIcon, SaveIcon, TrashIcon, UploadIcon, XIcon } from "@modrinth/assets";
import { Avatar,ConfirmModal } from "@modrinth/ui";
import { formatProjectStatus, formatProjectType } from "@modrinth/utils"; import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { ConfirmModal, Avatar } from "@modrinth/ui";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
const props = defineProps({ const props = defineProps({

View File

@ -37,7 +37,7 @@
</div> </div>
</div> </div>
<div class="adjacent-input" v-if="license.requiresOnlyOrLater"> <div v-if="license.requiresOnlyOrLater" class="adjacent-input">
<label for="or-later-checkbox"> <label for="or-later-checkbox">
<span class="label__title">Later editions</span> <span class="label__title">Later editions</span>
<span class="label__description"> <span class="label__description">
@ -60,11 +60,11 @@
<div class="adjacent-input"> <div class="adjacent-input">
<label for="license-url"> <label for="license-url">
<span class="label__title">License URL</span> <span class="label__title">License URL</span>
<span class="label__description" v-if="license?.friendly !== 'Custom'"> <span v-if="license?.friendly !== 'Custom'" class="label__description">
The web location of the full license text. If you don't provide a link, the license text The web location of the full license text. If you don't provide a link, the license text
will be displayed instead. will be displayed instead.
</span> </span>
<span class="label__description" v-else> <span v-else class="label__description">
The web location of the full license text. You have to provide a link since this is a The web location of the full license text. You have to provide a link since this is a
custom license. custom license.
</span> </span>
@ -83,8 +83,8 @@
</div> </div>
</div> </div>
<div class="adjacent-input" v-if="license?.friendly === 'Custom'"> <div v-if="license?.friendly === 'Custom'" class="adjacent-input">
<label for="license-spdx" v-if="!nonSpdxLicense"> <label v-if="!nonSpdxLicense" for="license-spdx">
<span class="label__title">SPDX identifier</span> <span class="label__title">SPDX identifier</span>
<span class="label__description"> <span class="label__description">
If your license does not have an offical If your license does not have an offical
@ -93,7 +93,7 @@
>, check the box and enter the name of the license instead. >, check the box and enter the name of the license instead.
</span> </span>
</label> </label>
<label for="license-name" v-else> <label v-else for="license-name">
<span class="label__title">License name</span> <span class="label__title">License name</span>
<span class="label__description" <span class="label__description"
>The full name of the license. If the license has a SPDX identifier, please uncheck the >The full name of the license. If the license has a SPDX identifier, please uncheck the
@ -104,8 +104,8 @@
<div class="input-stack w-1/2"> <div class="input-stack w-1/2">
<input <input
v-if="!nonSpdxLicense" v-if="!nonSpdxLicense"
v-model="license.short"
id="license-spdx" id="license-spdx"
v-model="license.short"
class="w-full" class="w-full"
type="text" type="text"
maxlength="128" maxlength="128"
@ -114,8 +114,8 @@
/> />
<input <input
v-else v-else
v-model="license.short"
id="license-name" id="license-name"
v-model="license.short"
class="w-full" class="w-full"
type="text" type="text"
maxlength="128" maxlength="128"
@ -154,22 +154,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Checkbox, DropdownSelect } from "@modrinth/ui";
import { SaveIcon } from "@modrinth/assets"; import { SaveIcon } from "@modrinth/assets";
import { Checkbox, DropdownSelect } from "@modrinth/ui";
import { import {
TeamMemberPermission, type BuiltinLicense,
builtinLicenses, builtinLicenses,
formatProjectType, formatProjectType,
type BuiltinLicense,
type Project, type Project,
type TeamMember, type TeamMember,
TeamMemberPermission,
} from "@modrinth/utils"; } from "@modrinth/utils";
import { computed, ref, type Ref } from "vue"; import { computed, type Ref,ref } from "vue";
const props = defineProps<{ const props = defineProps<{
project: Project; project: Project;
currentMember: TeamMember | undefined; currentMember: TeamMember | undefined;
patchProject: (payload: Object, quiet?: boolean) => Object; patchProject: (payload: object, quiet?: boolean) => object;
}>(); }>();
const licenseUrl = ref(props.project.license.url); const licenseUrl = ref(props.project.license.url);
@ -215,7 +215,7 @@ const licenseId = computed(() => {
let id = ""; let id = "";
if ( if (
(nonSpdxLicense && license.value.friendly === "Custom") || (nonSpdxLicense.value && license.value.friendly === "Custom") ||
license.value.short === "All-Rights-Reserved" || license.value.short === "All-Rights-Reserved" ||
license.value.short === "Unknown" license.value.short === "Unknown"
) { ) {
@ -227,7 +227,7 @@ const licenseId = computed(() => {
id += allowOrLater.value ? "-or-later" : "-only"; id += allowOrLater.value ? "-or-later" : "-only";
} }
if (nonSpdxLicense && license.value.friendly === "Custom") { if (nonSpdxLicense.value && license.value.friendly === "Custom") {
id = id.replaceAll(" ", "-"); id = id.replaceAll(" ", "-");
} }

View File

@ -122,8 +122,8 @@
</template> </template>
<script setup> <script setup>
import { DropdownSelect } from "@modrinth/ui";
import { SaveIcon } from "@modrinth/assets"; import { SaveIcon } from "@modrinth/assets";
import { DropdownSelect } from "@modrinth/ui";
const tags = useTags(); const tags = useTags();

View File

@ -518,19 +518,20 @@
</template> </template>
<script setup> <script setup>
import { Multiselect } from "vue-multiselect";
import { import {
TransferIcon,
CheckIcon, CheckIcon,
UsersIcon,
DropdownIcon,
SaveIcon,
UserPlusIcon,
UserXIcon,
OrganizationIcon,
CrownIcon, CrownIcon,
DropdownIcon,
OrganizationIcon,
SaveIcon,
TransferIcon,
UserPlusIcon,
UsersIcon,
UserXIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Avatar, Badge, Card, Checkbox, ConfirmModal } from "@modrinth/ui"; import { Avatar, Badge, Card, Checkbox, ConfirmModal } from "@modrinth/ui";
import { Multiselect } from "vue-multiselect";
import { removeSelfFromTeam } from "~/helpers/teams.js"; import { removeSelfFromTeam } from "~/helpers/teams.js";
const props = defineProps({ const props = defineProps({

View File

@ -113,8 +113,9 @@
</template> </template>
<script> <script>
import { StarIcon, SaveIcon } from "@modrinth/assets"; import { SaveIcon,StarIcon } from "@modrinth/assets";
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils"; import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
import Checkbox from "~/components/ui/Checkbox.vue"; import Checkbox from "~/components/ui/Checkbox.vue";
export default defineNuxtComponent({ export default defineNuxtComponent({

View File

@ -631,45 +631,45 @@
</template> </template>
<script> <script>
import { import {
Avatar, BoxIcon,
Badge, ChevronRightIcon,
CopyCode,
Checkbox,
ButtonStyled,
ConfirmModal,
MarkdownEditor,
} from "@modrinth/ui";
import {
FileIcon,
TrashIcon,
EditIcon,
DownloadIcon, DownloadIcon,
StarIcon, EditIcon,
ReportIcon, FileIcon,
SaveIcon,
XIcon,
HashIcon, HashIcon,
PlusIcon, PlusIcon,
TransferIcon, ReportIcon,
UploadIcon,
BoxIcon,
RightArrowIcon, RightArrowIcon,
ChevronRightIcon, SaveIcon,
StarIcon,
TransferIcon,
TrashIcon,
UploadIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import {
Avatar,
Badge,
ButtonStyled,
Checkbox,
ConfirmModal,
CopyCode,
MarkdownEditor,
} from "@modrinth/ui";
import { formatBytes, formatCategory } from "@modrinth/utils"; import { formatBytes, formatCategory } from "@modrinth/utils";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js"; import { Multiselect } from "vue-multiselect";
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 AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import Categories from "~/components/ui/search/Categories.vue";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
import Categories from "~/components/ui/search/Categories.vue";
import { useImageUpload } from "~/composables/image-upload.ts";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { inferVersionInfo } from "~/helpers/infer.js";
import { createDataPackVersion } from "~/helpers/package.js";
import { reportVersion } from "~/utils/report-helpers.ts";
export default defineNuxtComponent({ export default defineNuxtComponent({
components: { components: {

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