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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -110,7 +110,7 @@
<script setup>
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";
const modalOpen = ref(null);

View File

@ -90,10 +90,11 @@
</template>
<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 Categories from "~/components/ui/search/Categories.vue";
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import Categories from "~/components/ui/search/Categories.vue";
export default {
components: {

View File

@ -109,16 +109,17 @@
<script setup>
import {
ChevronRightIcon,
CheckIcon,
XIcon,
AsteriskIcon,
LightBulbIcon,
SendIcon,
ScaleIcon,
CheckIcon,
ChevronRightIcon,
DropdownIcon,
LightBulbIcon,
ScaleIcon,
SendIcon,
XIcon,
} from "@modrinth/assets";
import { formatProjectType } from "@modrinth/utils";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
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",
id: "feature-gallery-image",
description: "Featured gallery images may be the first impression of many users.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,9 +29,9 @@
</template>
<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 { keybinds, type KeybindListener, normalizeKeybind } from "@modrinth/moderation";
import { ref } from "vue";
const modal = ref<InstanceType<typeof NewModal>>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,11 +42,12 @@
</template>
<script setup lang="ts">
import { ref, nextTick, computed } from "vue";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
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<{
server: ModrinthServer;

View File

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

View File

@ -1,23 +1,23 @@
<script setup lang="ts">
import dayjs from "dayjs";
import {
MoreVerticalIcon,
HistoryIcon,
DownloadIcon,
SpinnerIcon,
EditIcon,
LockIcon,
TrashIcon,
FolderArchiveIcon,
BotIcon,
XIcon,
DownloadIcon,
EditIcon,
FolderArchiveIcon,
HistoryIcon,
LockIcon,
LockOpenIcon,
MoreVerticalIcon,
RotateCounterClockwiseIcon,
SpinnerIcon,
TrashIcon,
XIcon,
} from "@modrinth/assets";
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 { defineMessages, useVIntl } from "@vintl/vintl";
import dayjs from "dayjs";
import { computed,ref } from "vue";
const flags = useFeatureFlags();
const { formatMessage } = useVIntl();

View File

@ -45,11 +45,12 @@
</template>
<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 { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
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<{
server: ModrinthServer;

View File

@ -17,11 +17,13 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ConfirmModal, NewModal } from "@modrinth/ui";
import type { NewModal } from "@modrinth/ui";
import { ConfirmModal } from "@modrinth/ui";
import type { Backup } from "@modrinth/utils";
import { ref } from "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<{
server: ModrinthServer;

View File

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

View File

@ -229,17 +229,18 @@
<script setup lang="ts">
import {
DropdownIcon,
XIcon,
CheckIcon,
LockOpenIcon,
GameIcon,
DropdownIcon,
ExternalIcon,
GameIcon,
LockOpenIcon,
XIcon,
} from "@modrinth/assets";
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui";
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 { computed,ref } from "vue";
import Accordion from "~/components/ui/Accordion.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import ContentVersionFilter, {

View File

@ -58,11 +58,11 @@
<script setup lang="ts">
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 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 = {
name: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,11 +73,13 @@
</template>
<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 { ModrinthServersFetchError } from "@modrinth/utils";
import { ref, computed, nextTick } from "vue";
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import { computed, nextTick,ref } from "vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import { handleError } from "~/composables/servers/modrinth-servers.ts";
const cf = ref(false);

View File

@ -44,7 +44,7 @@
<script setup>
import * as THREE from "three";
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 showLabels = ref(false);

View File

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

View File

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

View File

@ -104,23 +104,23 @@
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import {
PlayIcon,
UpdatedIcon,
StopCircleIcon,
SlashIcon,
XIcon,
CheckIcon,
ServerIcon,
ClipboardCopyIcon,
InfoIcon,
MoreVerticalIcon,
ClipboardCopyIcon,
PlayIcon,
ServerIcon,
SlashIcon,
StopCircleIcon,
UpdatedIcon,
XIcon,
} from "@modrinth/assets";
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 { useStorage } from "@vueuse/core";
import { computed,ref } from "vue";
import { useRouter } from "vue-router";
const flags = useFeatureFlags();

View File

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

View File

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

View File

@ -67,10 +67,11 @@
</template>
<script setup lang="ts">
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { DownloadIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
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<{
server: ModrinthServer;

View File

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

View File

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

View File

@ -31,7 +31,8 @@
<script setup lang="ts">
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<{
isUpdating: boolean;

View File

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

View File

@ -102,9 +102,10 @@
<script setup lang="ts">
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 type { Project, Server } from "@modrinth/utils";
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<Partial<Server>>();

View File

@ -37,8 +37,9 @@
<script setup lang="ts">
import { RightArrowIcon } from "@modrinth/assets";
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 { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const emit = defineEmits(["reinstall"]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,25 @@
<script setup lang="ts">
import dayjs from "dayjs";
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui";
import { useVIntl } from "@vintl/vintl";
import { EditIcon, SettingsIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
CopyCode,
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 formatRelativeTime = useRelativeTime();
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const props = defineProps<{
notice: ServerNoticeType;
}>();
defineProps<{
notice: ServerNoticeType
}>()
</script>
<template>
<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 class="text-sm">
<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)
}})
</span>
@ -91,16 +99,16 @@ const props = defineProps<{
>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'server')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes
{{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span>
<span v-else-if="!notice.assigned.some((n) => n.kind === 'node')">
Assigned to
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers
{{ notice.assigned.filter((n) => n.kind === 'server').length }} servers
</span>
<span v-else>
Assigned to
{{ 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 === 'server').length }} servers and
{{ notice.assigned.filter((n) => n.kind === 'node').length }} nodes
</span>
<button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
import { ModrinthServerError } from "@modrinth/utils";
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { useServersFetch } from "./servers-fetch.ts";
import { ModrinthServerError } from "@modrinth/utils";
import {
GeneralModule,
ContentModule,
BackupsModule,
ContentModule,
FSModule,
GeneralModule,
NetworkModule,
StartupModule,
WSModule,
FSModule,
} from "./modules/index.ts";
import { useServersFetch } from "./servers-fetch.ts";
export function handleError(err: any) {
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 { 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 { ServerModule } from "./base.ts";

View File

@ -1,11 +1,12 @@
import type {
FileUploadQuery,
JWTAuth,
DirectoryResponse,
FilesystemOp,
FileUploadQuery,
FSQueuedOp,
JWTAuth,
} from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.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 type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";

View File

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

View File

@ -1,4 +1,5 @@
import type { Allocation } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.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 { ServerModule } from "./base.ts";

View File

@ -1,4 +1,5 @@
import type { JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.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 { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
import { $fetch, FetchError } from "ofetch";
export interface ServersFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

View File

@ -49,8 +49,9 @@
</template>
<script setup>
import { defineMessage, useVIntl } from "@vintl/vintl";
import { SadRinthbot } from "@modrinth/assets";
import { defineMessage, useVIntl } from "@vintl/vintl";
import Logo404 from "~/assets/images/404.svg";
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
import { configuredXss, md } from "@modrinth/utils";
import hljs from "highlight.js/lib/core";
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 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 kotlin from "highlight.js/lib/languages/kotlin";
import lua from "highlight.js/lib/languages/lua";
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 yaml from "highlight.js/lib/languages/yaml";

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import JSZip from "jszip";
import TOML from "@ltd/j-toml";
import JSZip from "jszip";
export const createDataPackVersion = async function (
project,
@ -141,12 +141,12 @@ export const createDataPackVersion = async function (
primaryZipReader.file("quilt.mod.json", JSON.stringify(quiltModJson));
}
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")) {
primaryZipReader.file(
"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>
<script setup>
import {
ModrinthIcon,
ArrowBigUpDashIcon,
BookmarkIcon,
ServerIcon,
LogInIcon,
DownloadIcon,
LibraryIcon,
XIcon,
IssuesIcon,
ReportIcon,
CompassIcon,
HamburgerIcon,
SearchIcon,
BellIcon,
SettingsIcon,
BlueskyIcon,
BookmarkIcon,
BoxIcon,
BracesIcon,
ChartIcon,
CollectionIcon,
CompassIcon,
CurrencyIcon,
DiscordIcon,
DownloadIcon,
DropdownIcon,
GithubIcon,
GlassesIcon,
HamburgerIcon,
HomeIcon,
IssuesIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
MastodonIcon,
ModrinthIcon,
MoonIcon,
SunIcon,
OrganizationIcon,
PackageOpenIcon,
PaintbrushIcon,
PlugIcon,
PlusIcon,
DropdownIcon,
LogOutIcon,
ChartIcon,
BoxIcon,
CollectionIcon,
OrganizationIcon,
UserIcon,
CurrencyIcon,
BracesIcon,
GlassesIcon,
PaintbrushIcon,
PackageOpenIcon,
DiscordIcon,
BlueskyIcon,
TwitterIcon,
MastodonIcon,
GithubIcon,
ReportIcon,
ScaleIcon,
SearchIcon,
ServerIcon,
SettingsIcon,
SunIcon,
TwitterIcon,
UserIcon,
XIcon,
} from "@modrinth/assets";
import {
Avatar,
Button,
ButtonStyled,
commonMessages,
OverflowMenu,
PagewideBanner,
Avatar,
commonMessages,
} from "@modrinth/ui";
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 ModalCreation from "~/components/ui/ModalCreation.vue";
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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