You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['custom/library'],
|
||||
ignorePatterns: ['**/*.scss', '**/*.svg', 'node_modules/', 'dist/', '**/*.gltf'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
}
|
||||
@@ -2,210 +2,210 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function toPascalCase(str: string): string {
|
||||
return str
|
||||
.split(/[-_.]/)
|
||||
.filter((part) => part.length > 0)
|
||||
.map((word) => {
|
||||
if (/^\d/.test(word)) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
})
|
||||
.join('')
|
||||
return str
|
||||
.split(/[-_.]/)
|
||||
.filter((part) => part.length > 0)
|
||||
.map((word) => {
|
||||
if (/^\d/.test(word)) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
function generateIconExports(): { imports: string; exports: string } {
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const iconsDir = path.join(packageRoot, 'icons')
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const iconsDir = path.join(packageRoot, 'icons')
|
||||
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
throw new Error(`Icons directory not found: ${iconsDir}`)
|
||||
}
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
throw new Error(`Icons directory not found: ${iconsDir}`)
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(iconsDir)
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.sort()
|
||||
const files = fs
|
||||
.readdirSync(iconsDir)
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.sort()
|
||||
|
||||
let imports = ''
|
||||
let exports = ''
|
||||
let imports = ''
|
||||
let exports = ''
|
||||
|
||||
files.forEach((file) => {
|
||||
const baseName = path.basename(file, '.svg')
|
||||
let pascalName = toPascalCase(baseName)
|
||||
files.forEach((file) => {
|
||||
const baseName = path.basename(file, '.svg')
|
||||
let pascalName = toPascalCase(baseName)
|
||||
|
||||
if (pascalName === '') {
|
||||
pascalName = 'Unknown'
|
||||
}
|
||||
if (pascalName === '') {
|
||||
pascalName = 'Unknown'
|
||||
}
|
||||
|
||||
if (!pascalName.endsWith('Icon')) {
|
||||
pascalName += 'Icon'
|
||||
}
|
||||
if (!pascalName.endsWith('Icon')) {
|
||||
pascalName += 'Icon'
|
||||
}
|
||||
|
||||
const privateName = `_${pascalName}`
|
||||
const privateName = `_${pascalName}`
|
||||
|
||||
imports += `import ${privateName} from './icons/${file}?component'\n`
|
||||
exports += `export const ${pascalName} = ${privateName}\n`
|
||||
})
|
||||
imports += `import ${privateName} from './icons/${file}?component'\n`
|
||||
exports += `export const ${pascalName} = ${privateName}\n`
|
||||
})
|
||||
|
||||
return { imports, exports }
|
||||
return { imports, exports }
|
||||
}
|
||||
|
||||
function runTests(): void {
|
||||
console.log('🧪 Running conversion tests...\n')
|
||||
console.log('🧪 Running conversion tests...\n')
|
||||
|
||||
const testCases: Array<{ input: string; expected: string }> = [
|
||||
{ input: 'align-left', expected: 'AlignLeftIcon' },
|
||||
{ input: 'arrow-big-up-dash', expected: 'ArrowBigUpDashIcon' },
|
||||
{ input: 'check-check', expected: 'CheckCheckIcon' },
|
||||
{ input: 'chevron-left', expected: 'ChevronLeftIcon' },
|
||||
{ input: 'file-archive', expected: 'FileArchiveIcon' },
|
||||
{ input: 'heart-handshake', expected: 'HeartHandshakeIcon' },
|
||||
{ input: 'monitor-smartphone', expected: 'MonitorSmartphoneIcon' },
|
||||
{ input: 'x-circle', expected: 'XCircleIcon' },
|
||||
{ input: 'rotate-ccw', expected: 'RotateCcwIcon' },
|
||||
{ input: 'bell-ring', expected: 'BellRingIcon' },
|
||||
{ input: 'more-horizontal', expected: 'MoreHorizontalIcon' },
|
||||
{ input: 'list_bulleted', expected: 'ListBulletedIcon' },
|
||||
{ input: 'test.name', expected: 'TestNameIcon' },
|
||||
{ input: 'test-name_final.icon', expected: 'TestNameFinalIcon' },
|
||||
]
|
||||
const testCases: Array<{ input: string; expected: string }> = [
|
||||
{ input: 'align-left', expected: 'AlignLeftIcon' },
|
||||
{ input: 'arrow-big-up-dash', expected: 'ArrowBigUpDashIcon' },
|
||||
{ input: 'check-check', expected: 'CheckCheckIcon' },
|
||||
{ input: 'chevron-left', expected: 'ChevronLeftIcon' },
|
||||
{ input: 'file-archive', expected: 'FileArchiveIcon' },
|
||||
{ input: 'heart-handshake', expected: 'HeartHandshakeIcon' },
|
||||
{ input: 'monitor-smartphone', expected: 'MonitorSmartphoneIcon' },
|
||||
{ input: 'x-circle', expected: 'XCircleIcon' },
|
||||
{ input: 'rotate-ccw', expected: 'RotateCcwIcon' },
|
||||
{ input: 'bell-ring', expected: 'BellRingIcon' },
|
||||
{ input: 'more-horizontal', expected: 'MoreHorizontalIcon' },
|
||||
{ input: 'list_bulleted', expected: 'ListBulletedIcon' },
|
||||
{ input: 'test.name', expected: 'TestNameIcon' },
|
||||
{ input: 'test-name_final.icon', expected: 'TestNameFinalIcon' },
|
||||
]
|
||||
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
const result = toPascalCase(input) + (toPascalCase(input).endsWith('Icon') ? '' : 'Icon')
|
||||
const success = result === expected
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
const result = toPascalCase(input) + (toPascalCase(input).endsWith('Icon') ? '' : 'Icon')
|
||||
const success = result === expected
|
||||
|
||||
if (success) {
|
||||
console.log(`✅ ${input} → ${result}`)
|
||||
passed++
|
||||
} else {
|
||||
console.log(`❌ ${input} → ${result} (expected: ${expected})`)
|
||||
failed++
|
||||
}
|
||||
})
|
||||
if (success) {
|
||||
console.log(`✅ ${input} → ${result}`)
|
||||
passed++
|
||||
} else {
|
||||
console.log(`❌ ${input} → ${result} (expected: ${expected})`)
|
||||
failed++
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`)
|
||||
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`)
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
if (failed > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function generateFiles(): void {
|
||||
try {
|
||||
console.log('🔄 Generating icon exports...')
|
||||
try {
|
||||
console.log('🔄 Generating icon exports...')
|
||||
|
||||
const { imports, exports } = generateIconExports()
|
||||
const output = `// Auto-generated icon imports and exports
|
||||
const { imports, exports } = generateIconExports()
|
||||
const output = `// Auto-generated icon imports and exports
|
||||
// Do not edit this file manually - run 'pnpm run fix' to regenerate
|
||||
|
||||
${imports}
|
||||
${exports}`
|
||||
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const outputPath = path.join(packageRoot, 'generated-icons.ts')
|
||||
fs.writeFileSync(outputPath, output)
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const outputPath = path.join(packageRoot, 'generated-icons.ts')
|
||||
fs.writeFileSync(outputPath, output)
|
||||
|
||||
console.log(`✅ Generated icon exports to: ${outputPath}`)
|
||||
console.log(
|
||||
`📦 Generated ${imports.split('\n').filter((line) => line.trim()).length} icon imports/exports`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`✅ Generated icon exports to: ${outputPath}`)
|
||||
console.log(
|
||||
`📦 Generated ${imports.split('\n').filter((line) => line.trim()).length} icon imports/exports`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = process.argv.slice(2)
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.includes('--test')) {
|
||||
runTests()
|
||||
} else if (args.includes('--validate')) {
|
||||
validateIconConsistency()
|
||||
} else {
|
||||
generateFiles()
|
||||
}
|
||||
if (args.includes('--test')) {
|
||||
runTests()
|
||||
} else if (args.includes('--validate')) {
|
||||
validateIconConsistency()
|
||||
} else {
|
||||
generateFiles()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
function getExpectedIconExports(iconsDir: string): string[] {
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
return []
|
||||
}
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(iconsDir)
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.map((file) => {
|
||||
const baseName = path.basename(file, '.svg')
|
||||
let pascalName = toPascalCase(baseName)
|
||||
return fs
|
||||
.readdirSync(iconsDir)
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.map((file) => {
|
||||
const baseName = path.basename(file, '.svg')
|
||||
let pascalName = toPascalCase(baseName)
|
||||
|
||||
if (pascalName === '') {
|
||||
pascalName = 'Unknown'
|
||||
}
|
||||
if (pascalName === '') {
|
||||
pascalName = 'Unknown'
|
||||
}
|
||||
|
||||
if (!pascalName.endsWith('Icon')) {
|
||||
pascalName += 'Icon'
|
||||
}
|
||||
if (!pascalName.endsWith('Icon')) {
|
||||
pascalName += 'Icon'
|
||||
}
|
||||
|
||||
return pascalName
|
||||
})
|
||||
.sort()
|
||||
return pascalName
|
||||
})
|
||||
.sort()
|
||||
}
|
||||
|
||||
function getActualIconExports(indexFile: string): string[] {
|
||||
if (!fs.existsSync(indexFile)) {
|
||||
return []
|
||||
}
|
||||
if (!fs.existsSync(indexFile)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(indexFile, 'utf8')
|
||||
const exportMatches = content.match(/export const (\w+Icon) = _\w+Icon/g) || []
|
||||
const content = fs.readFileSync(indexFile, 'utf8')
|
||||
const exportMatches = content.match(/export const (\w+Icon) = _\w+Icon/g) || []
|
||||
|
||||
return exportMatches
|
||||
.map((match) => {
|
||||
const result = match.match(/export const (\w+Icon)/)
|
||||
return result ? result[1] : ''
|
||||
})
|
||||
.filter((name) => name.endsWith('Icon'))
|
||||
.sort()
|
||||
return exportMatches
|
||||
.map((match) => {
|
||||
const result = match.match(/export const (\w+Icon)/)
|
||||
return result ? result[1] : ''
|
||||
})
|
||||
.filter((name) => name.endsWith('Icon'))
|
||||
.sort()
|
||||
}
|
||||
|
||||
function validateIconConsistency(): void {
|
||||
try {
|
||||
console.log('🔍 Validating icon consistency...')
|
||||
try {
|
||||
console.log('🔍 Validating icon consistency...')
|
||||
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const iconsDir = path.join(packageRoot, 'icons')
|
||||
const declarationFile = path.join(packageRoot, 'generated-icons.ts')
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
const iconsDir = path.join(packageRoot, 'icons')
|
||||
const declarationFile = path.join(packageRoot, 'generated-icons.ts')
|
||||
|
||||
const expectedExports = getExpectedIconExports(iconsDir)
|
||||
const actualExports = getActualIconExports(declarationFile)
|
||||
const expectedExports = getExpectedIconExports(iconsDir)
|
||||
const actualExports = getActualIconExports(declarationFile)
|
||||
|
||||
const missingExports = expectedExports.filter((name) => !actualExports.includes(name))
|
||||
const extraExports = actualExports.filter((name) => !expectedExports.includes(name))
|
||||
const missingExports = expectedExports.filter((name) => !actualExports.includes(name))
|
||||
const extraExports = actualExports.filter((name) => !expectedExports.includes(name))
|
||||
|
||||
if (missingExports.length > 0) {
|
||||
console.error(`❌ Missing icon exports: ${missingExports.join(', ')}`)
|
||||
console.error("Run 'pnpm run fix' to generate them.")
|
||||
process.exit(1)
|
||||
}
|
||||
if (missingExports.length > 0) {
|
||||
console.error(`❌ Missing icon exports: ${missingExports.join(', ')}`)
|
||||
console.error("Run 'pnpm run fix' to generate them.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (extraExports.length > 0) {
|
||||
console.error(
|
||||
`❌ Extra icon exports (no corresponding SVG files): ${extraExports.join(', ')}`,
|
||||
)
|
||||
console.error("Run 'pnpm run fix' to clean them up.")
|
||||
process.exit(1)
|
||||
}
|
||||
if (extraExports.length > 0) {
|
||||
console.error(
|
||||
`❌ Extra icon exports (no corresponding SVG files): ${extraExports.join(', ')}`,
|
||||
)
|
||||
console.error("Run 'pnpm run fix' to clean them up.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('✅ Icon exports are consistent with SVG files')
|
||||
} catch (error) {
|
||||
console.error('❌ Error validating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('✅ Icon exports are consistent with SVG files')
|
||||
} catch (error) {
|
||||
console.error('❌ Error validating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
2
packages/assets/eslint.config.mjs
Normal file
2
packages/assets/eslint.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
@@ -8,26 +8,26 @@ import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component'
|
||||
import _AsteriskIcon from './icons/asterisk.svg?component'
|
||||
import _BadgeCheckIcon from './icons/badge-check.svg?component'
|
||||
import _BanIcon from './icons/ban.svg?component'
|
||||
import _BellRingIcon from './icons/bell-ring.svg?component'
|
||||
import _BellIcon from './icons/bell.svg?component'
|
||||
import _BellRingIcon from './icons/bell-ring.svg?component'
|
||||
import _BlocksIcon from './icons/blocks.svg?component'
|
||||
import _BoldIcon from './icons/bold.svg?component'
|
||||
import _BookIcon from './icons/book.svg?component'
|
||||
import _BookOpenIcon from './icons/book-open.svg?component'
|
||||
import _BookTextIcon from './icons/book-text.svg?component'
|
||||
import _BookIcon from './icons/book.svg?component'
|
||||
import _BookmarkIcon from './icons/bookmark.svg?component'
|
||||
import _BotIcon from './icons/bot.svg?component'
|
||||
import _BoxImportIcon from './icons/box-import.svg?component'
|
||||
import _BoxIcon from './icons/box.svg?component'
|
||||
import _BoxImportIcon from './icons/box-import.svg?component'
|
||||
import _BracesIcon from './icons/braces.svg?component'
|
||||
import _BrushCleaningIcon from './icons/brush-cleaning.svg?component'
|
||||
import _CalendarIcon from './icons/calendar.svg?component'
|
||||
import _CardIcon from './icons/card.svg?component'
|
||||
import _ChangeSkinIcon from './icons/change-skin.svg?component'
|
||||
import _ChartIcon from './icons/chart.svg?component'
|
||||
import _CheckIcon from './icons/check.svg?component'
|
||||
import _CheckCheckIcon from './icons/check-check.svg?component'
|
||||
import _CheckCircleIcon from './icons/check-circle.svg?component'
|
||||
import _CheckIcon from './icons/check.svg?component'
|
||||
import _ChevronLeftIcon from './icons/chevron-left.svg?component'
|
||||
import _ChevronRightIcon from './icons/chevron-right.svg?component'
|
||||
import _ClearIcon from './icons/clear.svg?component'
|
||||
@@ -56,13 +56,13 @@ import _EditIcon from './icons/edit.svg?component'
|
||||
import _EllipsisVerticalIcon from './icons/ellipsis-vertical.svg?component'
|
||||
import _ExpandIcon from './icons/expand.svg?component'
|
||||
import _ExternalIcon from './icons/external.svg?component'
|
||||
import _EyeOffIcon from './icons/eye-off.svg?component'
|
||||
import _EyeIcon from './icons/eye.svg?component'
|
||||
import _EyeOffIcon from './icons/eye-off.svg?component'
|
||||
import _FileIcon from './icons/file.svg?component'
|
||||
import _FileArchiveIcon from './icons/file-archive.svg?component'
|
||||
import _FileTextIcon from './icons/file-text.svg?component'
|
||||
import _FileIcon from './icons/file.svg?component'
|
||||
import _FilterXIcon from './icons/filter-x.svg?component'
|
||||
import _FilterIcon from './icons/filter.svg?component'
|
||||
import _FilterXIcon from './icons/filter-x.svg?component'
|
||||
import _FolderArchiveIcon from './icons/folder-archive.svg?component'
|
||||
import _FolderOpenIcon from './icons/folder-open.svg?component'
|
||||
import _FolderSearchIcon from './icons/folder-search.svg?component'
|
||||
@@ -79,8 +79,8 @@ import _HashIcon from './icons/hash.svg?component'
|
||||
import _Heading1Icon from './icons/heading-1.svg?component'
|
||||
import _Heading2Icon from './icons/heading-2.svg?component'
|
||||
import _Heading3Icon from './icons/heading-3.svg?component'
|
||||
import _HeartHandshakeIcon from './icons/heart-handshake.svg?component'
|
||||
import _HeartIcon from './icons/heart.svg?component'
|
||||
import _HeartHandshakeIcon from './icons/heart-handshake.svg?component'
|
||||
import _HistoryIcon from './icons/history.svg?component'
|
||||
import _HomeIcon from './icons/home.svg?component'
|
||||
import _ImageIcon from './icons/image.svg?component'
|
||||
@@ -96,13 +96,13 @@ import _LeftArrowIcon from './icons/left-arrow.svg?component'
|
||||
import _LibraryIcon from './icons/library.svg?component'
|
||||
import _LightBulbIcon from './icons/light-bulb.svg?component'
|
||||
import _LinkIcon from './icons/link.svg?component'
|
||||
import _ListIcon from './icons/list.svg?component'
|
||||
import _ListBulletedIcon from './icons/list-bulleted.svg?component'
|
||||
import _ListEndIcon from './icons/list-end.svg?component'
|
||||
import _ListOrderedIcon from './icons/list-ordered.svg?component'
|
||||
import _ListIcon from './icons/list.svg?component'
|
||||
import _LoaderIcon from './icons/loader.svg?component'
|
||||
import _LockOpenIcon from './icons/lock-open.svg?component'
|
||||
import _LockIcon from './icons/lock.svg?component'
|
||||
import _LockOpenIcon from './icons/lock-open.svg?component'
|
||||
import _LogInIcon from './icons/log-in.svg?component'
|
||||
import _LogOutIcon from './icons/log-out.svg?component'
|
||||
import _MailIcon from './icons/mail.svg?component'
|
||||
@@ -113,8 +113,8 @@ import _MessageIcon from './icons/message.svg?component'
|
||||
import _MicrophoneIcon from './icons/microphone.svg?component'
|
||||
import _MinimizeIcon from './icons/minimize.svg?component'
|
||||
import _MinusIcon from './icons/minus.svg?component'
|
||||
import _MonitorSmartphoneIcon from './icons/monitor-smartphone.svg?component'
|
||||
import _MonitorIcon from './icons/monitor.svg?component'
|
||||
import _MonitorSmartphoneIcon from './icons/monitor-smartphone.svg?component'
|
||||
import _MoonIcon from './icons/moon.svg?component'
|
||||
import _MoreHorizontalIcon from './icons/more-horizontal.svg?component'
|
||||
import _MoreVerticalIcon from './icons/more-vertical.svg?component'
|
||||
@@ -123,16 +123,16 @@ import _NoSignalIcon from './icons/no-signal.svg?component'
|
||||
import _NotepadTextIcon from './icons/notepad-text.svg?component'
|
||||
import _OmorphiaIcon from './icons/omorphia.svg?component'
|
||||
import _OrganizationIcon from './icons/organization.svg?component'
|
||||
import _PackageIcon from './icons/package.svg?component'
|
||||
import _PackageClosedIcon from './icons/package-closed.svg?component'
|
||||
import _PackageOpenIcon from './icons/package-open.svg?component'
|
||||
import _PackageIcon from './icons/package.svg?component'
|
||||
import _PaintbrushIcon from './icons/paintbrush.svg?component'
|
||||
import _PickaxeIcon from './icons/pickaxe.svg?component'
|
||||
import _PlayIcon from './icons/play.svg?component'
|
||||
import _PlugIcon from './icons/plug.svg?component'
|
||||
import _PlusIcon from './icons/plus.svg?component'
|
||||
import _RadioButtonCheckedIcon from './icons/radio-button-checked.svg?component'
|
||||
import _RadioButtonIcon from './icons/radio-button.svg?component'
|
||||
import _RadioButtonCheckedIcon from './icons/radio-button-checked.svg?component'
|
||||
import _ReceiptTextIcon from './icons/receipt-text.svg?component'
|
||||
import _RedoIcon from './icons/redo.svg?component'
|
||||
import _ReplyIcon from './icons/reply.svg?component'
|
||||
@@ -147,8 +147,8 @@ import _ScaleIcon from './icons/scale.svg?component'
|
||||
import _ScanEyeIcon from './icons/scan-eye.svg?component'
|
||||
import _SearchIcon from './icons/search.svg?component'
|
||||
import _SendIcon from './icons/send.svg?component'
|
||||
import _ServerPlusIcon from './icons/server-plus.svg?component'
|
||||
import _ServerIcon from './icons/server.svg?component'
|
||||
import _ServerPlusIcon from './icons/server-plus.svg?component'
|
||||
import _SettingsIcon from './icons/settings.svg?component'
|
||||
import _ShareIcon from './icons/share.svg?component'
|
||||
import _ShieldIcon from './icons/shield.svg?component'
|
||||
@@ -177,23 +177,23 @@ import _TrashIcon from './icons/trash.svg?component'
|
||||
import _TriangleAlertIcon from './icons/triangle-alert.svg?component'
|
||||
import _UnderlineIcon from './icons/underline.svg?component'
|
||||
import _UndoIcon from './icons/undo.svg?component'
|
||||
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
||||
import _UnknownIcon from './icons/unknown.svg?component'
|
||||
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
||||
import _UnlinkIcon from './icons/unlink.svg?component'
|
||||
import _UnplugIcon from './icons/unplug.svg?component'
|
||||
import _UpdatedIcon from './icons/updated.svg?component'
|
||||
import _UploadIcon from './icons/upload.svg?component'
|
||||
import _UserIcon from './icons/user.svg?component'
|
||||
import _UserPlusIcon from './icons/user-plus.svg?component'
|
||||
import _UserXIcon from './icons/user-x.svg?component'
|
||||
import _UserIcon from './icons/user.svg?component'
|
||||
import _UsersIcon from './icons/users.svg?component'
|
||||
import _VersionIcon from './icons/version.svg?component'
|
||||
import _WikiIcon from './icons/wiki.svg?component'
|
||||
import _WindowIcon from './icons/window.svg?component'
|
||||
import _WorldIcon from './icons/world.svg?component'
|
||||
import _WrenchIcon from './icons/wrench.svg?component'
|
||||
import _XCircleIcon from './icons/x-circle.svg?component'
|
||||
import _XIcon from './icons/x.svg?component'
|
||||
import _XCircleIcon from './icons/x-circle.svg?component'
|
||||
import _ZoomInIcon from './icons/zoom-in.svg?component'
|
||||
import _ZoomOutIcon from './icons/zoom-out.svg?component'
|
||||
|
||||
|
||||
14
packages/assets/icons.d.ts
vendored
14
packages/assets/icons.d.ts
vendored
@@ -1,16 +1,16 @@
|
||||
declare module '*.svg?component' {
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
const src: FunctionalComponent<SVGAttributes>
|
||||
export default src
|
||||
const src: FunctionalComponent<SVGAttributes>
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const src: string
|
||||
export default src
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string
|
||||
export default src
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import './omorphia.scss'
|
||||
|
||||
import _FourOhFourNotFound from './branding/404.svg?component'
|
||||
// Branding
|
||||
import _ModrinthIcon from './branding/logo.svg?component'
|
||||
import _FourOhFourNotFound from './branding/404.svg?component'
|
||||
import _ModrinthPlusIcon from './branding/modrinth-plus.svg?component'
|
||||
import _AngryRinthbot from './branding/rinthbot/angry.webp'
|
||||
import _AnnoyedRinthbot from './branding/rinthbot/annoyed.webp'
|
||||
@@ -22,14 +22,6 @@ import _SleepingRinthbot from './branding/rinthbot/sleeping.webp'
|
||||
import _SobbingRinthbot from './branding/rinthbot/sobbing.webp'
|
||||
import _ThinkingRinthbot from './branding/rinthbot/thinking.webp'
|
||||
import _WavingRinthbot from './branding/rinthbot/waving.webp'
|
||||
|
||||
// External Icons
|
||||
import _SSODiscordIcon from './external/sso/discord.svg?component'
|
||||
import _SSOGitHubIcon from './external/sso/github.svg?component'
|
||||
import _SSOGitLabIcon from './external/sso/gitlab.svg?component'
|
||||
import _SSOGoogleIcon from './external/sso/google.svg?component'
|
||||
import _SSOMicrosoftIcon from './external/sso/microsoft.svg?component'
|
||||
import _SSOSteamIcon from './external/sso/steam.svg?component'
|
||||
import _AppleIcon from './external/apple.svg?component'
|
||||
import _BlueskyIcon from './external/bluesky.svg?component'
|
||||
import _BuyMeACoffeeIcon from './external/bmac.svg?component'
|
||||
@@ -42,6 +34,13 @@ import _OpenCollectiveIcon from './external/opencollective.svg?component'
|
||||
import _PatreonIcon from './external/patreon.svg?component'
|
||||
import _PayPalIcon from './external/paypal.svg?component'
|
||||
import _RedditIcon from './external/reddit.svg?component'
|
||||
// External Icons
|
||||
import _SSODiscordIcon from './external/sso/discord.svg?component'
|
||||
import _SSOGitHubIcon from './external/sso/github.svg?component'
|
||||
import _SSOGitLabIcon from './external/sso/gitlab.svg?component'
|
||||
import _SSOGoogleIcon from './external/sso/google.svg?component'
|
||||
import _SSOMicrosoftIcon from './external/sso/microsoft.svg?component'
|
||||
import _SSOSteamIcon from './external/sso/steam.svg?component'
|
||||
import _TumblrIcon from './external/tumblr.svg?component'
|
||||
import _TwitterIcon from './external/twitter.svg?component'
|
||||
import _WindowsIcon from './external/windows.svg?component'
|
||||
@@ -84,7 +83,6 @@ export const WindowsIcon = _WindowsIcon
|
||||
export const YouTubeIcon = _YouTubeIcon
|
||||
|
||||
// Skin Models
|
||||
export * from './generated-icons'
|
||||
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
|
||||
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
|
||||
|
||||
export * from './generated-icons'
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,21 +1,19 @@
|
||||
{
|
||||
"name": "@modrinth/assets",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"scripts": {
|
||||
"lint": "pnpm run icons:validate && eslint . && prettier --check .",
|
||||
"fix": "pnpm run icons:generate && eslint . --fix && prettier --write .",
|
||||
"icons:test": "jiti build/generate-exports.ts --test",
|
||||
"icons:validate": "jiti build/generate-exports.ts --validate",
|
||||
"icons:generate": "jiti build/generate-exports.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"jiti": "^2.4.2",
|
||||
"tsconfig": "workspace:*",
|
||||
"vue": "^3.5.13"
|
||||
}
|
||||
"name": "@modrinth/assets",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"scripts": {
|
||||
"lint": "pnpm run icons:validate && eslint . && prettier --check .",
|
||||
"fix": "pnpm run icons:generate && eslint . --fix && prettier --write .",
|
||||
"icons:test": "jiti build/generate-exports.ts --test",
|
||||
"icons:validate": "jiti build/generate-exports.ts --validate",
|
||||
"icons:generate": "jiti build/generate-exports.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"jiti": "^2.4.2",
|
||||
"vue": "^3.5.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[tabindex='0']:focus-visible {
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +1,45 @@
|
||||
// Use border box on everything to preserve everyone's sanity
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-base);
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-family: var(--font-standard);
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-base);
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-family: var(--font-standard);
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
// Font Sizes
|
||||
--font-size-xxs: 0.625rem; //10px
|
||||
--font-size-xs: 0.75rem; //12px
|
||||
--font-size-sm: 0.875rem; //14px
|
||||
--font-size-nm: 1rem; //16px
|
||||
--font-size-md: 1.125rem; //18px
|
||||
--font-size-lg: 1.25rem; //20px
|
||||
--font-size-xl: 1.5rem; //24px
|
||||
--font-size-2xl: 2rem; //32px
|
||||
--font-size-3xl: 3rem; //48px
|
||||
// Font Sizes
|
||||
--font-size-xxs: 0.625rem; //10px
|
||||
--font-size-xs: 0.75rem; //12px
|
||||
--font-size-sm: 0.875rem; //14px
|
||||
--font-size-nm: 1rem; //16px
|
||||
--font-size-md: 1.125rem; //18px
|
||||
--font-size-lg: 1.25rem; //20px
|
||||
--font-size-xl: 1.5rem; //24px
|
||||
--font-size-2xl: 2rem; //32px
|
||||
--font-size-3xl: 3rem; //48px
|
||||
|
||||
// Font Weights
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
// Font Weights
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
|
||||
--font-weight-text: var(--font-weight-medium);
|
||||
--font-weight-heading: var(--font-weight-extrabold);
|
||||
--font-weight-title: var(--font-weight-extrabold);
|
||||
--font-weight-text: var(--font-weight-medium);
|
||||
--font-weight-heading: var(--font-weight-extrabold);
|
||||
--font-weight-title: var(--font-weight-extrabold);
|
||||
}
|
||||
|
||||
a.uncolored {
|
||||
color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
@@ -49,189 +49,189 @@ input[type='password'],
|
||||
textarea,
|
||||
.input-text-inherit,
|
||||
.cm-content {
|
||||
border-radius: var(--radius-md);
|
||||
box-sizing: border-box;
|
||||
// safari iOS rounds inputs by default
|
||||
// set the appearance to none to prevent this
|
||||
appearance: none !important;
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 36px;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: var(--radius-md);
|
||||
box-sizing: border-box;
|
||||
// safari iOS rounds inputs by default
|
||||
// set the appearance to none to prevent this
|
||||
appearance: none !important;
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 36px;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-contrast);
|
||||
outline: none;
|
||||
}
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-contrast);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-base);
|
||||
opacity: 0.6;
|
||||
}
|
||||
&::placeholder {
|
||||
color: var(--color-base);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
white-space: pre-wrap !important;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
&::-webkit-inner-spin-button,
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&::-webkit-inner-spin-button,
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1px;
|
||||
.animated-dropdown {
|
||||
width: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1px;
|
||||
.animated-dropdown {
|
||||
width: unset;
|
||||
|
||||
.selected {
|
||||
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
||||
.selected {
|
||||
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
||||
|
||||
&.render-down {
|
||||
border-radius: var(--radius-md) 0 0 0;
|
||||
}
|
||||
&.render-down {
|
||||
border-radius: var(--radius-md) 0 0 0;
|
||||
}
|
||||
|
||||
&.render-up {
|
||||
border-radius: 0 0 0 var(--radius-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.render-up {
|
||||
border-radius: 0 0 0 var(--radius-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
input {
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
padding: 0 0.5rem 0 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
input {
|
||||
padding: 0 0.5rem 0 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:focus-within svg {
|
||||
opacity: 1;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
&:focus-within svg {
|
||||
opacity: 1;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
z-index: 1;
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
z-index: 1;
|
||||
|
||||
color: var(--color-base);
|
||||
opacity: 0.6;
|
||||
}
|
||||
color: var(--color-base);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.r-btn {
|
||||
@extend .transparent, .icon-only;
|
||||
.r-btn {
|
||||
@extend .transparent, .icon-only;
|
||||
|
||||
position: absolute;
|
||||
right: 0.125rem;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0.125rem;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
position: relative;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.chart {
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-animation {
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.button-animation,
|
||||
button {
|
||||
transform: none !important;
|
||||
}
|
||||
.button-animation,
|
||||
button {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.button-animation,
|
||||
button {
|
||||
transform: none !important;
|
||||
}
|
||||
.button-animation,
|
||||
button {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-contrast);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-contrast);
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block: var(--gap-md) var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
margin-block: var(--gap-md) var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.hljs,
|
||||
.hljs-subst {
|
||||
color: #444;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #888888;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
@@ -13,8 +13,8 @@
|
||||
.hljs-meta-keyword,
|
||||
.hljs-doctag,
|
||||
.hljs-name {
|
||||
color: #f58300;
|
||||
font-weight: bold;
|
||||
color: #f58300;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-type,
|
||||
@@ -25,13 +25,13 @@
|
||||
.hljs-quote,
|
||||
.hljs-template-tag,
|
||||
.hljs-deletion {
|
||||
color: var(--color-brand);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #008888;
|
||||
font-weight: bold;
|
||||
color: #008888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
@@ -41,49 +41,49 @@
|
||||
.hljs-link,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #bc6060;
|
||||
color: #bc6060;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #78a960;
|
||||
color: #78a960;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-code,
|
||||
.hljs-addition {
|
||||
color: #f58300;
|
||||
color: #f58300;
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: #1f7199;
|
||||
color: #1f7199;
|
||||
}
|
||||
|
||||
.hljs-meta-string {
|
||||
color: #4d99bf;
|
||||
color: #4d99bf;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #222222;
|
||||
padding: 1em 1em 1em 1em;
|
||||
border-width: 5px;
|
||||
border-radius: 2em;
|
||||
border-color: var(--color-button-bg);
|
||||
overflow-x: hidden;
|
||||
background-color: #222222;
|
||||
padding: 1em 1em 1em 1em;
|
||||
border-width: 5px;
|
||||
border-radius: 2em;
|
||||
border-color: var(--color-button-bg);
|
||||
overflow-x: hidden;
|
||||
|
||||
code {
|
||||
line-height: 100%;
|
||||
padding: 0.2em;
|
||||
letter-spacing: -0.05em;
|
||||
word-break: normal;
|
||||
font-family: monospace;
|
||||
}
|
||||
code {
|
||||
line-height: 100%;
|
||||
padding: 0.2em;
|
||||
letter-spacing: -0.05em;
|
||||
word-break: normal;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff?v=3.19') format('woff');
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff?v=3.19') format('woff');
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff?v=3.19') format('woff');
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff?v=3.19') format('woff');
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff?v=3.19') format('woff');
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff?v=3.19') format('woff');
|
||||
}
|
||||
|
||||
118
packages/assets/styles/normalize.scss
vendored
118
packages/assets/styles/normalize.scss
vendored
@@ -9,8 +9,8 @@
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
@@ -21,7 +21,7 @@ html {
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ body {
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,8 +38,8 @@ main {
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
@@ -51,9 +51,9 @@ h1 {
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,8 +62,8 @@ hr {
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
@@ -74,7 +74,7 @@ pre {
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,9 +83,9 @@ a {
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ abbr[title] {
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,8 +105,8 @@ strong {
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +114,7 @@ samp {
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,18 +124,18 @@ small {
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
@@ -146,7 +146,7 @@ sup {
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
@@ -162,10 +162,10 @@ input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,8 +175,8 @@ textarea {
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,8 +186,8 @@ input {
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +198,7 @@ button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,8 +209,8 @@ button::-moz-focus-inner,
|
||||
[type='button']::-moz-focus-inner,
|
||||
[type='reset']::-moz-focus-inner,
|
||||
[type='submit']::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,7 +221,7 @@ button:-moz-focusring,
|
||||
[type='button']:-moz-focusring,
|
||||
[type='reset']:-moz-focusring,
|
||||
[type='submit']:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,7 +229,7 @@ button:-moz-focusring,
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,12 +240,12 @@ fieldset {
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +253,7 @@ legend {
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +261,7 @@ progress {
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,8 +271,8 @@ textarea {
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,7 +281,7 @@ textarea {
|
||||
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,8 +290,8 @@ textarea {
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +299,7 @@ textarea {
|
||||
*/
|
||||
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,8 +308,8 @@ textarea {
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
@@ -320,7 +320,7 @@ textarea {
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -328,7 +328,7 @@ details {
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
@@ -339,7 +339,7 @@ summary {
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,5 +347,5 @@ template {
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,292 +1,292 @@
|
||||
.light-properties {
|
||||
--color-bg: #e5e7eb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-super-raised-bg: #e9e9e9;
|
||||
--color-button-bg: hsl(220, 13%, 91%);
|
||||
--color-button-border: rgba(161, 161, 161, 0.35);
|
||||
--color-scrollbar: #96a2b0;
|
||||
--color-bg: #e5e7eb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-super-raised-bg: #e9e9e9;
|
||||
--color-button-bg: hsl(220, 13%, 91%);
|
||||
--color-button-border: rgba(161, 161, 161, 0.35);
|
||||
--color-scrollbar: #96a2b0;
|
||||
|
||||
--color-divider: #babfc5;
|
||||
--color-divider-dark: #c8cdd3;
|
||||
--color-divider: #babfc5;
|
||||
--color-divider-dark: #c8cdd3;
|
||||
|
||||
--color-base: hsl(221, 39%, 11%);
|
||||
--color-secondary: #6b7280;
|
||||
--color-contrast: #1a202c;
|
||||
--color-accent-contrast: #ffffff;
|
||||
--color-base: hsl(221, 39%, 11%);
|
||||
--color-secondary: #6b7280;
|
||||
--color-contrast: #1a202c;
|
||||
--color-accent-contrast: #ffffff;
|
||||
|
||||
--color-red: #cb2245;
|
||||
--color-orange: #e08325;
|
||||
--color-green: #00af5c;
|
||||
--color-blue: #1f68c0;
|
||||
--color-purple: #8e32f3;
|
||||
--color-gray: #595b61;
|
||||
--color-red: #cb2245;
|
||||
--color-orange: #e08325;
|
||||
--color-green: #00af5c;
|
||||
--color-blue: #1f68c0;
|
||||
--color-purple: #8e32f3;
|
||||
--color-gray: #595b61;
|
||||
|
||||
--color-red-highlight: rgba(203, 34, 69, 0.25);
|
||||
--color-orange-highlight: rgba(224, 131, 37, 0.25);
|
||||
--color-green-highlight: rgba(0, 175, 92, 0.25);
|
||||
--color-blue-highlight: rgba(31, 104, 192, 0.25);
|
||||
--color-purple-highlight: rgba(142, 50, 243, 0.25);
|
||||
--color-gray-highlight: rgba(89, 91, 97, 0.25);
|
||||
--color-red-highlight: rgba(203, 34, 69, 0.25);
|
||||
--color-orange-highlight: rgba(224, 131, 37, 0.25);
|
||||
--color-green-highlight: rgba(0, 175, 92, 0.25);
|
||||
--color-blue-highlight: rgba(31, 104, 192, 0.25);
|
||||
--color-purple-highlight: rgba(142, 50, 243, 0.25);
|
||||
--color-gray-highlight: rgba(89, 91, 97, 0.25);
|
||||
|
||||
--color-red-bg: rgba(203, 34, 69, 0.1);
|
||||
--color-orange-bg: rgba(224, 131, 37, 0.1);
|
||||
--color-green-bg: rgba(0, 175, 92, 0.1);
|
||||
--color-blue-bg: rgba(31, 104, 192, 0.1);
|
||||
--color-purple-bg: rgba(142, 50, 243, 0.1);
|
||||
--color-red-bg: rgba(203, 34, 69, 0.1);
|
||||
--color-orange-bg: rgba(224, 131, 37, 0.1);
|
||||
--color-green-bg: rgba(0, 175, 92, 0.1);
|
||||
--color-blue-bg: rgba(31, 104, 192, 0.1);
|
||||
--color-purple-bg: rgba(142, 50, 243, 0.1);
|
||||
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: var(--color-green-highlight);
|
||||
--color-brand-shadow: rgba(0, 175, 92, 0.7);
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: var(--color-green-highlight);
|
||||
--color-brand-shadow: rgba(0, 175, 92, 0.7);
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 91%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 91%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 91%, 0.15);
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 91%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 91%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 91%, 0.15);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
|
||||
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
|
||||
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
|
||||
--brand-gradient-bg: linear-gradient(
|
||||
0deg,
|
||||
rgba(68, 182, 138, 0.175) 0%,
|
||||
rgba(58, 250, 112, 0.125) 100%
|
||||
);
|
||||
--brand-gradient-strong-bg: linear-gradient(
|
||||
270deg,
|
||||
rgba(68, 182, 138, 0.175) 0%,
|
||||
rgba(36, 225, 91, 0.12) 100%
|
||||
);
|
||||
--brand-gradient-button: rgba(255, 255, 255, 0.5);
|
||||
--brand-gradient-border: rgba(32, 64, 32, 0.15);
|
||||
--brand-gradient-fade-out-color: linear-gradient(to bottom, rgba(213, 235, 224, 0), #d0ece0 70%);
|
||||
--brand-gradient-bg: linear-gradient(
|
||||
0deg,
|
||||
rgba(68, 182, 138, 0.175) 0%,
|
||||
rgba(58, 250, 112, 0.125) 100%
|
||||
);
|
||||
--brand-gradient-strong-bg: linear-gradient(
|
||||
270deg,
|
||||
rgba(68, 182, 138, 0.175) 0%,
|
||||
rgba(36, 225, 91, 0.12) 100%
|
||||
);
|
||||
--brand-gradient-button: rgba(255, 255, 255, 0.5);
|
||||
--brand-gradient-border: rgba(32, 64, 32, 0.15);
|
||||
--brand-gradient-fade-out-color: linear-gradient(to bottom, rgba(213, 235, 224, 0), #d0ece0 70%);
|
||||
|
||||
--color-button-bg-selected: var(--color-brand);
|
||||
--color-button-text-selected: var(--color-accent-contrast);
|
||||
--color-button-bg-selected: var(--color-brand);
|
||||
--color-button-text-selected: var(--color-accent-contrast);
|
||||
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #f8f9fa 0%, #dce0e6 100%);
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #f8f9fa 0%, #dce0e6 100%);
|
||||
|
||||
--loading-bar-gradient: linear-gradient(to right, var(--color-brand) 0%, #00af5c 100%);
|
||||
--loading-bar-gradient: linear-gradient(to right, var(--color-brand) 0%, #00af5c 100%);
|
||||
|
||||
--color-platform-fabric: #8a7b71;
|
||||
--color-platform-quilt: #8b61b4;
|
||||
--color-platform-forge: #5b6197;
|
||||
--color-platform-neoforge: #dc895c;
|
||||
--color-platform-liteloader: #4c90de;
|
||||
--color-platform-bukkit: #e78362;
|
||||
--color-platform-bungeecord: #c69e39;
|
||||
--color-platform-folia: #6aa54f;
|
||||
--color-platform-paper: #e67e7e;
|
||||
--color-platform-purpur: #7763a3;
|
||||
--color-platform-spigot: #cd7a21;
|
||||
--color-platform-velocity: #4b98b0;
|
||||
--color-platform-waterfall: #5f83cb;
|
||||
--color-platform-sponge: #c49528;
|
||||
--color-platform-ornithe: #6097ca;
|
||||
--color-platform-bta-babric: #5ba938;
|
||||
--color-platform-legacy-fabric: #6879f6;
|
||||
--color-platform-nilloader: #dd5088;
|
||||
--color-platform-fabric: #8a7b71;
|
||||
--color-platform-quilt: #8b61b4;
|
||||
--color-platform-forge: #5b6197;
|
||||
--color-platform-neoforge: #dc895c;
|
||||
--color-platform-liteloader: #4c90de;
|
||||
--color-platform-bukkit: #e78362;
|
||||
--color-platform-bungeecord: #c69e39;
|
||||
--color-platform-folia: #6aa54f;
|
||||
--color-platform-paper: #e67e7e;
|
||||
--color-platform-purpur: #7763a3;
|
||||
--color-platform-spigot: #cd7a21;
|
||||
--color-platform-velocity: #4b98b0;
|
||||
--color-platform-waterfall: #5f83cb;
|
||||
--color-platform-sponge: #c49528;
|
||||
--color-platform-ornithe: #6097ca;
|
||||
--color-platform-bta-babric: #5ba938;
|
||||
--color-platform-legacy-fabric: #6879f6;
|
||||
--color-platform-nilloader: #dd5088;
|
||||
|
||||
--hover-brightness: 0.9;
|
||||
--hover-brightness: 0.9;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend .light-properties;
|
||||
--dark-color-base: #b0bac5;
|
||||
--dark-color-contrast: #ecf9fb;
|
||||
@extend .light-properties;
|
||||
--dark-color-base: #b0bac5;
|
||||
--dark-color-contrast: #ecf9fb;
|
||||
|
||||
--gap-xs: 0.25rem;
|
||||
--gap-sm: 0.5rem;
|
||||
--gap-md: 0.75rem;
|
||||
--gap-lg: 1rem;
|
||||
--gap-xl: 1.5rem;
|
||||
--gap-xs: 0.25rem;
|
||||
--gap-sm: 0.5rem;
|
||||
--gap-md: 0.75rem;
|
||||
--gap-lg: 1rem;
|
||||
--gap-xl: 1.5rem;
|
||||
|
||||
--radius-xs: 0.25rem;
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
--radius-max: 999999999px;
|
||||
--radius-xs: 0.25rem;
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
--radius-max: 999999999px;
|
||||
|
||||
--color-tooltip-text: var(--dark-color-contrast);
|
||||
--color-tooltip-bg: #000;
|
||||
--color-tooltip-text: var(--dark-color-contrast);
|
||||
--color-tooltip-bg: #000;
|
||||
|
||||
--color-ad: rgba(125, 75, 162, 0.2);
|
||||
--color-ad-raised: rgba(190, 140, 243, 0.5);
|
||||
--color-ad-contrast: black;
|
||||
--color-ad-highlight: var(--color-purple);
|
||||
--color-ad: rgba(125, 75, 162, 0.2);
|
||||
--color-ad-raised: rgba(190, 140, 243, 0.5);
|
||||
--color-ad-contrast: black;
|
||||
--color-ad-highlight: var(--color-purple);
|
||||
}
|
||||
|
||||
.light-mode,
|
||||
.light {
|
||||
@extend .light-properties;
|
||||
@extend .light-properties;
|
||||
}
|
||||
|
||||
.dark-mode,
|
||||
.dark,
|
||||
:root[data-theme='dark'] {
|
||||
--color-bg: #16181c;
|
||||
--color-raised-bg: #26292f;
|
||||
--color-super-raised-bg: #40434a;
|
||||
--color-button-bg: hsl(222, 13%, 30%);
|
||||
--color-button-border: rgba(193, 190, 209, 0.12);
|
||||
--color-scrollbar: var(--color-button-bg);
|
||||
--color-bg: #16181c;
|
||||
--color-raised-bg: #26292f;
|
||||
--color-super-raised-bg: #40434a;
|
||||
--color-button-bg: hsl(222, 13%, 30%);
|
||||
--color-button-border: rgba(193, 190, 209, 0.12);
|
||||
--color-scrollbar: var(--color-button-bg);
|
||||
|
||||
--color-divider: var(--color-button-bg);
|
||||
--color-divider-dark: #646c75;
|
||||
--color-divider: var(--color-button-bg);
|
||||
--color-divider-dark: #646c75;
|
||||
|
||||
--color-base: var(--dark-color-base);
|
||||
--color-secondary: #96a2b0;
|
||||
--color-contrast: var(--dark-color-contrast);
|
||||
--color-accent-contrast: #000000;
|
||||
--color-base: var(--dark-color-base);
|
||||
--color-secondary: #96a2b0;
|
||||
--color-contrast: var(--dark-color-contrast);
|
||||
--color-accent-contrast: #000000;
|
||||
|
||||
--color-red: #ff496e;
|
||||
--color-orange: #ffa347;
|
||||
--color-green: #1bd96a;
|
||||
--color-blue: #4f9cff;
|
||||
--color-purple: #c78aff;
|
||||
--color-gray: #9fa4b3;
|
||||
--color-red: #ff496e;
|
||||
--color-orange: #ffa347;
|
||||
--color-green: #1bd96a;
|
||||
--color-blue: #4f9cff;
|
||||
--color-purple: #c78aff;
|
||||
--color-gray: #9fa4b3;
|
||||
|
||||
--color-red-highlight: rgba(255, 73, 110, 0.25);
|
||||
--color-orange-highlight: rgba(255, 163, 71, 0.25);
|
||||
--color-green-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-blue-highlight: rgba(79, 156, 255, 0.25);
|
||||
--color-purple-highlight: rgba(199, 138, 255, 0.25);
|
||||
--color-gray-highlight: rgba(159, 164, 179, 0.25);
|
||||
--color-red-highlight: rgba(255, 73, 110, 0.25);
|
||||
--color-orange-highlight: rgba(255, 163, 71, 0.25);
|
||||
--color-green-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-blue-highlight: rgba(79, 156, 255, 0.25);
|
||||
--color-purple-highlight: rgba(199, 138, 255, 0.25);
|
||||
--color-gray-highlight: rgba(159, 164, 179, 0.25);
|
||||
|
||||
--color-red-bg: rgba(255, 73, 110, 0.2);
|
||||
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
||||
--color-green-bg: rgba(27, 217, 106, 0.2);
|
||||
--color-blue-bg: rgba(79, 156, 255, 0.2);
|
||||
--color-purple-bg: rgba(199, 138, 255, 0.2);
|
||||
--color-red-bg: rgba(255, 73, 110, 0.2);
|
||||
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
||||
--color-green-bg: rgba(27, 217, 106, 0.2);
|
||||
--color-blue-bg: rgba(79, 156, 255, 0.2);
|
||||
--color-purple-bg: rgba(199, 138, 255, 0.2);
|
||||
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-brand-shadow: rgba(27, 217, 106, 0.7);
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-brand-shadow: rgba(27, 217, 106, 0.7);
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 1px hsla(221, 39%, 11%, 0.25);
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 1px hsla(221, 39%, 11%, 0.25);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
|
||||
--brand-gradient-bg: linear-gradient(0deg, rgba(14, 35, 19, 0.2) 0%, rgba(55, 137, 73, 0.1) 100%);
|
||||
--brand-gradient-strong-bg: linear-gradient(270deg, #09110d 10%, #131f17 100%);
|
||||
--brand-gradient-button: rgba(255, 255, 255, 0.08);
|
||||
--brand-gradient-border: rgba(155, 255, 160, 0.08);
|
||||
--brand-gradient-fade-out-color: linear-gradient(to bottom, rgba(24, 30, 31, 0), #171d1e 80%);
|
||||
--brand-gradient-bg: linear-gradient(0deg, rgba(14, 35, 19, 0.2) 0%, rgba(55, 137, 73, 0.1) 100%);
|
||||
--brand-gradient-strong-bg: linear-gradient(270deg, #09110d 10%, #131f17 100%);
|
||||
--brand-gradient-button: rgba(255, 255, 255, 0.08);
|
||||
--brand-gradient-border: rgba(155, 255, 160, 0.08);
|
||||
--brand-gradient-fade-out-color: linear-gradient(to bottom, rgba(24, 30, 31, 0), #171d1e 80%);
|
||||
|
||||
--color-button-bg-selected: var(--color-brand-highlight);
|
||||
--color-button-text-selected: var(--color-brand);
|
||||
--color-button-bg-selected: var(--color-brand-highlight);
|
||||
--color-button-text-selected: var(--color-brand);
|
||||
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
|
||||
|
||||
--loading-bar-gradient: linear-gradient(to right, var(--color-brand) 0%, #1ffa9a 100%);
|
||||
--loading-bar-gradient: linear-gradient(to right, var(--color-brand) 0%, #1ffa9a 100%);
|
||||
|
||||
--color-platform-fabric: #dbb69b;
|
||||
--color-platform-quilt: #c796f9;
|
||||
--color-platform-forge: #959eef;
|
||||
--color-platform-neoforge: #f99e6b;
|
||||
--color-platform-liteloader: #7ab0ee;
|
||||
--color-platform-bukkit: #f6af7b;
|
||||
--color-platform-bungeecord: #d2c080;
|
||||
--color-platform-folia: #a5e388;
|
||||
--color-platform-paper: #eeaaaa;
|
||||
--color-platform-purpur: #c3abf7;
|
||||
--color-platform-spigot: #f1cc84;
|
||||
--color-platform-velocity: #83d5ef;
|
||||
--color-platform-waterfall: #78a4fb;
|
||||
--color-platform-sponge: #f9e580;
|
||||
--color-platform-ornithe: #87c7ff;
|
||||
--color-platform-bta-babric: #72cc4a;
|
||||
--color-platform-legacy-fabric: #6879f6;
|
||||
--color-platform-nilloader: #f45e9a;
|
||||
--color-platform-fabric: #dbb69b;
|
||||
--color-platform-quilt: #c796f9;
|
||||
--color-platform-forge: #959eef;
|
||||
--color-platform-neoforge: #f99e6b;
|
||||
--color-platform-liteloader: #7ab0ee;
|
||||
--color-platform-bukkit: #f6af7b;
|
||||
--color-platform-bungeecord: #d2c080;
|
||||
--color-platform-folia: #a5e388;
|
||||
--color-platform-paper: #eeaaaa;
|
||||
--color-platform-purpur: #c3abf7;
|
||||
--color-platform-spigot: #f1cc84;
|
||||
--color-platform-velocity: #83d5ef;
|
||||
--color-platform-waterfall: #78a4fb;
|
||||
--color-platform-sponge: #f9e580;
|
||||
--color-platform-ornithe: #87c7ff;
|
||||
--color-platform-bta-babric: #72cc4a;
|
||||
--color-platform-legacy-fabric: #6879f6;
|
||||
--color-platform-nilloader: #f45e9a;
|
||||
|
||||
--hover-brightness: 1.25;
|
||||
--hover-brightness: 1.25;
|
||||
|
||||
--experimental-color-button-bg: #33363d;
|
||||
--experimental-color-button-bg: #33363d;
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
@extend .dark-mode;
|
||||
--color-bg: #000000;
|
||||
--color-raised-bg: #101013;
|
||||
--color-button-bg: #222329;
|
||||
@extend .dark-mode;
|
||||
--color-bg: #000000;
|
||||
--color-raised-bg: #101013;
|
||||
--color-button-bg: #222329;
|
||||
|
||||
--color-ad: #0d1828;
|
||||
--color-ad: #0d1828;
|
||||
|
||||
--brand-gradient-bg: linear-gradient(
|
||||
0deg,
|
||||
rgba(22, 66, 51, 0.15) 0%,
|
||||
rgba(55, 137, 73, 0.1) 100%
|
||||
);
|
||||
--brand-gradient-strong-bg: linear-gradient(
|
||||
270deg,
|
||||
rgba(9, 18, 14, 0.6) 10%,
|
||||
rgba(19, 31, 23, 0.5) 100%
|
||||
);
|
||||
--brand-gradient-bg: linear-gradient(
|
||||
0deg,
|
||||
rgba(22, 66, 51, 0.15) 0%,
|
||||
rgba(55, 137, 73, 0.1) 100%
|
||||
);
|
||||
--brand-gradient-strong-bg: linear-gradient(
|
||||
270deg,
|
||||
rgba(9, 18, 14, 0.6) 10%,
|
||||
rgba(19, 31, 23, 0.5) 100%
|
||||
);
|
||||
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #1b1b20 0%, #25262b 100%);
|
||||
--color-gradient-button-bg: linear-gradient(180deg, #1b1b20 0%, #25262b 100%);
|
||||
}
|
||||
|
||||
.retro-mode {
|
||||
--brand-gradient-strong-bg: #3a3b38;
|
||||
--brand-gradient-strong-bg: #3a3b38;
|
||||
}
|
||||
|
||||
.experimental-styles-within {
|
||||
--color-link: var(--color-blue) !important;
|
||||
--color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future
|
||||
--color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future
|
||||
--color-link: var(--color-blue) !important;
|
||||
--color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future
|
||||
--color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future
|
||||
}
|
||||
|
||||
.light-experiments {
|
||||
--color-bg: #ebebeb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-button-bg: #f5f5f5;
|
||||
--color-base: #2c2e31;
|
||||
--color-secondary: #484d54;
|
||||
--color-accent-contrast: #ffffff;
|
||||
--color-bg: #ebebeb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-button-bg: #f5f5f5;
|
||||
--color-base: #2c2e31;
|
||||
--color-secondary: #484d54;
|
||||
--color-accent-contrast: #ffffff;
|
||||
}
|
||||
|
||||
.light-mode,
|
||||
.light {
|
||||
.experimental-styles-within,
|
||||
&.experimental-styles-within {
|
||||
@extend .light-experiments;
|
||||
}
|
||||
.experimental-styles-within,
|
||||
&.experimental-styles-within {
|
||||
@extend .light-experiments;
|
||||
}
|
||||
}
|
||||
|
||||
.experimental-styles-within {
|
||||
.light-mode,
|
||||
.light {
|
||||
@extend .light-experiments;
|
||||
}
|
||||
.light-mode,
|
||||
.light {
|
||||
@extend .light-experiments;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-experiments {
|
||||
--color-button-bg: var(--experimental-color-button-bg);
|
||||
--color-button-bg: var(--experimental-color-button-bg);
|
||||
}
|
||||
|
||||
.dark-mode:not(.oled-mode),
|
||||
.dark:not(.oled-mode) {
|
||||
.experimental-styles-within,
|
||||
&.experimental-styles-within {
|
||||
@extend .dark-experiments;
|
||||
}
|
||||
.experimental-styles-within,
|
||||
&.experimental-styles-within {
|
||||
@extend .dark-experiments;
|
||||
}
|
||||
}
|
||||
|
||||
.experimental-styles-within {
|
||||
.dark-mode:not(.oled-mode),
|
||||
.dark:not(.oled-mode) {
|
||||
@extend .dark-experiments;
|
||||
}
|
||||
.dark-mode:not(.oled-mode),
|
||||
.dark:not(.oled-mode) {
|
||||
@extend .dark-experiments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"extends": "tsconfig/base.json",
|
||||
"include": [".", "icons.d.ts", ".eslintrc.js"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
"extends": "@modrinth/tooling-config/typescript/vue.json"
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['custom/library'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as path from 'path'
|
||||
|
||||
import { repoPath } from './utils'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import * as path from 'path'
|
||||
import { repoPath, toVarName } from './utils'
|
||||
import { glob } from 'glob'
|
||||
import * as path from 'path'
|
||||
|
||||
import { PUBLIC_SRC, PUBLIC_LOCATIONS, ARTICLES_GLOB, COMPILED_DIR } from './blog.config'
|
||||
import { ARTICLES_GLOB, COMPILED_DIR, PUBLIC_LOCATIONS, PUBLIC_SRC } from './blog.config'
|
||||
import { repoPath, toVarName } from './utils'
|
||||
|
||||
async function checkPublicAssets() {
|
||||
const srcFiles = await glob('**/*', { cwd: PUBLIC_SRC, dot: true })
|
||||
let allOk = true
|
||||
for (const target of PUBLIC_LOCATIONS) {
|
||||
for (const relativeFile of srcFiles) {
|
||||
const shouldExist = path.posix.join(target, relativeFile)
|
||||
try {
|
||||
await fs.access(shouldExist)
|
||||
} catch {
|
||||
console.error(`⚠️ Missing public asset: ${shouldExist}`)
|
||||
allOk = false
|
||||
}
|
||||
}
|
||||
if (allOk) {
|
||||
console.log(`✅ All public assets exist in: ${target}`)
|
||||
}
|
||||
}
|
||||
if (!allOk) process.exit(1)
|
||||
const srcFiles = await glob('**/*', { cwd: PUBLIC_SRC, dot: true })
|
||||
let allOk = true
|
||||
for (const target of PUBLIC_LOCATIONS) {
|
||||
for (const relativeFile of srcFiles) {
|
||||
const shouldExist = path.posix.join(target, relativeFile)
|
||||
try {
|
||||
await fs.access(shouldExist)
|
||||
} catch {
|
||||
console.error(`⚠️ Missing public asset: ${shouldExist}`)
|
||||
allOk = false
|
||||
}
|
||||
}
|
||||
if (allOk) {
|
||||
console.log(`✅ All public assets exist in: ${target}`)
|
||||
}
|
||||
}
|
||||
if (!allOk) process.exit(1)
|
||||
}
|
||||
|
||||
async function checkCompiledArticles() {
|
||||
const mdFiles = await glob(ARTICLES_GLOB)
|
||||
const compiledFiles = await glob(`${COMPILED_DIR}/*.ts`)
|
||||
const compiledVarNames = compiledFiles.map((f) => path.basename(f, '.ts'))
|
||||
const mdFiles = await glob(ARTICLES_GLOB)
|
||||
const compiledFiles = await glob(`${COMPILED_DIR}/*.ts`)
|
||||
const compiledVarNames = compiledFiles.map((f) => path.basename(f, '.ts'))
|
||||
|
||||
// Check all .md have compiled .ts and .content.ts and the proper public thumbnail
|
||||
for (const file of mdFiles) {
|
||||
const varName = toVarName(path.basename(file, '.md'))
|
||||
const compiledPath = path.posix.join(COMPILED_DIR, varName + '.ts')
|
||||
const contentPath = path.posix.join(COMPILED_DIR, varName + '.content.ts')
|
||||
if (!compiledVarNames.includes(varName)) {
|
||||
console.error(`⚠️ Missing compiled article for: ${file} (should be: ${compiledPath})`)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
await fs.access(compiledPath)
|
||||
} catch {
|
||||
console.error(`⚠️ Compiled article file not found: ${compiledPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
await fs.access(contentPath)
|
||||
} catch {
|
||||
console.error(`⚠️ Compiled article content file not found: ${contentPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
// Check all .md have compiled .ts and .content.ts and the proper public thumbnail
|
||||
for (const file of mdFiles) {
|
||||
const varName = toVarName(path.basename(file, '.md'))
|
||||
const compiledPath = path.posix.join(COMPILED_DIR, varName + '.ts')
|
||||
const contentPath = path.posix.join(COMPILED_DIR, varName + '.content.ts')
|
||||
if (!compiledVarNames.includes(varName)) {
|
||||
console.error(`⚠️ Missing compiled article for: ${file} (should be: ${compiledPath})`)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
await fs.access(compiledPath)
|
||||
} catch {
|
||||
console.error(`⚠️ Compiled article file not found: ${compiledPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
await fs.access(contentPath)
|
||||
} catch {
|
||||
console.error(`⚠️ Compiled article content file not found: ${contentPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Check compiled .ts still have corresponding .md
|
||||
for (const compiled of compiledFiles) {
|
||||
const varName = path.basename(compiled, '.ts')
|
||||
if (varName === 'index' || varName.endsWith('.content')) continue
|
||||
// Check compiled .ts still have corresponding .md
|
||||
for (const compiled of compiledFiles) {
|
||||
const varName = path.basename(compiled, '.ts')
|
||||
if (varName === 'index' || varName.endsWith('.content')) continue
|
||||
|
||||
const mdPathGlob = repoPath(`packages/blog/articles/**/${varName.replace(/_/g, '*')}.md`)
|
||||
const found = await glob(mdPathGlob)
|
||||
if (!found.length) {
|
||||
console.error(`❌ Compiled article ${compiled} has no matching markdown source!`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
const mdPathGlob = repoPath(`packages/blog/articles/**/${varName.replace(/_/g, '*')}.md`)
|
||||
const found = await glob(mdPathGlob)
|
||||
if (!found.length) {
|
||||
console.error(`❌ Compiled article ${compiled} has no matching markdown source!`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
'🎉 All articles are correctly compiled, matched, and have thumbnails (if declared)!',
|
||||
)
|
||||
console.log(
|
||||
'🎉 All articles are correctly compiled, matched, and have thumbnails (if declared)!',
|
||||
)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔎 Checking public assets...')
|
||||
await checkPublicAssets()
|
||||
console.log('🔎 Checking public assets...')
|
||||
await checkPublicAssets()
|
||||
|
||||
console.log('🔎 Checking compiled articles...')
|
||||
await checkCompiledArticles()
|
||||
console.log('🔎 Checking compiled articles...')
|
||||
await checkCompiledArticles()
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('❌ Error in check.ts:', e)
|
||||
process.exit(1)
|
||||
console.error('❌ Error in check.ts:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import * as path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { md } from '@modrinth/utils'
|
||||
import { promises as fs } from 'fs'
|
||||
import { glob } from 'glob'
|
||||
import matter from 'gray-matter'
|
||||
import { minify } from 'html-minifier-terser'
|
||||
import { copyDir, toVarName } from './utils'
|
||||
import * as path from 'path'
|
||||
import RSS from 'rss'
|
||||
import { parseStringPromise } from 'xml2js'
|
||||
import { glob } from 'glob'
|
||||
|
||||
import {
|
||||
ARTICLES_GLOB,
|
||||
COMPILED_DIR,
|
||||
ROOT_FILE,
|
||||
PUBLIC_SRC,
|
||||
PUBLIC_LOCATIONS,
|
||||
RSS_PATH,
|
||||
JSON_PATH,
|
||||
SITE_URL,
|
||||
ARTICLES_GLOB,
|
||||
COMPILED_DIR,
|
||||
JSON_PATH,
|
||||
PUBLIC_LOCATIONS,
|
||||
PUBLIC_SRC,
|
||||
ROOT_FILE,
|
||||
RSS_PATH,
|
||||
SITE_URL,
|
||||
} from './blog.config'
|
||||
import { copyDir, toVarName } from './utils'
|
||||
|
||||
async function ensureCompiledDir() {
|
||||
await fs.mkdir(COMPILED_DIR, { recursive: true })
|
||||
await fs.mkdir(COMPILED_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
async function hasThumbnail(slug: string): Promise<boolean> {
|
||||
const thumbnailPath = path.posix.join(PUBLIC_SRC, slug, 'thumbnail.webp')
|
||||
try {
|
||||
await fs.access(thumbnailPath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const thumbnailPath = path.posix.join(PUBLIC_SRC, slug, 'thumbnail.webp')
|
||||
try {
|
||||
await fs.access(thumbnailPath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getArticleLink(slug: string): string {
|
||||
return `${SITE_URL}/news/article/${slug}`
|
||||
return `${SITE_URL}/news/article/${slug}`
|
||||
}
|
||||
|
||||
function getThumbnailUrl(slug: string, hasThumb: boolean): string {
|
||||
if (hasThumb) {
|
||||
return `${SITE_URL}/news/article/${slug}/thumbnail.webp`
|
||||
} else {
|
||||
return `${SITE_URL}/news/default.webp`
|
||||
}
|
||||
if (hasThumb) {
|
||||
return `${SITE_URL}/news/article/${slug}/thumbnail.webp`
|
||||
} else {
|
||||
return `${SITE_URL}/news/default.webp`
|
||||
}
|
||||
}
|
||||
|
||||
async function compileArticles() {
|
||||
await ensureCompiledDir()
|
||||
await ensureCompiledDir()
|
||||
|
||||
const files = await glob(ARTICLES_GLOB)
|
||||
console.log(`🔎 Found ${files.length} markdown articles!`)
|
||||
const articleExports: string[] = []
|
||||
const articlesArray: string[] = []
|
||||
const articlesForRss = []
|
||||
const articlesForJson = []
|
||||
const files = await glob(ARTICLES_GLOB)
|
||||
console.log(`🔎 Found ${files.length} markdown articles!`)
|
||||
const articleExports: string[] = []
|
||||
const articlesArray: string[] = []
|
||||
const articlesForRss = []
|
||||
const articlesForJson = []
|
||||
|
||||
for (const file of files) {
|
||||
const src = await fs.readFile(file, 'utf8')
|
||||
const { content, data } = matter(src)
|
||||
for (const file of files) {
|
||||
const src = await fs.readFile(file, 'utf8')
|
||||
const { content, data } = matter(src)
|
||||
|
||||
const { title, summary, date, slug: frontSlug, authors: authorsData, ...rest } = data
|
||||
if (!title || !summary || !date) {
|
||||
console.error(`❌ Missing required frontmatter in ${file}. Required: title, summary, date`)
|
||||
process.exit(1)
|
||||
}
|
||||
const { title, summary, date, slug: frontSlug, authors: authorsData, ...rest } = data
|
||||
if (!title || !summary || !date) {
|
||||
console.error(`❌ Missing required frontmatter in ${file}. Required: title, summary, date`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const html = md().render(content)
|
||||
const minifiedHtml = await minify(html, {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
})
|
||||
const html = md().render(content)
|
||||
const minifiedHtml = await minify(html, {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
})
|
||||
|
||||
const authors = authorsData ? authorsData : []
|
||||
const authors = authorsData ? authorsData : []
|
||||
|
||||
const slug = frontSlug || path.basename(file, '.md')
|
||||
const varName = toVarName(slug)
|
||||
const exportFile = path.posix.join(COMPILED_DIR, `${varName}.ts`)
|
||||
const contentFile = path.posix.join(COMPILED_DIR, `${varName}.content.ts`)
|
||||
const thumbnailPresent = await hasThumbnail(slug)
|
||||
const slug = frontSlug || path.basename(file, '.md')
|
||||
const varName = toVarName(slug)
|
||||
const exportFile = path.posix.join(COMPILED_DIR, `${varName}.ts`)
|
||||
const contentFile = path.posix.join(COMPILED_DIR, `${varName}.content.ts`)
|
||||
const thumbnailPresent = await hasThumbnail(slug)
|
||||
|
||||
const contentTs = `
|
||||
const contentTs = `
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const html = \`${minifiedHtml}\`;
|
||||
`.trimStart()
|
||||
await fs.writeFile(contentFile, contentTs, 'utf8')
|
||||
await fs.writeFile(contentFile, contentTs, 'utf8')
|
||||
|
||||
const ts = `
|
||||
const ts = `
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(\`./${varName}.content\`).then(m => m.html),
|
||||
@@ -96,35 +96,35 @@ export const article = {
|
||||
authors: ${JSON.stringify(authors)},
|
||||
thumbnail: ${thumbnailPresent},
|
||||
${Object.keys(rest)
|
||||
.map((k) => `${k}: ${JSON.stringify(rest[k])},`)
|
||||
.join('\n ')}
|
||||
.map((k) => `${k}: ${JSON.stringify(rest[k])},`)
|
||||
.join('\n ')}
|
||||
};
|
||||
`.trimStart()
|
||||
|
||||
await fs.writeFile(exportFile, ts, 'utf8')
|
||||
articleExports.push(`import { article as ${varName} } from "./${varName}";`)
|
||||
articlesArray.push(varName)
|
||||
await fs.writeFile(exportFile, ts, 'utf8')
|
||||
articleExports.push(`import { article as ${varName} } from "./${varName}";`)
|
||||
articlesArray.push(varName)
|
||||
|
||||
articlesForRss.push({
|
||||
title,
|
||||
summary,
|
||||
date,
|
||||
slug,
|
||||
html: minifiedHtml,
|
||||
} as never)
|
||||
articlesForRss.push({
|
||||
title,
|
||||
summary,
|
||||
date,
|
||||
slug,
|
||||
html: minifiedHtml,
|
||||
} as never)
|
||||
|
||||
articlesForJson.push({
|
||||
title,
|
||||
summary,
|
||||
thumbnail: getThumbnailUrl(slug, thumbnailPresent),
|
||||
date: new Date(date).toISOString(),
|
||||
link: getArticleLink(slug),
|
||||
} as never)
|
||||
}
|
||||
articlesForJson.push({
|
||||
title,
|
||||
summary,
|
||||
thumbnail: getThumbnailUrl(slug, thumbnailPresent),
|
||||
date: new Date(date).toISOString(),
|
||||
link: getArticleLink(slug),
|
||||
} as never)
|
||||
}
|
||||
|
||||
console.log(`📂 Compiled ${files.length} articles.`)
|
||||
console.log(`📂 Compiled ${files.length} articles.`)
|
||||
|
||||
const rootExport = `
|
||||
const rootExport = `
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
${articleExports.join('\n')}
|
||||
|
||||
@@ -133,124 +133,124 @@ export const articles = [
|
||||
];
|
||||
`.trimStart()
|
||||
|
||||
await fs.writeFile(ROOT_FILE, rootExport, 'utf8')
|
||||
console.log(`🌟 Done! Wrote root articles export.`)
|
||||
await fs.writeFile(ROOT_FILE, rootExport, 'utf8')
|
||||
console.log(`🌟 Done! Wrote root articles export.`)
|
||||
|
||||
await generateRssFeed(articlesForRss)
|
||||
await generateJsonFile(articlesForJson)
|
||||
await generateRssFeed(articlesForRss)
|
||||
await generateJsonFile(articlesForJson)
|
||||
}
|
||||
|
||||
async function generateRssFeed(articles): Promise<void> {
|
||||
const sorted = [...articles].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
)
|
||||
const sorted = [...articles].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
)
|
||||
|
||||
let currentRssArticles: { title: string; html: string }[] = []
|
||||
try {
|
||||
const xml = await fs.readFile(RSS_PATH, 'utf8')
|
||||
const parsed = await parseStringPromise(xml)
|
||||
const items = parsed.rss?.channel?.[0]?.item || []
|
||||
currentRssArticles = items.map((item) => ({
|
||||
title: (item.title?.[0] ?? '').trim(),
|
||||
html: (item['content:encoded']?.[0] ?? '').replace(/^<!\[CDATA\[|\]\]>$/g, '').trim(),
|
||||
}))
|
||||
} catch {
|
||||
currentRssArticles = []
|
||||
}
|
||||
let currentRssArticles: { title: string; html: string }[] = []
|
||||
try {
|
||||
const xml = await fs.readFile(RSS_PATH, 'utf8')
|
||||
const parsed = await parseStringPromise(xml)
|
||||
const items = parsed.rss?.channel?.[0]?.item || []
|
||||
currentRssArticles = items.map((item) => ({
|
||||
title: (item.title?.[0] ?? '').trim(),
|
||||
html: (item['content:encoded']?.[0] ?? '').replace(/^<!\[CDATA\[|\]\]>$/g, '').trim(),
|
||||
}))
|
||||
} catch {
|
||||
currentRssArticles = []
|
||||
}
|
||||
|
||||
const newArr = sorted.map((a) => ({
|
||||
title: (a.title ?? '').trim(),
|
||||
html: (a.html ?? '').trim(),
|
||||
}))
|
||||
const newArr = sorted.map((a) => ({
|
||||
title: (a.title ?? '').trim(),
|
||||
html: (a.html ?? '').trim(),
|
||||
}))
|
||||
|
||||
let isEqual = currentRssArticles.length === newArr.length
|
||||
if (isEqual) {
|
||||
for (let i = 0; i < newArr.length; ++i) {
|
||||
if (
|
||||
currentRssArticles[i].title !== newArr[i].title ||
|
||||
currentRssArticles[i].html !== newArr[i].html
|
||||
) {
|
||||
isEqual = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let isEqual = currentRssArticles.length === newArr.length
|
||||
if (isEqual) {
|
||||
for (let i = 0; i < newArr.length; ++i) {
|
||||
if (
|
||||
currentRssArticles[i].title !== newArr[i].title ||
|
||||
currentRssArticles[i].html !== newArr[i].html
|
||||
) {
|
||||
isEqual = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEqual) {
|
||||
console.log(`⏭️ RSS feed not regenerated (articles unchanged)`)
|
||||
return
|
||||
}
|
||||
if (isEqual) {
|
||||
console.log(`⏭️ RSS feed not regenerated (articles unchanged)`)
|
||||
return
|
||||
}
|
||||
|
||||
const feed = new RSS({
|
||||
title: 'Modrinth News',
|
||||
description: 'Keep up-to-date on the latest news from Modrinth.',
|
||||
feed_url: `${SITE_URL}/news/feed/rss.xml`,
|
||||
site_url: `${SITE_URL}/news/`,
|
||||
language: 'en',
|
||||
generator: '@modrinth/blog',
|
||||
})
|
||||
const feed = new RSS({
|
||||
title: 'Modrinth News',
|
||||
description: 'Keep up-to-date on the latest news from Modrinth.',
|
||||
feed_url: `${SITE_URL}/news/feed/rss.xml`,
|
||||
site_url: `${SITE_URL}/news/`,
|
||||
language: 'en',
|
||||
generator: '@modrinth/blog',
|
||||
})
|
||||
|
||||
for (const article of sorted) {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
description: article.summary,
|
||||
url: `${SITE_URL}/news/article/${article.slug}/`,
|
||||
guid: `${SITE_URL}/news/article/${article.slug}/`,
|
||||
date: article.date,
|
||||
custom_elements: [{ 'content:encoded': `<![CDATA[${article.html}]]>` }],
|
||||
})
|
||||
}
|
||||
for (const article of sorted) {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
description: article.summary,
|
||||
url: `${SITE_URL}/news/article/${article.slug}/`,
|
||||
guid: `${SITE_URL}/news/article/${article.slug}/`,
|
||||
date: article.date,
|
||||
custom_elements: [{ 'content:encoded': `<![CDATA[${article.html}]]>` }],
|
||||
})
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(RSS_PATH), { recursive: true })
|
||||
await fs.writeFile(RSS_PATH, feed.xml({ indent: true }), 'utf8')
|
||||
console.log(`📂 RSS feed written to ${RSS_PATH}`)
|
||||
await fs.mkdir(path.dirname(RSS_PATH), { recursive: true })
|
||||
await fs.writeFile(RSS_PATH, feed.xml({ indent: true }), 'utf8')
|
||||
console.log(`📂 RSS feed written to ${RSS_PATH}`)
|
||||
}
|
||||
|
||||
async function generateJsonFile(articles): Promise<void> {
|
||||
const sorted = [...articles].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
)
|
||||
const json = { articles: sorted }
|
||||
await fs.mkdir(path.dirname(JSON_PATH), { recursive: true })
|
||||
await fs.writeFile(JSON_PATH, JSON.stringify(json, null, 2) + '\n', 'utf8')
|
||||
console.log(`📝 Wrote JSON articles to ${JSON_PATH}`)
|
||||
const sorted = [...articles].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
)
|
||||
const json = { articles: sorted }
|
||||
await fs.mkdir(path.dirname(JSON_PATH), { recursive: true })
|
||||
await fs.writeFile(JSON_PATH, JSON.stringify(json, null, 2) + '\n', 'utf8')
|
||||
console.log(`📝 Wrote JSON articles to ${JSON_PATH}`)
|
||||
}
|
||||
|
||||
async function deleteDirContents(dir: string) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.posix.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await fs.rm(fullPath, { recursive: true, force: true })
|
||||
} else {
|
||||
await fs.unlink(fullPath)
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error deleting contents of ${dir}:`, error)
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.posix.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await fs.rm(fullPath, { recursive: true, force: true })
|
||||
} else {
|
||||
await fs.unlink(fullPath)
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error deleting contents of ${dir}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPublicAssets() {
|
||||
console.log('🚚 Copying ./public to all PUBLIC_LOCATIONS...')
|
||||
for (const loc of PUBLIC_LOCATIONS) {
|
||||
await deleteDirContents(loc)
|
||||
await copyDir(PUBLIC_SRC, loc)
|
||||
console.log(`📂 Copied ./public to ${loc}`)
|
||||
}
|
||||
console.log('🎉 All public assets copied!')
|
||||
console.log('🚚 Copying ./public to all PUBLIC_LOCATIONS...')
|
||||
for (const loc of PUBLIC_LOCATIONS) {
|
||||
await deleteDirContents(loc)
|
||||
await copyDir(PUBLIC_SRC, loc)
|
||||
console.log(`📂 Copied ./public to ${loc}`)
|
||||
}
|
||||
console.log('🎉 All public assets copied!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await compileArticles()
|
||||
await copyPublicAssets()
|
||||
await compileArticles()
|
||||
await copyPublicAssets()
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('❌ Error in compile.ts:', e)
|
||||
process.exit(1)
|
||||
console.error('❌ Error in compile.ts:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./a_new_chapter_for_modrinth_servers.content`).then((m) => m.html),
|
||||
title: 'A New Chapter for Modrinth Servers',
|
||||
summary: 'Modrinth Servers is now fully operated in-house by the Modrinth Team.',
|
||||
date: '2025-03-13T00:00:00.000Z',
|
||||
slug: 'a-new-chapter-for-modrinth-servers',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./a_new_chapter_for_modrinth_servers.content`).then((m) => m.html),
|
||||
title: 'A New Chapter for Modrinth Servers',
|
||||
summary: 'Modrinth Servers is now fully operated in-house by the Modrinth Team.',
|
||||
date: '2025-03-13T00:00:00.000Z',
|
||||
slug: 'a-new-chapter-for-modrinth-servers',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./accelerating_development.content`).then((m) => m.html),
|
||||
title: "Accelerating Modrinth's Development",
|
||||
summary: 'Our fundraiser and the future of Modrinth!',
|
||||
date: '2023-02-01T20:00:00.000Z',
|
||||
slug: 'accelerating-development',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4'],
|
||||
thumbnail: false,
|
||||
html: () => import(`./accelerating_development.content`).then((m) => m.html),
|
||||
title: "Accelerating Modrinth's Development",
|
||||
summary: 'Our fundraiser and the future of Modrinth!',
|
||||
date: '2023-02-01T20:00:00.000Z',
|
||||
slug: 'accelerating-development',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./becoming_sustainable.content`).then((m) => m.html),
|
||||
title: 'Quintupling Creator Revenue and Becoming Sustainable',
|
||||
summary: 'Announcing an update to our monetization program, creator split, and more!',
|
||||
date: '2024-09-13T20:00:00.000Z',
|
||||
slug: 'becoming-sustainable',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Becoming Sustainable',
|
||||
short_summary: 'Announcing 5x creator revenue and updates to the monetization program.',
|
||||
html: () => import(`./becoming_sustainable.content`).then((m) => m.html),
|
||||
title: 'Quintupling Creator Revenue and Becoming Sustainable',
|
||||
summary: 'Announcing an update to our monetization program, creator split, and more!',
|
||||
date: '2024-09-13T20:00:00.000Z',
|
||||
slug: 'becoming-sustainable',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Becoming Sustainable',
|
||||
short_summary: 'Announcing 5x creator revenue and updates to the monetization program.',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./capital_return.content`).then((m) => m.html),
|
||||
title: 'A Sustainable Path Forward for Modrinth',
|
||||
summary: 'Our capital return and what’s next.',
|
||||
date: '2024-04-04T20:00:00.000Z',
|
||||
slug: 'capital-return',
|
||||
authors: ['MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
html: () => import(`./capital_return.content`).then((m) => m.html),
|
||||
title: 'A Sustainable Path Forward for Modrinth',
|
||||
summary: 'Our capital return and what’s next.',
|
||||
date: '2024-04-04T20:00:00.000Z',
|
||||
slug: 'capital-return',
|
||||
authors: ['MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./carbon_ads.content`).then((m) => m.html),
|
||||
title: "Modrinth's Carbon Ads experiment",
|
||||
summary: 'Experimenting with a different ad providers to find one which one works for us.',
|
||||
date: '2022-09-08T00:00:00.000Z',
|
||||
slug: 'carbon-ads',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./carbon_ads.content`).then((m) => m.html),
|
||||
title: "Modrinth's Carbon Ads experiment",
|
||||
summary: 'Experimenting with a different ad providers to find one which one works for us.',
|
||||
date: '2022-09-08T00:00:00.000Z',
|
||||
slug: 'carbon-ads',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./creator_monetization.content`).then((m) => m.html),
|
||||
title: 'Creators can now make money on Modrinth!',
|
||||
summary:
|
||||
'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.',
|
||||
date: '2022-11-12T00:00:00.000Z',
|
||||
slug: 'creator-monetization',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./creator_monetization.content`).then((m) => m.html),
|
||||
title: 'Creators can now make money on Modrinth!',
|
||||
summary:
|
||||
'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.',
|
||||
date: '2022-11-12T00:00:00.000Z',
|
||||
slug: 'creator-monetization',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./creator_update.content`).then((m) => m.html),
|
||||
title: 'Creator Update: Analytics, Organizations, Collections, and more',
|
||||
summary: 'December may be over, but we’re not done giving gifts.',
|
||||
date: '2024-01-06T20:00:00.000Z',
|
||||
slug: 'creator-update',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
short_title: 'The Creator Update',
|
||||
short_summary: 'Adding analytics, orgs, collections, and more!',
|
||||
html: () => import(`./creator_update.content`).then((m) => m.html),
|
||||
title: 'Creator Update: Analytics, Organizations, Collections, and more',
|
||||
summary: 'December may be over, but we’re not done giving gifts.',
|
||||
date: '2024-01-06T20:00:00.000Z',
|
||||
slug: 'creator-update',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
short_title: 'The Creator Update',
|
||||
short_summary: 'Adding analytics, orgs, collections, and more!',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./creator_updates_july_2025.content`).then((m) => m.html),
|
||||
title: 'Creator Updates, July 2025',
|
||||
summary: 'Addressing recent growth and growing pains that have been affecting creators.',
|
||||
date: '2025-07-02T04:20:00.000Z',
|
||||
slug: 'creator-updates-july-2025',
|
||||
authors: ['MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
html: () => import(`./creator_updates_july_2025.content`).then((m) => m.html),
|
||||
title: 'Creator Updates, July 2025',
|
||||
summary: 'Addressing recent growth and growing pains that have been affecting creators.',
|
||||
date: '2025-07-02T04:20:00.000Z',
|
||||
slug: 'creator-updates-july-2025',
|
||||
authors: ['MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./design_refresh.content`).then((m) => m.html),
|
||||
title: 'Introducing Modrinth+, a refreshed site look, and a new advertising system!',
|
||||
summary: 'Learn about this major update to Modrinth.',
|
||||
date: '2024-08-21T20:00:00.000Z',
|
||||
slug: 'design-refresh',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Modrinth+ and New Ads',
|
||||
short_summary:
|
||||
'Introducing a new ad system, a subscription to remove ads, and a redesign of the website!',
|
||||
html: () => import(`./design_refresh.content`).then((m) => m.html),
|
||||
title: 'Introducing Modrinth+, a refreshed site look, and a new advertising system!',
|
||||
summary: 'Learn about this major update to Modrinth.',
|
||||
date: '2024-08-21T20:00:00.000Z',
|
||||
slug: 'design-refresh',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Modrinth+ and New Ads',
|
||||
short_summary:
|
||||
'Introducing a new ad system, a subscription to remove ads, and a redesign of the website!',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./download_adjustment.content`).then((m) => m.html),
|
||||
title: 'Correcting Inflated Download Counts due to Rate Limiting Issue',
|
||||
summary: 'A rate limiting issue caused inflated download counts in certain countries.',
|
||||
date: '2023-11-10T20:00:00.000Z',
|
||||
slug: 'download-adjustment',
|
||||
authors: ['6plzAzU4', 'MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
short_title: 'Correcting Inflated Download Counts',
|
||||
html: () => import(`./download_adjustment.content`).then((m) => m.html),
|
||||
title: 'Correcting Inflated Download Counts due to Rate Limiting Issue',
|
||||
summary: 'A rate limiting issue caused inflated download counts in certain countries.',
|
||||
date: '2023-11-10T20:00:00.000Z',
|
||||
slug: 'download-adjustment',
|
||||
authors: ['6plzAzU4', 'MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
short_title: 'Correcting Inflated Download Counts',
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
import { article as windows_borderless_malware_disclosure } from './windows_borderless_malware_disclosure'
|
||||
import { article as whats_modrinth } from './whats_modrinth'
|
||||
import { article as a_new_chapter_for_modrinth_servers } from './a_new_chapter_for_modrinth_servers'
|
||||
import { article as accelerating_development } from './accelerating_development'
|
||||
import { article as becoming_sustainable } from './becoming_sustainable'
|
||||
import { article as capital_return } from './capital_return'
|
||||
import { article as carbon_ads } from './carbon_ads'
|
||||
import { article as creator_monetization } from './creator_monetization'
|
||||
import { article as creator_update } from './creator_update'
|
||||
import { article as creator_updates_july_2025 } from './creator_updates_july_2025'
|
||||
import { article as design_refresh } from './design_refresh'
|
||||
import { article as download_adjustment } from './download_adjustment'
|
||||
import { article as knossos_v2_1_0 } from './knossos_v2_1_0'
|
||||
import { article as licensing_guide } from './licensing_guide'
|
||||
import { article as modpack_changes } from './modpack_changes'
|
||||
import { article as modpacks_alpha } from './modpacks_alpha'
|
||||
import { article as modrinth_app_beta } from './modrinth_app_beta'
|
||||
import { article as modrinth_beta } from './modrinth_beta'
|
||||
import { article as modrinth_servers_beta } from './modrinth_servers_beta'
|
||||
import { article as new_site_beta } from './new_site_beta'
|
||||
import { article as plugins_resource_packs } from './plugins_resource_packs'
|
||||
import { article as pride_campaign_2025 } from './pride_campaign_2025'
|
||||
import { article as redesign } from './redesign'
|
||||
import { article as skins_now_in_modrinth_app } from './skins_now_in_modrinth_app'
|
||||
import { article as two_years_of_modrinth } from './two_years_of_modrinth'
|
||||
import { article as two_years_of_modrinth_history } from './two_years_of_modrinth_history'
|
||||
import { article as skins_now_in_modrinth_app } from './skins_now_in_modrinth_app'
|
||||
import { article as redesign } from './redesign'
|
||||
import { article as pride_campaign_2025 } from './pride_campaign_2025'
|
||||
import { article as plugins_resource_packs } from './plugins_resource_packs'
|
||||
import { article as new_site_beta } from './new_site_beta'
|
||||
import { article as modrinth_servers_beta } from './modrinth_servers_beta'
|
||||
import { article as modrinth_beta } from './modrinth_beta'
|
||||
import { article as modrinth_app_beta } from './modrinth_app_beta'
|
||||
import { article as modpacks_alpha } from './modpacks_alpha'
|
||||
import { article as modpack_changes } from './modpack_changes'
|
||||
import { article as licensing_guide } from './licensing_guide'
|
||||
import { article as knossos_v2_1_0 } from './knossos_v2_1_0'
|
||||
import { article as download_adjustment } from './download_adjustment'
|
||||
import { article as design_refresh } from './design_refresh'
|
||||
import { article as creator_updates_july_2025 } from './creator_updates_july_2025'
|
||||
import { article as creator_update } from './creator_update'
|
||||
import { article as creator_monetization } from './creator_monetization'
|
||||
import { article as carbon_ads } from './carbon_ads'
|
||||
import { article as capital_return } from './capital_return'
|
||||
import { article as becoming_sustainable } from './becoming_sustainable'
|
||||
import { article as accelerating_development } from './accelerating_development'
|
||||
import { article as a_new_chapter_for_modrinth_servers } from './a_new_chapter_for_modrinth_servers'
|
||||
import { article as whats_modrinth } from './whats_modrinth'
|
||||
import { article as windows_borderless_malware_disclosure } from './windows_borderless_malware_disclosure'
|
||||
|
||||
export const articles = [
|
||||
windows_borderless_malware_disclosure,
|
||||
whats_modrinth,
|
||||
two_years_of_modrinth,
|
||||
two_years_of_modrinth_history,
|
||||
skins_now_in_modrinth_app,
|
||||
redesign,
|
||||
pride_campaign_2025,
|
||||
plugins_resource_packs,
|
||||
new_site_beta,
|
||||
modrinth_servers_beta,
|
||||
modrinth_beta,
|
||||
modrinth_app_beta,
|
||||
modpacks_alpha,
|
||||
modpack_changes,
|
||||
licensing_guide,
|
||||
knossos_v2_1_0,
|
||||
download_adjustment,
|
||||
design_refresh,
|
||||
creator_updates_july_2025,
|
||||
creator_update,
|
||||
creator_monetization,
|
||||
carbon_ads,
|
||||
capital_return,
|
||||
becoming_sustainable,
|
||||
accelerating_development,
|
||||
a_new_chapter_for_modrinth_servers,
|
||||
windows_borderless_malware_disclosure,
|
||||
whats_modrinth,
|
||||
two_years_of_modrinth,
|
||||
two_years_of_modrinth_history,
|
||||
skins_now_in_modrinth_app,
|
||||
redesign,
|
||||
pride_campaign_2025,
|
||||
plugins_resource_packs,
|
||||
new_site_beta,
|
||||
modrinth_servers_beta,
|
||||
modrinth_beta,
|
||||
modrinth_app_beta,
|
||||
modpacks_alpha,
|
||||
modpack_changes,
|
||||
licensing_guide,
|
||||
knossos_v2_1_0,
|
||||
download_adjustment,
|
||||
design_refresh,
|
||||
creator_updates_july_2025,
|
||||
creator_update,
|
||||
creator_monetization,
|
||||
carbon_ads,
|
||||
capital_return,
|
||||
becoming_sustainable,
|
||||
accelerating_development,
|
||||
a_new_chapter_for_modrinth_servers,
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./knossos_v2_1_0.content`).then((m) => m.html),
|
||||
title: 'This week in Modrinth development: Filters and Fixes',
|
||||
summary:
|
||||
'Continuing to improve the user interface after a great first week since Modrinth launched out of beta.',
|
||||
date: '2022-03-09T00:00:00.000Z',
|
||||
slug: 'knossos-v2.1.0',
|
||||
authors: ['Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./knossos_v2_1_0.content`).then((m) => m.html),
|
||||
title: 'This week in Modrinth development: Filters and Fixes',
|
||||
summary:
|
||||
'Continuing to improve the user interface after a great first week since Modrinth launched out of beta.',
|
||||
date: '2022-03-09T00:00:00.000Z',
|
||||
slug: 'knossos-v2.1.0',
|
||||
authors: ['Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./licensing_guide.content`).then((m) => m.html),
|
||||
title: "Beginner's Guide to Licensing your Mods",
|
||||
summary:
|
||||
"Software licenses; the nitty-gritty legal aspect of software development. They're more important than you think.",
|
||||
date: '2021-05-16T00:00:00.000Z',
|
||||
slug: 'licensing-guide',
|
||||
authors: ['6plzAzU4', 'aNd6VJql'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./licensing_guide.content`).then((m) => m.html),
|
||||
title: "Beginner's Guide to Licensing your Mods",
|
||||
summary:
|
||||
"Software licenses; the nitty-gritty legal aspect of software development. They're more important than you think.",
|
||||
date: '2021-05-16T00:00:00.000Z',
|
||||
slug: 'licensing-guide',
|
||||
authors: ['6plzAzU4', 'aNd6VJql'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./modpack_changes.content`).then((m) => m.html),
|
||||
title: 'Changes to Modrinth Modpacks',
|
||||
summary: 'CurseForge CDN links requested to be removed by the end of the month',
|
||||
date: '2022-05-28T00:00:00.000Z',
|
||||
slug: 'modpack-changes',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./modpack_changes.content`).then((m) => m.html),
|
||||
title: 'Changes to Modrinth Modpacks',
|
||||
summary: 'CurseForge CDN links requested to be removed by the end of the month',
|
||||
date: '2022-05-28T00:00:00.000Z',
|
||||
slug: 'modpack-changes',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./modpacks_alpha.content`).then((m) => m.html),
|
||||
title: 'Modrinth Modpacks: Now in alpha testing',
|
||||
summary:
|
||||
"After over a year of development, we're happy to announce that modpack support is now in alpha testing.",
|
||||
date: '2022-05-15T00:00:00.000Z',
|
||||
slug: 'modpacks-alpha',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./modpacks_alpha.content`).then((m) => m.html),
|
||||
title: 'Modrinth Modpacks: Now in alpha testing',
|
||||
summary:
|
||||
"After over a year of development, we're happy to announce that modpack support is now in alpha testing.",
|
||||
date: '2022-05-15T00:00:00.000Z',
|
||||
slug: 'modpacks-alpha',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./modrinth_app_beta.content`).then((m) => m.html),
|
||||
title: 'Introducing Modrinth App Beta',
|
||||
summary:
|
||||
'Changing the modded Minecraft landscape with the new Modrinth App, alongside several other major features.',
|
||||
date: '2023-08-05T20:00:00.000Z',
|
||||
slug: 'modrinth-app-beta',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: false,
|
||||
short_title: 'Modrinth App Beta and Upgraded Authentication',
|
||||
short_summary: 'Launching Modrinth App Beta and upgrading authentication.',
|
||||
html: () => import(`./modrinth_app_beta.content`).then((m) => m.html),
|
||||
title: 'Introducing Modrinth App Beta',
|
||||
summary:
|
||||
'Changing the modded Minecraft landscape with the new Modrinth App, alongside several other major features.',
|
||||
date: '2023-08-05T20:00:00.000Z',
|
||||
slug: 'modrinth-app-beta',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: false,
|
||||
short_title: 'Modrinth App Beta and Upgraded Authentication',
|
||||
short_summary: 'Launching Modrinth App Beta and upgrading authentication.',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./modrinth_beta.content`).then((m) => m.html),
|
||||
title: 'Welcome to Modrinth Beta',
|
||||
summary:
|
||||
'After six months of work, Modrinth enters Beta, helping modders host their mods with ease!',
|
||||
date: '2020-12-01T00:00:00.000Z',
|
||||
slug: 'modrinth-beta',
|
||||
authors: ['Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./modrinth_beta.content`).then((m) => m.html),
|
||||
title: 'Welcome to Modrinth Beta',
|
||||
summary:
|
||||
'After six months of work, Modrinth enters Beta, helping modders host their mods with ease!',
|
||||
date: '2020-12-01T00:00:00.000Z',
|
||||
slug: 'modrinth-beta',
|
||||
authors: ['Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./modrinth_servers_beta.content`).then((m) => m.html),
|
||||
title: 'Host your own server with Modrinth Servers — now in beta',
|
||||
summary: 'Fast, simple, reliable servers directly integrated into Modrinth.',
|
||||
date: '2024-11-03T06:00:00.000Z',
|
||||
slug: 'modrinth-servers-beta',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Introducing Modrinth Servers',
|
||||
short_summary: 'Host your next Minecraft server with Modrinth.',
|
||||
html: () => import(`./modrinth_servers_beta.content`).then((m) => m.html),
|
||||
title: 'Host your own server with Modrinth Servers — now in beta',
|
||||
summary: 'Fast, simple, reliable servers directly integrated into Modrinth.',
|
||||
date: '2024-11-03T06:00:00.000Z',
|
||||
slug: 'modrinth-servers-beta',
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
short_title: 'Introducing Modrinth Servers',
|
||||
short_summary: 'Host your next Minecraft server with Modrinth.',
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./new_site_beta.content`).then((m) => m.html),
|
||||
title: '(April Fools 2023) Powering up your experience: Modrinth Technologies™️ beta launch!',
|
||||
summary: "Welcome to the new era of Modrinth. We can't wait to hear your feedback.",
|
||||
date: '2023-04-01T08:00:00.000Z',
|
||||
slug: 'new-site-beta',
|
||||
authors: [],
|
||||
thumbnail: true,
|
||||
short_title: '(April Fools 2023) Modrinth Technologies™️ beta launch!',
|
||||
short_summary: 'Power up your experience.',
|
||||
html: () => import(`./new_site_beta.content`).then((m) => m.html),
|
||||
title: '(April Fools 2023) Powering up your experience: Modrinth Technologies™️ beta launch!',
|
||||
summary: "Welcome to the new era of Modrinth. We can't wait to hear your feedback.",
|
||||
date: '2023-04-01T08:00:00.000Z',
|
||||
slug: 'new-site-beta',
|
||||
authors: [],
|
||||
thumbnail: true,
|
||||
short_title: '(April Fools 2023) Modrinth Technologies™️ beta launch!',
|
||||
short_summary: 'Power up your experience.',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./plugins_resource_packs.content`).then((m) => m.html),
|
||||
title: 'Plugins and Resource Packs now have a home on Modrinth',
|
||||
summary:
|
||||
'A small update with a big impact: plugins and resource packs are now available on Modrinth!',
|
||||
date: '2022-08-27T00:00:00.000Z',
|
||||
slug: 'plugins-resource-packs',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./plugins_resource_packs.content`).then((m) => m.html),
|
||||
title: 'Plugins and Resource Packs now have a home on Modrinth',
|
||||
summary:
|
||||
'A small update with a big impact: plugins and resource packs are now available on Modrinth!',
|
||||
date: '2022-08-27T00:00:00.000Z',
|
||||
slug: 'plugins-resource-packs',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./pride_campaign_2025.content`).then((m) => m.html),
|
||||
title: 'A Pride Month Success: Over $8,400 Raised for The Trevor Project!',
|
||||
summary: 'Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.',
|
||||
date: '2025-07-01T18:00:00.000Z',
|
||||
slug: 'pride-campaign-2025',
|
||||
authors: ['6plzAzU4', 'bOHH0P9Z', '2cqK8Q5p', 'vNcGR3Fd'],
|
||||
thumbnail: true,
|
||||
short_title: 'Pride Month Fundraiser 2025',
|
||||
short_summary: 'A reflection on our Pride Month fundraiser campaign.',
|
||||
html: () => import(`./pride_campaign_2025.content`).then((m) => m.html),
|
||||
title: 'A Pride Month Success: Over $8,400 Raised for The Trevor Project!',
|
||||
summary: 'Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.',
|
||||
date: '2025-07-01T18:00:00.000Z',
|
||||
slug: 'pride-campaign-2025',
|
||||
authors: ['6plzAzU4', 'bOHH0P9Z', '2cqK8Q5p', 'vNcGR3Fd'],
|
||||
thumbnail: true,
|
||||
short_title: 'Pride Month Fundraiser 2025',
|
||||
short_summary: 'A reflection on our Pride Month fundraiser campaign.',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./redesign.content`).then((m) => m.html),
|
||||
title: 'Now showing on Modrinth: A new look!',
|
||||
summary: 'Releasing many new features and improvements, including a redesign!',
|
||||
date: '2022-02-27T00:00:00.000Z',
|
||||
slug: 'redesign',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./redesign.content`).then((m) => m.html),
|
||||
title: 'Now showing on Modrinth: A new look!',
|
||||
summary: 'Releasing many new features and improvements, including a redesign!',
|
||||
date: '2022-02-27T00:00:00.000Z',
|
||||
slug: 'redesign',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./skins_now_in_modrinth_app.content`).then((m) => m.html),
|
||||
title: 'Skins — Now in Modrinth App!',
|
||||
summary:
|
||||
'Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.',
|
||||
date: '2025-07-06T23:45:00.000Z',
|
||||
slug: 'skins-now-in-modrinth-app',
|
||||
authors: ['bOHH0P9Z', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./skins_now_in_modrinth_app.content`).then((m) => m.html),
|
||||
title: 'Skins — Now in Modrinth App!',
|
||||
summary:
|
||||
'Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.',
|
||||
date: '2025-07-06T23:45:00.000Z',
|
||||
slug: 'skins-now-in-modrinth-app',
|
||||
authors: ['bOHH0P9Z', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./two_years_of_modrinth.content`).then((m) => m.html),
|
||||
title: "Modrinth's Anniversary Update",
|
||||
summary: "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./two_years_of_modrinth.content`).then((m) => m.html),
|
||||
title: "Modrinth's Anniversary Update",
|
||||
summary: "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./two_years_of_modrinth_history.content`).then((m) => m.html),
|
||||
title: 'Two years of Modrinth: a retrospective',
|
||||
summary: 'The history of Modrinth as we know it from December 2020 to December 2022.',
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth-history',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: false,
|
||||
html: () => import(`./two_years_of_modrinth_history.content`).then((m) => m.html),
|
||||
title: 'Two years of Modrinth: a retrospective',
|
||||
summary: 'The history of Modrinth as we know it from December 2020 to December 2022.',
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth-history',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./whats_modrinth.content`).then((m) => m.html),
|
||||
title: 'What is Modrinth?',
|
||||
summary:
|
||||
"Hello, we are Modrinth – an open source mods hosting platform. Sounds dry, doesn't it? So let me tell you our story – and I promise, it won't be boring!",
|
||||
date: '2020-11-27T00:00:00.000Z',
|
||||
slug: 'whats-modrinth',
|
||||
authors: ['aNd6VJql'],
|
||||
thumbnail: false,
|
||||
html: () => import(`./whats_modrinth.content`).then((m) => m.html),
|
||||
title: 'What is Modrinth?',
|
||||
summary:
|
||||
"Hello, we are Modrinth – an open source mods hosting platform. Sounds dry, doesn't it? So let me tell you our story – and I promise, it won't be boring!",
|
||||
date: '2020-11-27T00:00:00.000Z',
|
||||
slug: 'whats-modrinth',
|
||||
authors: ['aNd6VJql'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||
export const article = {
|
||||
html: () => import(`./windows_borderless_malware_disclosure.content`).then((m) => m.html),
|
||||
title: 'Malware Discovery Disclosure: "Windows Borderless" mod',
|
||||
summary: 'Threat Analysis and Plan of Action',
|
||||
date: '2024-05-07T20:00:00.000Z',
|
||||
slug: 'windows-borderless-malware-disclosure',
|
||||
authors: ['Dc7EYhxG', 'MpxzqsyW'],
|
||||
thumbnail: true,
|
||||
html: () => import(`./windows_borderless_malware_disclosure.content`).then((m) => m.html),
|
||||
title: 'Malware Discovery Disclosure: "Windows Borderless" mod',
|
||||
summary: 'Threat Analysis and Plan of Action',
|
||||
date: '2024-05-07T20:00:00.000Z',
|
||||
slug: 'windows-borderless-malware-disclosure',
|
||||
authors: ['Dc7EYhxG', 'MpxzqsyW'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
2
packages/blog/eslint.config.mjs
Normal file
2
packages/blog/eslint.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
@@ -1,29 +1,27 @@
|
||||
{
|
||||
"name": "@modrinth/blog",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "jiti ./check.ts && eslint . && prettier --check .",
|
||||
"fix": "jiti ./compile.ts && eslint . --fix && prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^9.0.0",
|
||||
"@types/html-minifier-terser": "^7.0.2",
|
||||
"@types/rss": "^0.0.32",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"jiti": "^2.4.2",
|
||||
"tsconfig": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"glob": "^10.2.7",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"rss": "^1.2.2",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
"name": "@modrinth/blog",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "jiti ./check.ts && eslint . && prettier --check .",
|
||||
"fix": "jiti ./compile.ts && eslint . --fix && prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@types/glob": "^9.0.0",
|
||||
"@types/html-minifier-terser": "^7.0.2",
|
||||
"@types/rss": "^0.0.32",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"jiti": "^2.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"glob": "^10.2.7",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"rss": "^1.2.2",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
{
|
||||
"extends": "tsconfig/base.json",
|
||||
"include": [".", ".eslintrc.js"],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom"],
|
||||
"noImplicitAny": false
|
||||
}
|
||||
"extends": "@modrinth/tooling-config/typescript/base.json"
|
||||
}
|
||||
|
||||
@@ -3,38 +3,38 @@ import * as path from 'path'
|
||||
|
||||
let REPO_ROOT_CACHE: string | null = null
|
||||
export function getRepoRoot(): string {
|
||||
if (REPO_ROOT_CACHE) return REPO_ROOT_CACHE
|
||||
return (REPO_ROOT_CACHE = execSync('git rev-parse --show-toplevel').toString().trim())
|
||||
if (REPO_ROOT_CACHE) return REPO_ROOT_CACHE
|
||||
return (REPO_ROOT_CACHE = execSync('git rev-parse --show-toplevel').toString().trim())
|
||||
}
|
||||
|
||||
export function repoPath(...segments: string[]): string {
|
||||
return path.posix.join(getRepoRoot(), ...segments)
|
||||
return path.posix.join(getRepoRoot(), ...segments)
|
||||
}
|
||||
|
||||
export async function copyDir(
|
||||
src: string,
|
||||
dest: string,
|
||||
logFn: (src: string, dest: string) => void = () => {},
|
||||
src: string,
|
||||
dest: string,
|
||||
logFn: (src: string, dest: string) => void = () => {},
|
||||
): Promise<void> {
|
||||
const { promises: fs } = await import('fs')
|
||||
await fs.mkdir(dest, { recursive: true })
|
||||
const entries = await fs.readdir(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.posix.join(src, entry.name)
|
||||
const destPath = path.posix.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await copyDir(srcPath, destPath, logFn)
|
||||
} else if (entry.isFile()) {
|
||||
await fs.copyFile(srcPath, destPath)
|
||||
logFn(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
const { promises: fs } = await import('fs')
|
||||
await fs.mkdir(dest, { recursive: true })
|
||||
const entries = await fs.readdir(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.posix.join(src, entry.name)
|
||||
const destPath = path.posix.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await copyDir(srcPath, destPath, logFn)
|
||||
} else if (entry.isFile()) {
|
||||
await fs.copyFile(srcPath, destPath)
|
||||
logFn(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toVarName(file: string): string {
|
||||
return file
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
.replace(/^_+/, '')
|
||||
.replace(/_+$/, '')
|
||||
return file
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
.replace(/^_+/, '')
|
||||
.replace(/_+$/, '')
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# `eslint-config-custom`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
@@ -1,27 +0,0 @@
|
||||
const { resolve } = require('node:path')
|
||||
|
||||
const project = resolve(process.cwd(), 'tsconfig.json')
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'turbo',
|
||||
],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules/', 'dist/'],
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
const { resolve } = require('node:path')
|
||||
|
||||
const project = resolve(process.cwd(), 'tsconfig.json')
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@nuxtjs/eslint-config-typescript',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'turbo',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['.nuxt/**', '.output/**', 'node_modules', 'dist/**'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'import/extensions': ['error', 'always', { ignorePackages: true }],
|
||||
},
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"license": "MIT",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^2.0.7",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
const { resolve } = require('node:path')
|
||||
|
||||
const project = resolve(process.cwd(), 'tsconfig.json')
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'turbo',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules/', 'dist/', '.eslintrc.js', '*.d.ts'],
|
||||
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
camelcase: 'off',
|
||||
'no-console': 'off',
|
||||
'no-bitwise': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
'vue/no-v-html': 'off',
|
||||
},
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['custom/library'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
}
|
||||
1
packages/moderation/.prettierignore
Normal file
1
packages/moderation/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
src/locales/**
|
||||
@@ -147,14 +147,14 @@ Text inputs can be conditionally shown based on selected actions:
|
||||
|
||||
```typescript
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Additional Information',
|
||||
variable: 'INFO',
|
||||
showWhen: {
|
||||
requiredActions: ['specific_action_id'],
|
||||
excludedActions: ['incompatible_action_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Additional Information',
|
||||
variable: 'INFO',
|
||||
showWhen: {
|
||||
requiredActions: ['specific_action_id'],
|
||||
excludedActions: ['incompatible_action_id'],
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
2
packages/moderation/eslint.config.mjs
Normal file
2
packages/moderation/eslint.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
@@ -1,23 +1,21 @@
|
||||
{
|
||||
"name": "@modrinth/moderation",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write . && pnpm run intl:extract",
|
||||
"intl:extract": "formatjs extract \"**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore \"node_modules/**/*\" --out-file src/locales/en-US/index.json --preserve-whitespace"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"tsconfig": "workspace:*"
|
||||
}
|
||||
"name": "@modrinth/moderation",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore \"node_modules/**/*\" --out-file src/locales/en-US/index.json --preserve-whitespace"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@modrinth/tooling-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import type { Stage } from '../types/stage'
|
||||
import modpackPermissionsStage from './modpack-permissions-stage'
|
||||
import categories from './stages/categories'
|
||||
import reupload from './stages/reupload'
|
||||
import description from './stages/description'
|
||||
import gallery from './stages/gallery'
|
||||
import license from './stages/license'
|
||||
import links from './stages/links'
|
||||
import reupload from './stages/reupload'
|
||||
import ruleFollowing from './stages/rule-following'
|
||||
import sideTypes from './stages/side-types'
|
||||
import statusAlerts from './stages/status-alerts'
|
||||
import summary from './stages/summary'
|
||||
import titleSlug from './stages/title-slug'
|
||||
import versions from './stages/versions'
|
||||
import license from './stages/license'
|
||||
import undefinedProject from './stages/undefined-project'
|
||||
import statusAlerts from './stages/status-alerts'
|
||||
import versions from './stages/versions'
|
||||
|
||||
export default [
|
||||
titleSlug,
|
||||
summary,
|
||||
description,
|
||||
links,
|
||||
license,
|
||||
categories,
|
||||
sideTypes,
|
||||
gallery,
|
||||
versions,
|
||||
reupload,
|
||||
ruleFollowing,
|
||||
modpackPermissionsStage,
|
||||
statusAlerts,
|
||||
undefinedProject,
|
||||
titleSlug,
|
||||
summary,
|
||||
description,
|
||||
links,
|
||||
license,
|
||||
categories,
|
||||
sideTypes,
|
||||
gallery,
|
||||
versions,
|
||||
reupload,
|
||||
ruleFollowing,
|
||||
modpackPermissionsStage,
|
||||
statusAlerts,
|
||||
undefinedProject,
|
||||
] as ReadonlyArray<Stage>
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import type { KeybindListener } from '../types/keybinds'
|
||||
|
||||
const keybinds: KeybindListener[] = [
|
||||
{
|
||||
id: 'next-stage',
|
||||
keybind: 'ArrowRight',
|
||||
description: 'Go to next stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoNext(),
|
||||
},
|
||||
{
|
||||
id: 'previous-stage',
|
||||
keybind: 'ArrowLeft',
|
||||
description: 'Go to previous stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoBack(),
|
||||
},
|
||||
{
|
||||
id: 'generate-message',
|
||||
keybind: 'Ctrl+Shift+E',
|
||||
description: 'Generate moderation message',
|
||||
action: (ctx) => ctx.actions.tryGenerateMessage(),
|
||||
},
|
||||
{
|
||||
id: 'toggle-collapse',
|
||||
keybind: 'Shift+C',
|
||||
description: 'Toggle collapse/expand',
|
||||
action: (ctx) => ctx.actions.tryToggleCollapse(),
|
||||
},
|
||||
{
|
||||
id: 'reset-progress',
|
||||
keybind: 'Ctrl+Shift+R',
|
||||
description: 'Reset moderation progress',
|
||||
action: (ctx) => ctx.actions.tryResetProgress(),
|
||||
},
|
||||
{
|
||||
id: 'skip-project',
|
||||
keybind: 'Ctrl+Shift+S',
|
||||
description: 'Skip to next project',
|
||||
enabled: (ctx) => ctx.state.futureProjectCount > 0 && !ctx.state.isDone,
|
||||
action: (ctx) => ctx.actions.trySkipProject(),
|
||||
},
|
||||
{
|
||||
id: 'next-stage',
|
||||
keybind: 'ArrowRight',
|
||||
description: 'Go to next stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoNext(),
|
||||
},
|
||||
{
|
||||
id: 'previous-stage',
|
||||
keybind: 'ArrowLeft',
|
||||
description: 'Go to previous stage',
|
||||
enabled: (ctx) => !ctx.state.isDone && !ctx.state.hasGeneratedMessage,
|
||||
action: (ctx) => ctx.actions.tryGoBack(),
|
||||
},
|
||||
{
|
||||
id: 'generate-message',
|
||||
keybind: 'Ctrl+Shift+E',
|
||||
description: 'Generate moderation message',
|
||||
action: (ctx) => ctx.actions.tryGenerateMessage(),
|
||||
},
|
||||
{
|
||||
id: 'toggle-collapse',
|
||||
keybind: 'Shift+C',
|
||||
description: 'Toggle collapse/expand',
|
||||
action: (ctx) => ctx.actions.tryToggleCollapse(),
|
||||
},
|
||||
{
|
||||
id: 'reset-progress',
|
||||
keybind: 'Ctrl+Shift+R',
|
||||
description: 'Reset moderation progress',
|
||||
action: (ctx) => ctx.actions.tryResetProgress(),
|
||||
},
|
||||
{
|
||||
id: 'skip-project',
|
||||
keybind: 'Ctrl+Shift+S',
|
||||
description: 'Skip to next project',
|
||||
enabled: (ctx) => ctx.state.futureProjectCount > 0 && !ctx.state.isDone,
|
||||
action: (ctx) => ctx.actions.trySkipProject(),
|
||||
},
|
||||
]
|
||||
|
||||
export default keybinds
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import type { ModerationModpackPermissionApprovalType, Project } from '@modrinth/utils'
|
||||
import type { Stage } from '../types/stage'
|
||||
import { PackageOpenIcon } from '@modrinth/assets'
|
||||
import type { ModerationModpackPermissionApprovalType, Project } from '@modrinth/utils'
|
||||
|
||||
import type { Stage } from '../types/stage'
|
||||
|
||||
export default {
|
||||
id: 'modpack-permissions',
|
||||
title: 'Modpack Permissions',
|
||||
icon: PackageOpenIcon,
|
||||
// Replace me please.
|
||||
guidance_url:
|
||||
'https://www.notion.so/Content-Moderation-Cheat-Sheets-22d5ee711bf081a4920ef08879fe6bf5?source=copy_link#22d5ee711bf08116bd8bc1186f357062',
|
||||
shouldShow: (project: Project) => project.project_type === 'modpack',
|
||||
actions: [
|
||||
{
|
||||
id: 'button',
|
||||
type: 'button',
|
||||
label: 'This dummy button must be present or the stage will not appear.',
|
||||
},
|
||||
],
|
||||
id: 'modpack-permissions',
|
||||
title: 'Modpack Permissions',
|
||||
icon: PackageOpenIcon,
|
||||
// Replace me please.
|
||||
guidance_url:
|
||||
'https://www.notion.so/Content-Moderation-Cheat-Sheets-22d5ee711bf081a4920ef08879fe6bf5?source=copy_link#22d5ee711bf08116bd8bc1186f357062',
|
||||
shouldShow: (project: Project) => project.project_type === 'modpack',
|
||||
actions: [
|
||||
{
|
||||
id: 'button',
|
||||
type: 'button',
|
||||
label: 'This dummy button must be present or the stage will not appear.',
|
||||
},
|
||||
],
|
||||
} as Stage
|
||||
|
||||
export const finalPermissionMessages: Record<
|
||||
ModerationModpackPermissionApprovalType['id'],
|
||||
string | undefined
|
||||
ModerationModpackPermissionApprovalType['id'],
|
||||
string | undefined
|
||||
> = {
|
||||
yes: undefined,
|
||||
'with-attribution-and-source': undefined,
|
||||
'with-attribution': `The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your Modpack's description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):`,
|
||||
no: 'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:',
|
||||
'permanent-no': `The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:`,
|
||||
unidentified: `The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:`,
|
||||
yes: undefined,
|
||||
'with-attribution-and-source': undefined,
|
||||
'with-attribution': `The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your Modpack's description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):`,
|
||||
no: 'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:',
|
||||
'permanent-no': `The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:`,
|
||||
unidentified: `The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:`,
|
||||
}
|
||||
|
||||
@@ -1,292 +1,293 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
|
||||
export const coreNags: Nag[] = [
|
||||
{
|
||||
id: 'moderator-feedback',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderator-feedback.title',
|
||||
defaultMessage: 'Review moderator feedback',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.moderator-feedback.description',
|
||||
defaultMessage:
|
||||
'Review and address all concerns from the moderation team before resubmitting.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.tags.rejectedStatuses.includes(context.project.status),
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderation.title',
|
||||
defaultMessage: 'Visit moderation thread',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-version',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-version.title',
|
||||
defaultMessage: 'Upload a version',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.upload-version.description',
|
||||
defaultMessage: 'At least one version is required for a project to be submitted for review.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: defineMessage({
|
||||
id: 'nags.versions.title',
|
||||
defaultMessage: 'Visit versions page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-description.title',
|
||||
defaultMessage: 'Add a description',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-description.description',
|
||||
defaultMessage:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.body === '',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.description.title',
|
||||
defaultMessage: 'Visit description settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-icon',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-icon.title',
|
||||
defaultMessage: 'Add an icon',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-icon.description',
|
||||
defaultMessage:
|
||||
'Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => !context.project.icon_url,
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-gallery-image.title',
|
||||
defaultMessage: 'Upload a gallery image',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const projectType = formatProjectType(context.project.project_type).toLowerCase()
|
||||
let msg = ''
|
||||
if (context.project.project_type === 'resourcepack') {
|
||||
msg =
|
||||
', except for audio or localization packs. If this describes your pack, please select the appropriate tag'
|
||||
}
|
||||
const resourcepackMessage = msg
|
||||
{
|
||||
id: 'moderator-feedback',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderator-feedback.title',
|
||||
defaultMessage: 'Review moderator feedback',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.moderator-feedback.description',
|
||||
defaultMessage:
|
||||
'Review and address all concerns from the moderation team before resubmitting.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.tags.rejectedStatuses.includes(context.project.status),
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: defineMessage({
|
||||
id: 'nags.moderation.title',
|
||||
defaultMessage: 'Visit moderation thread',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-version',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-version.title',
|
||||
defaultMessage: 'Upload a version',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.upload-version.description',
|
||||
defaultMessage: 'At least one version is required for a project to be submitted for review.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: defineMessage({
|
||||
id: 'nags.versions.title',
|
||||
defaultMessage: 'Visit versions page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-description.title',
|
||||
defaultMessage: 'Add a description',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-description.description',
|
||||
defaultMessage:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.body === '',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.description.title',
|
||||
defaultMessage: 'Visit description settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-icon',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-icon.title',
|
||||
defaultMessage: 'Add an icon',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-icon.description',
|
||||
defaultMessage:
|
||||
'Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => !context.project.icon_url,
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upload-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.upload-gallery-image.title',
|
||||
defaultMessage: 'Upload a gallery image',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const projectType = formatProjectType(context.project.project_type).toLowerCase()
|
||||
let msg = ''
|
||||
if (context.project.project_type === 'resourcepack') {
|
||||
msg =
|
||||
', except for audio or localization packs. If this describes your pack, please select the appropriate tag'
|
||||
}
|
||||
const resourcepackMessage = msg
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.upload-gallery-image.description',
|
||||
defaultMessage:
|
||||
'At least one gallery image is required to showcase the content of your {type}{resourcepackMessage}.',
|
||||
}),
|
||||
{
|
||||
type: projectType,
|
||||
resourcepackMessage: resourcepackMessage,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
(context.project.project_type === 'resourcepack' ||
|
||||
context.project.project_type === 'shader') &&
|
||||
(!context.project.gallery || context.project.gallery?.length === 0) &&
|
||||
!(
|
||||
context.project.categories.includes('audio') ||
|
||||
context.project.additional_categories.includes('audio') ||
|
||||
context.project.categories.includes('locale') ||
|
||||
context.project.additional_categories.includes('locale')
|
||||
)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'feature-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.feature-gallery-image.title',
|
||||
defaultMessage: 'Feature a gallery image',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.feature-gallery-image.description',
|
||||
defaultMessage:
|
||||
'The featured gallery image is often how your project makes its first impression.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
|
||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-tags.title',
|
||||
defaultMessage: 'Select tags',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.select-tags.description',
|
||||
defaultMessage:
|
||||
'Select the tags that correctly apply to your project to help the right users find it.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.project.versions.length > 0 && context.project.categories.length < 1,
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.tags.title',
|
||||
defaultMessage: 'Visit tag settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-links',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-links.title',
|
||||
defaultMessage: 'Add external links',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-links.description',
|
||||
defaultMessage:
|
||||
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) =>
|
||||
!(
|
||||
context.project.issues_url ||
|
||||
context.project.source_url ||
|
||||
context.project.wiki_url ||
|
||||
context.project.discord_url ||
|
||||
context.project.donation_urls.length > 0
|
||||
),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.links.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-environments',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-environments.title',
|
||||
defaultMessage: 'Select supported environments',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.upload-gallery-image.description',
|
||||
defaultMessage:
|
||||
'At least one gallery image is required to showcase the content of your {type}{resourcepackMessage}.',
|
||||
}),
|
||||
{
|
||||
type: projectType,
|
||||
resourcepackMessage: resourcepackMessage,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
(context.project.project_type === 'resourcepack' ||
|
||||
context.project.project_type === 'shader') &&
|
||||
(!context.project.gallery || context.project.gallery?.length === 0) &&
|
||||
!(
|
||||
context.project.categories.includes('audio') ||
|
||||
context.project.additional_categories.includes('audio') ||
|
||||
context.project.categories.includes('locale') ||
|
||||
context.project.additional_categories.includes('locale')
|
||||
)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'feature-gallery-image',
|
||||
title: defineMessage({
|
||||
id: 'nags.feature-gallery-image.title',
|
||||
defaultMessage: 'Feature a gallery image',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.feature-gallery-image.description',
|
||||
defaultMessage:
|
||||
'The featured gallery image is often how your project makes its first impression.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
|
||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
||||
},
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: defineMessage({
|
||||
id: 'nags.gallery.title',
|
||||
defaultMessage: 'Visit gallery page',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-tags.title',
|
||||
defaultMessage: 'Select tags',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.select-tags.description',
|
||||
defaultMessage:
|
||||
'Select the tags that correctly apply to your project to help the right users find it.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) =>
|
||||
context.project.versions.length > 0 && context.project.categories.length < 1,
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.tags.title',
|
||||
defaultMessage: 'Visit tag settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-links',
|
||||
title: defineMessage({
|
||||
id: 'nags.add-links.title',
|
||||
defaultMessage: 'Add external links',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.add-links.description',
|
||||
defaultMessage:
|
||||
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
|
||||
}),
|
||||
status: 'suggestion',
|
||||
shouldShow: (context: NagContext) =>
|
||||
!(
|
||||
context.project.issues_url ||
|
||||
context.project.source_url ||
|
||||
context.project.wiki_url ||
|
||||
context.project.discord_url ||
|
||||
context.project.donation_urls.length > 0
|
||||
),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.links.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-environments',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-environments.title',
|
||||
defaultMessage: 'Select supported environments',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-environments.description',
|
||||
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
||||
return (
|
||||
context.project.versions.length > 0 &&
|
||||
!excludedTypes.includes(context.project.project_type) &&
|
||||
(context.project.client_side === 'unknown' ||
|
||||
context.project.server_side === 'unknown' ||
|
||||
(context.project.client_side === 'unsupported' &&
|
||||
context.project.server_side === 'unsupported'))
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.environments.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-license',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-license.title',
|
||||
defaultMessage: 'Select a license',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-environments.description',
|
||||
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
||||
return (
|
||||
context.project.versions.length > 0 &&
|
||||
!excludedTypes.includes(context.project.project_type) &&
|
||||
(context.project.client_side === 'unknown' ||
|
||||
context.project.server_side === 'unknown' ||
|
||||
(context.project.client_side === 'unsupported' &&
|
||||
context.project.server_side === 'unsupported'))
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.environments.title',
|
||||
defaultMessage: 'Visit general settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'select-license',
|
||||
title: defineMessage({
|
||||
id: 'nags.select-license.title',
|
||||
defaultMessage: 'Select a license',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-license.description',
|
||||
defaultMessage: 'Select the license your {projectType} is distributed under.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.license.title',
|
||||
defaultMessage: 'Visit license settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.select-license.description',
|
||||
defaultMessage: 'Select the license your {projectType} is distributed under.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: defineMessage({
|
||||
id: 'nags.settings.license.title',
|
||||
defaultMessage: 'Visit license settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { renderHighlightedString } from '@modrinth/utils'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
export const MIN_DESCRIPTION_CHARS = 200
|
||||
export const MAX_HEADER_LENGTH = 80
|
||||
@@ -8,388 +9,388 @@ export const MIN_SUMMARY_CHARS = 30
|
||||
export const MIN_CHARS_PER_IMAGE = 60
|
||||
|
||||
export function analyzeHeaderLength(markdown: string): {
|
||||
hasLongHeaders: boolean
|
||||
longHeaders: string[]
|
||||
hasLongHeaders: boolean
|
||||
longHeaders: string[]
|
||||
} {
|
||||
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
|
||||
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
|
||||
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
|
||||
const headerRegex = /^(#{1,3})\s+(.+)$/gm
|
||||
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
|
||||
const headerRegex = /^(#{1,3})\s+(.+)$/gm
|
||||
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
|
||||
|
||||
const longHeaders: string[] = []
|
||||
const longHeaders: string[] = []
|
||||
|
||||
headers.forEach((match) => {
|
||||
const headerText = match[2].trim()
|
||||
const sentenceEnders = /[.!?]+/g
|
||||
const sentences = headerText.split(sentenceEnders).filter((s) => s.trim().length > 0)
|
||||
headers.forEach((match) => {
|
||||
const headerText = match[2].trim()
|
||||
const sentenceEnders = /[.!?]+/g
|
||||
const sentences = headerText.split(sentenceEnders).filter((s) => s.trim().length > 0)
|
||||
|
||||
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
|
||||
const hasMultipleSentences = sentences.length > 1
|
||||
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
|
||||
const hasMultipleSentences = sentences.length > 1
|
||||
|
||||
if (isVeryLong || hasMultipleSentences) {
|
||||
longHeaders.push(headerText)
|
||||
}
|
||||
})
|
||||
if (isVeryLong || hasMultipleSentences) {
|
||||
longHeaders.push(headerText)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
hasLongHeaders: longHeaders.length > 0,
|
||||
longHeaders,
|
||||
}
|
||||
return {
|
||||
hasLongHeaders: longHeaders.length > 0,
|
||||
longHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
export function analyzeImageContent(markdown: string): {
|
||||
imageHeavy: boolean
|
||||
hasEmptyAltText: boolean
|
||||
imageHeavy: boolean
|
||||
hasEmptyAltText: boolean
|
||||
} {
|
||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
|
||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
||||
|
||||
const htmlImageRegex = /<img[^>]*>/gi
|
||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
||||
const htmlImageRegex = /<img[^>]*>/gi
|
||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
||||
|
||||
const totalImages = images.length + htmlImages.length
|
||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
const totalImages = images.length + htmlImages.length
|
||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const textLength = countText(withoutCodeBlocks)
|
||||
const recommendedTextLength = MIN_CHARS_PER_IMAGE * totalImages
|
||||
const imageHeavy =
|
||||
recommendedTextLength > MIN_DESCRIPTION_CHARS && textLength < recommendedTextLength
|
||||
const textLength = countText(withoutCodeBlocks)
|
||||
const recommendedTextLength = MIN_CHARS_PER_IMAGE * totalImages
|
||||
const imageHeavy =
|
||||
recommendedTextLength > MIN_DESCRIPTION_CHARS && textLength < recommendedTextLength
|
||||
|
||||
const hasEmptyAltText =
|
||||
images.some((match) => !match[1]?.trim()) ||
|
||||
htmlImages.some((match) => {
|
||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
||||
return !altMatch || !altMatch[1]?.trim()
|
||||
})
|
||||
const hasEmptyAltText =
|
||||
images.some((match) => !match[1]?.trim()) ||
|
||||
htmlImages.some((match) => {
|
||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
||||
return !altMatch || !altMatch[1]?.trim()
|
||||
})
|
||||
|
||||
return { imageHeavy, hasEmptyAltText }
|
||||
return { imageHeavy, hasEmptyAltText }
|
||||
}
|
||||
|
||||
export function countText(markdown: string): number {
|
||||
if (!markdown) return 0
|
||||
if (!markdown) return 0
|
||||
|
||||
const fallback = (md: string): number => {
|
||||
const withoutCode = md.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
const withoutImagesAndLinks = withoutCode
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
.replace(/\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
const withoutHtml = withoutImagesAndLinks.replace(/<[^>]+>/g, ' ')
|
||||
const withoutMdSyntax = withoutHtml
|
||||
.replace(/^>{1}\s?.*$/gm, ' ')
|
||||
.replace(/^#{1,6}\s+/gm, ' ')
|
||||
.replace(/[*_~`>-]/g, ' ')
|
||||
.replace(/\|/g, ' ')
|
||||
return withoutMdSyntax.replace(/\s+/g, ' ').trim().length
|
||||
}
|
||||
const fallback = (md: string): number => {
|
||||
const withoutCode = md.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
const withoutImagesAndLinks = withoutCode
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
.replace(/\[[^\]]*]\([^)]+\)/g, ' ')
|
||||
const withoutHtml = withoutImagesAndLinks.replace(/<[^>]+>/g, ' ')
|
||||
const withoutMdSyntax = withoutHtml
|
||||
.replace(/^>{1}\s?.*$/gm, ' ')
|
||||
.replace(/^#{1,6}\s+/gm, ' ')
|
||||
.replace(/[*_~`>-]/g, ' ')
|
||||
.replace(/\|/g, ' ')
|
||||
return withoutMdSyntax.replace(/\s+/g, ' ').trim().length
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || typeof globalThis.DOMParser === 'undefined') {
|
||||
console.warn(`[Moderation] SSR: no window/DOMParser, falling back for countText`)
|
||||
return fallback(markdown)
|
||||
}
|
||||
if (typeof window === 'undefined' || typeof globalThis.DOMParser === 'undefined') {
|
||||
console.warn(`[Moderation] SSR: no window/DOMParser, falling back for countText`)
|
||||
return fallback(markdown)
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlString = renderHighlightedString(markdown)
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(htmlString, 'text/html')
|
||||
const walker = doc.createTreeWalker(doc.body || doc, NodeFilter.SHOW_TEXT)
|
||||
try {
|
||||
const htmlString = renderHighlightedString(markdown)
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(htmlString, 'text/html')
|
||||
const walker = doc.createTreeWalker(doc.body || doc, NodeFilter.SHOW_TEXT)
|
||||
|
||||
const textList: string[] = []
|
||||
let node = walker.nextNode()
|
||||
while (node) {
|
||||
if (node.textContent) textList.push(node.textContent)
|
||||
node = walker.nextNode()
|
||||
}
|
||||
return textList.join(' ').replace(/\s+/g, ' ').trim().length
|
||||
} catch {
|
||||
return fallback(markdown)
|
||||
}
|
||||
const textList: string[] = []
|
||||
let node = walker.nextNode()
|
||||
while (node) {
|
||||
if (node.textContent) textList.push(node.textContent)
|
||||
node = walker.nextNode()
|
||||
}
|
||||
return textList.join(' ').replace(/\s+/g, ' ').trim().length
|
||||
} catch {
|
||||
return fallback(markdown)
|
||||
}
|
||||
}
|
||||
|
||||
export const descriptionNags: Nag[] = [
|
||||
{
|
||||
id: 'description-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.description-too-short.title',
|
||||
defaultMessage: 'Expand the description',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const readableLength = countText(context.project.body || '')
|
||||
{
|
||||
id: 'description-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.description-too-short.title',
|
||||
defaultMessage: 'Expand the description',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const readableLength = countText(context.project.body || '')
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.description-too-short.description',
|
||||
defaultMessage:
|
||||
'Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative description.',
|
||||
}),
|
||||
{
|
||||
length: readableLength,
|
||||
minChars: MIN_DESCRIPTION_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const readableLength = countText(context.project.body || '')
|
||||
return readableLength < MIN_DESCRIPTION_CHARS && readableLength > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'long-headers',
|
||||
title: defineMessage({
|
||||
id: 'nags.long-headers.title',
|
||||
defaultMessage: 'Shorten headers',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
const count = longHeaders.length
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.description-too-short.description',
|
||||
defaultMessage:
|
||||
'Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative description.',
|
||||
}),
|
||||
{
|
||||
length: readableLength,
|
||||
minChars: MIN_DESCRIPTION_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const readableLength = countText(context.project.body || '')
|
||||
return readableLength < MIN_DESCRIPTION_CHARS && readableLength > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'long-headers',
|
||||
title: defineMessage({
|
||||
id: 'nags.long-headers.title',
|
||||
defaultMessage: 'Shorten headers',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
const count = longHeaders.length
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.long-headers.description',
|
||||
defaultMessage:
|
||||
'{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences.',
|
||||
}),
|
||||
{
|
||||
count,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
return hasLongHeaders
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-too-short.title',
|
||||
defaultMessage: 'Expand the summary',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.long-headers.description',
|
||||
defaultMessage:
|
||||
'{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences.',
|
||||
}),
|
||||
{
|
||||
count,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||
return hasLongHeaders
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-too-short',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-too-short.title',
|
||||
defaultMessage: 'Expand the summary',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.summary-too-short.description',
|
||||
defaultMessage:
|
||||
'Your summary is {length} characters. At least {minChars} characters is recommended to create an informative and enticing summary.',
|
||||
}),
|
||||
{
|
||||
length: context.project.description?.length || 0,
|
||||
minChars: MIN_SUMMARY_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summaryLength = context.project.description?.trim()?.length || 0
|
||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-special-formatting',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-special-formatting.title',
|
||||
defaultMessage: 'Clear up the summary',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-special-formatting.description',
|
||||
defaultMessage: `Your summary should not contain formatting, line breaks, special characters, or links, since the summary will only display plain text.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return Boolean(
|
||||
summary.match(/https:\/\//g) ||
|
||||
summary.match(/http:\/\//g) ||
|
||||
summary.match(/# .*/g) ||
|
||||
summary.match(/---/g) ||
|
||||
summary.match(/\n/g) ||
|
||||
summary.match(/\[.*\]\(.*\)/g) ||
|
||||
summary.match(/!\[.*\]/g) ||
|
||||
summary.match(/`.*`/g) ||
|
||||
summary.match(/\*.*\*/g) ||
|
||||
summary.match(/_.*_/g) ||
|
||||
summary.match(/~~.*~~/g) ||
|
||||
summary.match(/```/g) ||
|
||||
summary.match(/> /g),
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minecraft-title-clause',
|
||||
title: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.title',
|
||||
defaultMessage: 'Avoid brand infringement',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.description',
|
||||
defaultMessage: `Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the name.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
const wordsInTitle = title.split(' ').filter((word) => word.length > 0)
|
||||
return title.includes('minecraft') && title.length > 0 && wordsInTitle.length <= 3
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'title-contains-technical-info',
|
||||
title: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.title',
|
||||
defaultMessage: 'Clean up the name',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.description',
|
||||
defaultMessage:
|
||||
"Keeping your project's Name clean and makes it memorable easier to find. Version and loader information is automatically displayed alongside your project.",
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
if (!title) return false
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.summary-too-short.description',
|
||||
defaultMessage:
|
||||
'Your summary is {length} characters. At least {minChars} characters is recommended to create an informative and enticing summary.',
|
||||
}),
|
||||
{
|
||||
length: context.project.description?.length || 0,
|
||||
minChars: MIN_SUMMARY_CHARS,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summaryLength = context.project.description?.trim()?.length || 0
|
||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-special-formatting',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-special-formatting.title',
|
||||
defaultMessage: 'Clear up the summary',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-special-formatting.description',
|
||||
defaultMessage: `Your summary should not contain formatting, line breaks, special characters, or links, since the summary will only display plain text.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return Boolean(
|
||||
summary.match(/https:\/\//g) ||
|
||||
summary.match(/http:\/\//g) ||
|
||||
summary.match(/# .*/g) ||
|
||||
summary.match(/---/g) ||
|
||||
summary.match(/\n/g) ||
|
||||
summary.match(/\[.*\]\(.*\)/g) ||
|
||||
summary.match(/!\[.*\]/g) ||
|
||||
summary.match(/`.*`/g) ||
|
||||
summary.match(/\*.*\*/g) ||
|
||||
summary.match(/_.*_/g) ||
|
||||
summary.match(/~~.*~~/g) ||
|
||||
summary.match(/```/g) ||
|
||||
summary.match(/> /g),
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minecraft-title-clause',
|
||||
title: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.title',
|
||||
defaultMessage: 'Avoid brand infringement',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.minecraft-title-clause.description',
|
||||
defaultMessage: `Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the name.`,
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
const wordsInTitle = title.split(' ').filter((word) => word.length > 0)
|
||||
return title.includes('minecraft') && title.length > 0 && wordsInTitle.length <= 3
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'title-contains-technical-info',
|
||||
title: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.title',
|
||||
defaultMessage: 'Clean up the name',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.title-contains-technical-info.description',
|
||||
defaultMessage:
|
||||
"Keeping your project's Name clean and makes it memorable easier to find. Version and loader information is automatically displayed alongside your project.",
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.toLowerCase() || ''
|
||||
if (!title) return false
|
||||
|
||||
const loaderNames =
|
||||
context.tags.loaders?.map((loader: { name: string }) => loader.name?.toLowerCase()) || []
|
||||
const hasLoader = loaderNames.some((loader) => loader && title.includes(loader.toLowerCase()))
|
||||
const versionPatterns = [/\b1\.\d+(\.\d+)?\b/]
|
||||
const hasVersionPattern = versionPatterns.some((pattern) => pattern.test(title))
|
||||
const loaderNames =
|
||||
context.tags.loaders?.map((loader: { name: string }) => loader.name?.toLowerCase()) || []
|
||||
const hasLoader = loaderNames.some((loader) => loader && title.includes(loader.toLowerCase()))
|
||||
const versionPatterns = [/\b1\.\d+(\.\d+)?\b/]
|
||||
const hasVersionPattern = versionPatterns.some((pattern) => pattern.test(title))
|
||||
|
||||
return hasLoader || hasVersionPattern
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-same-as-title',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-same-as-title.title',
|
||||
defaultMessage: 'Make the summary unique',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-same-as-title.description',
|
||||
defaultMessage:
|
||||
"Your summary can not be the same as your project's Name. It's important to create an informative and enticing Summary.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.trim() || ''
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return title === summary && title.length > 0 && summary.length > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Don't like this one, is this needed?
|
||||
id: 'image-heavy-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.image-heavy-description.title',
|
||||
defaultMessage: 'Ensure accessibility',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.image-heavy-description.description',
|
||||
defaultMessage:
|
||||
'Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
||||
return imageHeavy
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'missing-alt-text',
|
||||
title: defineMessage({
|
||||
id: 'nags.missing-alt-text.title',
|
||||
defaultMessage: 'Add image alt text',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.missing-alt-text.description',
|
||||
defaultMessage:
|
||||
'Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
||||
return hasEmptyAltText
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
return hasLoader || hasVersionPattern
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-title.title',
|
||||
defaultMessage: 'Edit title',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-same-as-title',
|
||||
title: defineMessage({
|
||||
id: 'nags.summary-same-as-title.title',
|
||||
defaultMessage: 'Make the summary unique',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.summary-same-as-title.description',
|
||||
defaultMessage:
|
||||
"Your summary can not be the same as your project's Name. It's important to create an informative and enticing Summary.",
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const title = context.project.title?.trim() || ''
|
||||
const summary = context.project.description?.trim() || ''
|
||||
return title === summary && title.length > 0 && summary.length > 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-summary.title',
|
||||
defaultMessage: 'Edit summary',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Don't like this one, is this needed?
|
||||
id: 'image-heavy-description',
|
||||
title: defineMessage({
|
||||
id: 'nags.image-heavy-description.title',
|
||||
defaultMessage: 'Ensure accessibility',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.image-heavy-description.description',
|
||||
defaultMessage:
|
||||
'Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
||||
return imageHeavy
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'missing-alt-text',
|
||||
title: defineMessage({
|
||||
id: 'nags.missing-alt-text.title',
|
||||
defaultMessage: 'Add image alt text',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.missing-alt-text.description',
|
||||
defaultMessage:
|
||||
'Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
||||
return hasEmptyAltText
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-description.title',
|
||||
defaultMessage: 'Edit description',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './core'
|
||||
export * from './links'
|
||||
export * from './description'
|
||||
export * from './links'
|
||||
export * from './tags'
|
||||
|
||||
@@ -1,281 +1,282 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
|
||||
export const commonLinkDomains = {
|
||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'docs.google.com'],
|
||||
discord: ['discord.gg', 'discord.com', 'dsc.gg'],
|
||||
licenseBlocklist: [
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'modrinth.com',
|
||||
'curseforge.com',
|
||||
'twitter.com',
|
||||
'x.com',
|
||||
'discord.gg',
|
||||
'discord.com',
|
||||
'instagram.com',
|
||||
'facebook.com',
|
||||
'tiktok.com',
|
||||
'reddit.com',
|
||||
'twitch.tv',
|
||||
'patreon.com',
|
||||
'ko-fi.com',
|
||||
'paypal.com',
|
||||
'buymeacoffee.com',
|
||||
'google.com',
|
||||
'example.com',
|
||||
't.me',
|
||||
],
|
||||
linkShorteners: ['bit.ly', 'adf.ly', 'tinyurl.com', 'short.io', 'is.gd'],
|
||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'docs.google.com'],
|
||||
discord: ['discord.gg', 'discord.com', 'dsc.gg'],
|
||||
licenseBlocklist: [
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'modrinth.com',
|
||||
'curseforge.com',
|
||||
'twitter.com',
|
||||
'x.com',
|
||||
'discord.gg',
|
||||
'discord.com',
|
||||
'instagram.com',
|
||||
'facebook.com',
|
||||
'tiktok.com',
|
||||
'reddit.com',
|
||||
'twitch.tv',
|
||||
'patreon.com',
|
||||
'ko-fi.com',
|
||||
'paypal.com',
|
||||
'buymeacoffee.com',
|
||||
'google.com',
|
||||
'example.com',
|
||||
't.me',
|
||||
],
|
||||
linkShorteners: ['bit.ly', 'adf.ly', 'tinyurl.com', 'short.io', 'is.gd'],
|
||||
}
|
||||
|
||||
export function isCommonUrl(url: string | null, commonDomains: string[]): boolean {
|
||||
if (url === null || url === '') return true
|
||||
try {
|
||||
const domain = new URL(url).hostname.toLowerCase()
|
||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
if (url === null || url === '') return true
|
||||
try {
|
||||
const domain = new URL(url).hostname.toLowerCase()
|
||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isCommonUrlOfType(url: string | null, commonDomains: string[]): boolean {
|
||||
if (url === null || url === '') return false
|
||||
return isCommonUrl(url, commonDomains)
|
||||
if (url === null || url === '') return false
|
||||
return isCommonUrl(url, commonDomains)
|
||||
}
|
||||
|
||||
export function isDiscordUrl(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.discord)
|
||||
return isCommonUrlOfType(url, commonLinkDomains.discord)
|
||||
}
|
||||
|
||||
export function isLinkShortener(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.linkShorteners)
|
||||
return isCommonUrlOfType(url, commonLinkDomains.linkShorteners)
|
||||
}
|
||||
|
||||
export function isUncommonLicenseUrl(url: string | null): boolean {
|
||||
return isCommonUrlOfType(url, commonLinkDomains.licenseBlocklist)
|
||||
return isCommonUrlOfType(url, commonLinkDomains.licenseBlocklist)
|
||||
}
|
||||
|
||||
export const linksNags: Nag[] = [
|
||||
{
|
||||
id: 'verify-external-links',
|
||||
title: defineMessage({
|
||||
id: 'nags.verify-external-links.title',
|
||||
defaultMessage: 'Verify external links',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.verify-external-links.description',
|
||||
defaultMessage:
|
||||
'Some of your external links may be using domains that are inappropriate for that type of link.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
!isCommonUrl(context.project.source_url ?? null, commonLinkDomains.source) ||
|
||||
!isCommonUrl(context.project.issues_url ?? null, commonLinkDomains.issues) ||
|
||||
!isCommonUrl(context.project.discord_url ?? null, commonLinkDomains.discord)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'misused-discord-link',
|
||||
title: defineMessage({
|
||||
id: 'nags.misused-discord-link.title',
|
||||
defaultMessage: 'Move Discord invite',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.misused-discord-link-description',
|
||||
defaultMessage:
|
||||
'Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) =>
|
||||
isDiscordUrl(context.project.source_url ?? null) ||
|
||||
isDiscordUrl(context.project.issues_url ?? null) ||
|
||||
isDiscordUrl(context.project.wiki_url ?? null),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'link-shortener-usage',
|
||||
title: defineMessage({
|
||||
id: 'nags.link-shortener-usage.title',
|
||||
defaultMessage: "Don't use link shorteners",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.link-shortener-usage.description',
|
||||
defaultMessage:
|
||||
'Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.donation_urls) {
|
||||
for (const donation of context.project.donation_urls) {
|
||||
if (isLinkShortener(donation.url ?? null)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
id: 'verify-external-links',
|
||||
title: defineMessage({
|
||||
id: 'nags.verify-external-links.title',
|
||||
defaultMessage: 'Verify external links',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.verify-external-links.description',
|
||||
defaultMessage:
|
||||
'Some of your external links may be using domains that are inappropriate for that type of link.',
|
||||
}),
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
!isCommonUrl(context.project.source_url ?? null, commonLinkDomains.source) ||
|
||||
!isCommonUrl(context.project.issues_url ?? null, commonLinkDomains.issues) ||
|
||||
!isCommonUrl(context.project.discord_url ?? null, commonLinkDomains.discord)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'misused-discord-link',
|
||||
title: defineMessage({
|
||||
id: 'nags.misused-discord-link.title',
|
||||
defaultMessage: 'Move Discord invite',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.misused-discord-link-description',
|
||||
defaultMessage:
|
||||
'Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) =>
|
||||
isDiscordUrl(context.project.source_url ?? null) ||
|
||||
isDiscordUrl(context.project.issues_url ?? null) ||
|
||||
isDiscordUrl(context.project.wiki_url ?? null),
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'link-shortener-usage',
|
||||
title: defineMessage({
|
||||
id: 'nags.link-shortener-usage.title',
|
||||
defaultMessage: "Don't use link shorteners",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'nags.link-shortener-usage.description',
|
||||
defaultMessage:
|
||||
'Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links.',
|
||||
}),
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.donation_urls) {
|
||||
for (const donation of context.project.donation_urls) {
|
||||
if (isLinkShortener(donation.url ?? null)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
isLinkShortener(context.project.source_url ?? null) ||
|
||||
isLinkShortener(context.project.issues_url ?? null) ||
|
||||
isLinkShortener(context.project.wiki_url ?? null) ||
|
||||
isLinkShortener(context.project.discord_url ?? null) ||
|
||||
Boolean(context.project.license.url && isLinkShortener(context.project.license.url ?? null))
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'invalid-license-url',
|
||||
title: defineMessage({
|
||||
id: 'nags.invalid-license-url.title',
|
||||
defaultMessage: 'Add a valid license link',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const licenseUrl = context.project.license.url
|
||||
return (
|
||||
isLinkShortener(context.project.source_url ?? null) ||
|
||||
isLinkShortener(context.project.issues_url ?? null) ||
|
||||
isLinkShortener(context.project.wiki_url ?? null) ||
|
||||
isLinkShortener(context.project.discord_url ?? null) ||
|
||||
Boolean(context.project.license.url && isLinkShortener(context.project.license.url ?? null))
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'invalid-license-url',
|
||||
title: defineMessage({
|
||||
id: 'nags.invalid-license-url.title',
|
||||
defaultMessage: 'Add a valid license link',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const licenseUrl = context.project.license.url
|
||||
|
||||
if (!licenseUrl) {
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.default',
|
||||
defaultMessage: 'License URL is invalid.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (!licenseUrl) {
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.default',
|
||||
defaultMessage: 'License URL is invalid.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.domain',
|
||||
defaultMessage:
|
||||
'Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc.',
|
||||
}),
|
||||
{ domain },
|
||||
)
|
||||
} catch {
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.malformed',
|
||||
defaultMessage:
|
||||
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const licenseUrl = context.project.license.url
|
||||
if (!licenseUrl) return false
|
||||
try {
|
||||
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.domain',
|
||||
defaultMessage:
|
||||
'Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc.',
|
||||
}),
|
||||
{ domain },
|
||||
)
|
||||
} catch {
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.invalid-license-url.description.malformed',
|
||||
defaultMessage:
|
||||
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const licenseUrl = context.project.license.url
|
||||
if (!licenseUrl) return false
|
||||
|
||||
const isBlocklisted = isUncommonLicenseUrl(licenseUrl)
|
||||
const isBlocklisted = isUncommonLicenseUrl(licenseUrl)
|
||||
|
||||
try {
|
||||
new URL(licenseUrl)
|
||||
return isBlocklisted
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-license.title',
|
||||
defaultMessage: 'Edit license',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gpl-license-source-required',
|
||||
title: defineMessage({
|
||||
id: 'nags.gpl-license-source-required.title',
|
||||
defaultMessage: 'Provide source code',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
try {
|
||||
new URL(licenseUrl)
|
||||
return isBlocklisted
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-license.title',
|
||||
defaultMessage: 'Edit license',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gpl-license-source-required',
|
||||
title: defineMessage({
|
||||
id: 'nags.gpl-license-source-required.title',
|
||||
defaultMessage: 'Provide source code',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.gpl-license-source-required.description',
|
||||
defaultMessage:
|
||||
'Your {projectType} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const gplLicenses = [
|
||||
'GPL-2.0',
|
||||
'GPL-2.0+',
|
||||
'GPL-2.0-only',
|
||||
'GPL-2.0-or-later',
|
||||
'GPL-3.0',
|
||||
'GPL-3.0+',
|
||||
'GPL-3.0-only',
|
||||
'GPL-3.0-or-later',
|
||||
'LGPL-2.1',
|
||||
'LGPL-2.1+',
|
||||
'LGPL-2.1-only',
|
||||
'LGPL-2.1-or-later',
|
||||
'LGPL-3.0',
|
||||
'LGPL-3.0+',
|
||||
'LGPL-3.0-only',
|
||||
'LGPL-3.0-or-later',
|
||||
'AGPL-3.0',
|
||||
'AGPL-3.0+',
|
||||
'AGPL-3.0-only',
|
||||
'AGPL-3.0-or-later',
|
||||
'MPL-2.0',
|
||||
]
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.gpl-license-source-required.description',
|
||||
defaultMessage:
|
||||
'Your {projectType} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license.',
|
||||
}),
|
||||
{
|
||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const gplLicenses = [
|
||||
'GPL-2.0',
|
||||
'GPL-2.0+',
|
||||
'GPL-2.0-only',
|
||||
'GPL-2.0-or-later',
|
||||
'GPL-3.0',
|
||||
'GPL-3.0+',
|
||||
'GPL-3.0-only',
|
||||
'GPL-3.0-or-later',
|
||||
'LGPL-2.1',
|
||||
'LGPL-2.1+',
|
||||
'LGPL-2.1-only',
|
||||
'LGPL-2.1-or-later',
|
||||
'LGPL-3.0',
|
||||
'LGPL-3.0+',
|
||||
'LGPL-3.0-only',
|
||||
'LGPL-3.0-or-later',
|
||||
'AGPL-3.0',
|
||||
'AGPL-3.0+',
|
||||
'AGPL-3.0-only',
|
||||
'AGPL-3.0-or-later',
|
||||
'MPL-2.0',
|
||||
]
|
||||
|
||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
||||
const hasSourceUrl = !!context.project.source_url
|
||||
const hasAdditionalFiles = (context: NagContext) => {
|
||||
let hasAdditional = true
|
||||
context.versions.forEach((version) => {
|
||||
if (version.files.length < 2) hasAdditional = false
|
||||
})
|
||||
return hasAdditional
|
||||
}
|
||||
const notSourceAsDistributed = (context: NagContext) =>
|
||||
context.project.project_type === 'mod' || context.project.project_type === 'plugin'
|
||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
||||
const hasSourceUrl = !!context.project.source_url
|
||||
const hasAdditionalFiles = (context: NagContext) => {
|
||||
let hasAdditional = true
|
||||
context.versions.forEach((version) => {
|
||||
if (version.files.length < 2) hasAdditional = false
|
||||
})
|
||||
return hasAdditional
|
||||
}
|
||||
const notSourceAsDistributed = (context: NagContext) =>
|
||||
context.project.project_type === 'mod' || context.project.project_type === 'plugin'
|
||||
|
||||
return (
|
||||
isGplLicense &&
|
||||
notSourceAsDistributed(context) &&
|
||||
!hasSourceUrl &&
|
||||
!hasAdditionalFiles(context)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
return (
|
||||
isGplLicense &&
|
||||
notSourceAsDistributed(context) &&
|
||||
!hasSourceUrl &&
|
||||
!hasAdditionalFiles(context)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: defineMessage({
|
||||
id: 'nags.visit-links-settings.title',
|
||||
defaultMessage: 'Visit links settings',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,160 +1,161 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
|
||||
const allResolutionTags = ['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+']
|
||||
|
||||
const MAX_TAG_COUNT = 8
|
||||
|
||||
function getCategories(
|
||||
project: Project & { actualProjectType: string },
|
||||
tags: {
|
||||
categories?: {
|
||||
project_type: string
|
||||
}[]
|
||||
},
|
||||
project: Project & { actualProjectType: string },
|
||||
tags: {
|
||||
categories?: {
|
||||
project_type: string
|
||||
}[]
|
||||
},
|
||||
) {
|
||||
return (
|
||||
tags.categories?.filter(
|
||||
(category: { project_type: string }) => category.project_type === project.actualProjectType,
|
||||
) ?? []
|
||||
)
|
||||
return (
|
||||
tags.categories?.filter(
|
||||
(category: { project_type: string }) => category.project_type === project.actualProjectType,
|
||||
) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
export const tagsNags: Nag[] = [
|
||||
{
|
||||
id: 'too-many-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.too-many-tags.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
const maxTagCount = MAX_TAG_COUNT
|
||||
{
|
||||
id: 'too-many-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.too-many-tags.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
const maxTagCount = MAX_TAG_COUNT
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.too-many-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {tagCount} tags. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results.",
|
||||
}),
|
||||
{
|
||||
tagCount,
|
||||
maxTagCount,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return tagCount > MAX_TAG_COUNT
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'multiple-resolution-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.title',
|
||||
defaultMessage: 'Select correct resolution',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.too-many-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {tagCount} tags. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results.",
|
||||
}),
|
||||
{
|
||||
tagCount,
|
||||
maxTagCount,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const tagCount =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return tagCount > MAX_TAG_COUNT
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'multiple-resolution-tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.title',
|
||||
defaultMessage: 'Select correct resolution',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
|
||||
const sortedTags = resolutionTags.toSorted((a, b) => {
|
||||
return allResolutionTags.indexOf(a) - allResolutionTags.indexOf(b)
|
||||
})
|
||||
const sortedTags = resolutionTags.toSorted((a, b) => {
|
||||
return allResolutionTags.indexOf(a) - allResolutionTags.indexOf(b)
|
||||
})
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution.",
|
||||
}),
|
||||
{
|
||||
count: resolutionTags.length,
|
||||
tags: sortedTags
|
||||
.join(', ')
|
||||
.replace('8x-', '8x or lower')
|
||||
.replace('512x+', '512x or higher'),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.project_type !== 'resourcepack') return false
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.multiple-resolution-tags.description',
|
||||
defaultMessage:
|
||||
"You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution.",
|
||||
}),
|
||||
{
|
||||
count: resolutionTags.length,
|
||||
tags: sortedTags
|
||||
.join(', ')
|
||||
.replace('8x-', '8x or lower')
|
||||
.replace('512x+', '512x or higher'),
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
if (context.project.project_type !== 'resourcepack') return false
|
||||
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
return resolutionTags.length > 1
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'all-tags-selected',
|
||||
title: defineMessage({
|
||||
id: 'nags.all-tags-selected.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalAvailableTags = categoriesForProjectType.length
|
||||
const resolutionTags = context.project.categories
|
||||
.concat(context.project.additional_categories)
|
||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
||||
return resolutionTags.length > 1
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'all-tags-selected',
|
||||
title: defineMessage({
|
||||
id: 'nags.all-tags-selected.title',
|
||||
defaultMessage: 'Select accurate tags',
|
||||
}),
|
||||
description: (context: NagContext) => {
|
||||
const { formatMessage } = useVIntl()
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalAvailableTags = categoriesForProjectType.length
|
||||
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.all-tags-selected.description',
|
||||
defaultMessage:
|
||||
"You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project.",
|
||||
}),
|
||||
{
|
||||
totalAvailableTags,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalSelectedTags =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return (
|
||||
totalSelectedTags === categoriesForProjectType.length &&
|
||||
context.project.project_type !== 'project'
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'nags.all-tags-selected.description',
|
||||
defaultMessage:
|
||||
"You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project.",
|
||||
}),
|
||||
{
|
||||
totalAvailableTags,
|
||||
},
|
||||
)
|
||||
},
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const categoriesForProjectType = getCategories(
|
||||
context.project as Project & { actualProjectType: string },
|
||||
context.tags,
|
||||
)
|
||||
const totalSelectedTags =
|
||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||
return (
|
||||
totalSelectedTags === categoriesForProjectType.length &&
|
||||
context.project.project_type !== 'project'
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: defineMessage({
|
||||
id: 'nags.edit-tags.title',
|
||||
defaultMessage: 'Edit tags',
|
||||
}),
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import type { ReportQuickReply } from '../types/reports'
|
||||
|
||||
export default [
|
||||
{
|
||||
label: 'Antivirus',
|
||||
message: async () => (await import('./messages/reports/antivirus.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Spam',
|
||||
message: async () => (await import('./messages/reports/spam.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Gameplay Issue',
|
||||
message: async () => (await import('./messages/reports/gameplay-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Platform Issue',
|
||||
message: async () => (await import('./messages/reports/platform-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Stale',
|
||||
message: async () => (await import('./messages/reports/stale.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Confirmed Malware',
|
||||
message: async () => (await import('./messages/reports/confirmed-malware.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Antivirus',
|
||||
message: async () => (await import('./messages/reports/antivirus.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Spam',
|
||||
message: async () => (await import('./messages/reports/spam.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Gameplay Issue',
|
||||
message: async () => (await import('./messages/reports/gameplay-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Platform Issue',
|
||||
message: async () => (await import('./messages/reports/platform-issue.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Stale',
|
||||
message: async () => (await import('./messages/reports/stale.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
label: 'Confirmed Malware',
|
||||
message: async () => (await import('./messages/reports/confirmed-malware.md?raw')).default,
|
||||
private: false,
|
||||
},
|
||||
] as ReadonlyArray<ReportQuickReply>
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { TagsIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const categories: Stage = {
|
||||
title: "Are the project's tags accurate?",
|
||||
id: 'tags',
|
||||
icon: TagsIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/tags',
|
||||
shouldShow: (project) =>
|
||||
project.categories.length > 0 || project.additional_categories.length > 0,
|
||||
text: async () => {
|
||||
return (await import('../messages/checklist-text/categories.md?raw')).default
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'categories_inaccurate',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 700,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/categories/inaccurate.md?raw')).default,
|
||||
disablesActions: ['categories_optimization_misused', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_optimization_misused',
|
||||
type: 'button',
|
||||
label: 'Optimization',
|
||||
weight: 701,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) =>
|
||||
project.categories.includes('optimization') ||
|
||||
project.additional_categories.includes('optimization'),
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/optimization_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_resolutions_misused',
|
||||
type: 'button',
|
||||
label: 'Resolutions',
|
||||
weight: 702,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'resourcepack',
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/resolutions_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_optimization_misused'],
|
||||
},
|
||||
],
|
||||
title: "Are the project's tags accurate?",
|
||||
id: 'tags',
|
||||
icon: TagsIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/tags',
|
||||
shouldShow: (project) =>
|
||||
project.categories.length > 0 || project.additional_categories.length > 0,
|
||||
text: async () => {
|
||||
return (await import('../messages/checklist-text/categories.md?raw')).default
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'categories_inaccurate',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 700,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/categories/inaccurate.md?raw')).default,
|
||||
disablesActions: ['categories_optimization_misused', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_optimization_misused',
|
||||
type: 'button',
|
||||
label: 'Optimization',
|
||||
weight: 701,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) =>
|
||||
project.categories.includes('optimization') ||
|
||||
project.additional_categories.includes('optimization'),
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/optimization_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_resolutions_misused'],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'categories_resolutions_misused',
|
||||
type: 'button',
|
||||
label: 'Resolutions',
|
||||
weight: 702,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'resourcepack',
|
||||
message: async () =>
|
||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||
(await import('../messages/categories/resolutions_misused.md?raw')).default,
|
||||
disablesActions: ['categories_inaccurate', 'categories_optimization_misused'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default categories
|
||||
|
||||
@@ -1,109 +1,110 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { LibraryIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const description: Stage = {
|
||||
title: 'Is the description sufficient, accurate, and accessible?',
|
||||
id: 'description',
|
||||
icon: LibraryIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/',
|
||||
actions: [
|
||||
{
|
||||
id: 'description_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient (custom)',
|
||||
weight: 400,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/insufficient.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please elaborate on how the author can improve their description.',
|
||||
variable: 'EXPLAINER',
|
||||
large: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_packs',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-packs.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_projects',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type !== 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-projects.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 402,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_unfinished',
|
||||
type: 'button',
|
||||
label: 'Unfinished',
|
||||
weight: 403,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/unfinished.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_headers_as_body',
|
||||
type: 'button',
|
||||
label: 'Headers as body text',
|
||||
weight: 404,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/headers-as-body.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_image_only',
|
||||
type: 'button',
|
||||
label: 'Image-only',
|
||||
weight: 405,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/image-only.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_standard_text',
|
||||
type: 'button',
|
||||
label: 'Non-standard text',
|
||||
weight: 406,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/description/non-standard-text.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_clarity',
|
||||
type: 'button',
|
||||
label: 'Unclear / Misleading',
|
||||
weight: 407,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/description/clarity.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: 'Is the description sufficient, accurate, and accessible?',
|
||||
id: 'description',
|
||||
icon: LibraryIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/',
|
||||
actions: [
|
||||
{
|
||||
id: 'description_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient (custom)',
|
||||
weight: 400,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/insufficient.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please elaborate on how the author can improve their description.',
|
||||
variable: 'EXPLAINER',
|
||||
large: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_packs',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-packs.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_insufficient_projects',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 401,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => project.project_type !== 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/description/insufficient-projects.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 402,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_unfinished',
|
||||
type: 'button',
|
||||
label: 'Unfinished',
|
||||
weight: 403,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/unfinished.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_headers_as_body',
|
||||
type: 'button',
|
||||
label: 'Headers as body text',
|
||||
weight: 404,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/description/headers-as-body.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_image_only',
|
||||
type: 'button',
|
||||
label: 'Image-only',
|
||||
weight: 405,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/description/image-only.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_non_standard_text',
|
||||
type: 'button',
|
||||
label: 'Non-standard text',
|
||||
weight: 406,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/description/non-standard-text.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'description_clarity',
|
||||
type: 'button',
|
||||
label: 'Unclear / Misleading',
|
||||
weight: 407,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/description/clarity.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default description
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { ImageIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const gallery: Stage = {
|
||||
title: "Are this project's gallery images sufficient?",
|
||||
id: 'gallery',
|
||||
icon: ImageIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/gallery',
|
||||
actions: [
|
||||
{
|
||||
id: 'gallery_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 900,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/gallery/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'gallery_not_relevant',
|
||||
type: 'button',
|
||||
label: 'Not relevant',
|
||||
weight: 901,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.gallery && project.gallery.length > 0,
|
||||
message: async () => (await import('../messages/gallery/not-relevant.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: "Are this project's gallery images sufficient?",
|
||||
id: 'gallery',
|
||||
icon: ImageIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#general-expectations',
|
||||
navigate: '/gallery',
|
||||
actions: [
|
||||
{
|
||||
id: 'gallery_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 900,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/gallery/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'gallery_not_relevant',
|
||||
type: 'button',
|
||||
label: 'Not relevant',
|
||||
weight: 901,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.gallery && project.gallery.length > 0,
|
||||
message: async () => (await import('../messages/gallery/not-relevant.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default gallery
|
||||
|
||||
@@ -1,84 +1,85 @@
|
||||
import { BookTextIcon } from '@modrinth/assets'
|
||||
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const licensesNotRequiringSource: string[] = [
|
||||
'LicenseRef-All-Rights-Reserved',
|
||||
'Apache-2.0',
|
||||
'BSD-2-Clause',
|
||||
'BSD-3-Clause',
|
||||
'CC0-1.0',
|
||||
'CC-BY-4.0',
|
||||
'CC-BY-SA-4.0',
|
||||
'CC-BY-NC-4.0',
|
||||
'CC-BY-NC-SA-4.0',
|
||||
'CC-BY-ND-4.0',
|
||||
'CC-BY-NC-ND-4.0',
|
||||
'ISC',
|
||||
'MIT',
|
||||
'Zlib',
|
||||
'LicenseRef-All-Rights-Reserved',
|
||||
'Apache-2.0',
|
||||
'BSD-2-Clause',
|
||||
'BSD-3-Clause',
|
||||
'CC0-1.0',
|
||||
'CC-BY-4.0',
|
||||
'CC-BY-SA-4.0',
|
||||
'CC-BY-NC-4.0',
|
||||
'CC-BY-NC-SA-4.0',
|
||||
'CC-BY-ND-4.0',
|
||||
'CC-BY-NC-ND-4.0',
|
||||
'ISC',
|
||||
'MIT',
|
||||
'Zlib',
|
||||
]
|
||||
|
||||
const licenseStage: Stage = {
|
||||
title: 'Is this license and link valid?',
|
||||
text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default,
|
||||
id: 'license',
|
||||
icon: BookTextIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/license',
|
||||
actions: [
|
||||
{
|
||||
id: 'license_invalid_link',
|
||||
type: 'button',
|
||||
label: 'Invalid Link',
|
||||
weight: 600,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => Boolean(project.license.url),
|
||||
message: async () => (await import('../messages/license/invalid_link.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_invalid_link-custom_license',
|
||||
type: 'toggle',
|
||||
label: 'Invalid Link: Custom License',
|
||||
weight: 601,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/license/invalid_link-custom_license.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'license_no_source',
|
||||
type: 'conditional-button',
|
||||
label: 'No Source',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => !licensesNotRequiringSource.includes(project.license.id),
|
||||
messageVariants: [
|
||||
{
|
||||
conditions: {
|
||||
excludedActions: ['license_no_source-fork'],
|
||||
},
|
||||
weight: 602,
|
||||
message: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
},
|
||||
],
|
||||
fallbackWeight: 602,
|
||||
fallbackMessage: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_no_source-fork',
|
||||
type: 'toggle',
|
||||
label: 'No Source: Fork',
|
||||
weight: 602,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/license/no_source-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Is this license and link valid?',
|
||||
text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default,
|
||||
id: 'license',
|
||||
icon: BookTextIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings/license',
|
||||
actions: [
|
||||
{
|
||||
id: 'license_invalid_link',
|
||||
type: 'button',
|
||||
label: 'Invalid Link',
|
||||
weight: 600,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => Boolean(project.license.url),
|
||||
message: async () => (await import('../messages/license/invalid_link.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_invalid_link-custom_license',
|
||||
type: 'toggle',
|
||||
label: 'Invalid Link: Custom License',
|
||||
weight: 601,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/license/invalid_link-custom_license.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'license_no_source',
|
||||
type: 'conditional-button',
|
||||
label: 'No Source',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => !licensesNotRequiringSource.includes(project.license.id),
|
||||
messageVariants: [
|
||||
{
|
||||
conditions: {
|
||||
excludedActions: ['license_no_source-fork'],
|
||||
},
|
||||
weight: 602,
|
||||
message: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
},
|
||||
],
|
||||
fallbackWeight: 602,
|
||||
fallbackMessage: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_no_source-fork',
|
||||
type: 'toggle',
|
||||
label: 'No Source: Fork',
|
||||
weight: 602,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/license/no_source-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default licenseStage
|
||||
|
||||
@@ -1,88 +1,89 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { LinkIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const links: Stage = {
|
||||
title: "Are the project's links accurate and accessible?",
|
||||
id: 'links',
|
||||
icon: LinkIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
navigate: '/settings/links',
|
||||
shouldShow: (project) =>
|
||||
Boolean(
|
||||
project.issues_url ||
|
||||
project.source_url ||
|
||||
project.wiki_url ||
|
||||
project.discord_url ||
|
||||
project.donation_urls.length > 0,
|
||||
),
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/links/base.md?raw')).default
|
||||
title: "Are the project's links accurate and accessible?",
|
||||
id: 'links',
|
||||
icon: LinkIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
navigate: '/settings/links',
|
||||
shouldShow: (project) =>
|
||||
Boolean(
|
||||
project.issues_url ||
|
||||
project.source_url ||
|
||||
project.wiki_url ||
|
||||
project.discord_url ||
|
||||
project.donation_urls.length > 0,
|
||||
),
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/links/base.md?raw')).default
|
||||
|
||||
if (project.donation_urls.length > 0) {
|
||||
text += (await import('../messages/checklist-text/links/donation/donations.md?raw')).default
|
||||
if (project.donation_urls.length > 0) {
|
||||
text += (await import('../messages/checklist-text/links/donation/donations.md?raw')).default
|
||||
|
||||
for (const donation of project.donation_urls) {
|
||||
text += (await import(`../messages/checklist-text/links/donation/donation.md?raw`)).default
|
||||
.replace('{URL}', donation.url)
|
||||
.replace('{PLATFORM}', donation.platform)
|
||||
}
|
||||
}
|
||||
for (const donation of project.donation_urls) {
|
||||
text += (await import(`../messages/checklist-text/links/donation/donation.md?raw`)).default
|
||||
.replace('{URL}', donation.url)
|
||||
.replace('{PLATFORM}', donation.platform)
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'links_misused',
|
||||
type: 'button',
|
||||
label: 'Links are misused',
|
||||
weight: 500,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/links/misused.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What links are misused?',
|
||||
variable: 'MISUSED_LINKS',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'links_unaccessible',
|
||||
type: 'button',
|
||||
label: 'Links are inaccessible',
|
||||
weight: 510,
|
||||
suggestedStatus: 'flagged',
|
||||
// Theoretically a conditional could go here to prevent overlap of misuse and unaccessible messages repeating while still allowing for a multi-select in each.
|
||||
// if links_misused was selected, send nothing.
|
||||
message: async () => (await import('../messages/links/not_accessible.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'links_unaccessible_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Warn of inaccessible link?',
|
||||
shouldShow: (project) => Boolean(project.source_url || project.discord_url),
|
||||
options: [
|
||||
{
|
||||
label: 'Source',
|
||||
weight: 511,
|
||||
shouldShow: (project) => Boolean(project.source_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-source.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
weight: 512,
|
||||
shouldShow: (project) => Boolean(project.discord_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-discord.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
return text
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'links_misused',
|
||||
type: 'button',
|
||||
label: 'Links are misused',
|
||||
weight: 500,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/links/misused.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What links are misused?',
|
||||
variable: 'MISUSED_LINKS',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'links_unaccessible',
|
||||
type: 'button',
|
||||
label: 'Links are inaccessible',
|
||||
weight: 510,
|
||||
suggestedStatus: 'flagged',
|
||||
// Theoretically a conditional could go here to prevent overlap of misuse and unaccessible messages repeating while still allowing for a multi-select in each.
|
||||
// if links_misused was selected, send nothing.
|
||||
message: async () => (await import('../messages/links/not_accessible.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'links_unaccessible_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Warn of inaccessible link?',
|
||||
shouldShow: (project) => Boolean(project.source_url || project.discord_url),
|
||||
options: [
|
||||
{
|
||||
label: 'Source',
|
||||
weight: 511,
|
||||
shouldShow: (project) => Boolean(project.source_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-source.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
weight: 512,
|
||||
shouldShow: (project) => Boolean(project.discord_url),
|
||||
message: async () =>
|
||||
(await import('../messages/links/not_accessible-discord.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default links
|
||||
|
||||
@@ -1,111 +1,112 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { CopyrightIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const reupload: Stage = {
|
||||
title: 'Does the author have proper permissions to post this project?',
|
||||
id: 'reupload',
|
||||
icon: CopyrightIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
actions: [
|
||||
{
|
||||
id: 'reupload_reupload',
|
||||
type: 'button',
|
||||
label: 'Re-upload',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/reupload.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What is the title of the original project?',
|
||||
variable: 'ORIGINAL_PROJECT',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks'],
|
||||
},
|
||||
{
|
||||
label: 'What is the author of the original project?',
|
||||
variable: 'ORIGINAL_AUTHOR',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks Team'],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_unclear_fork',
|
||||
type: 'button',
|
||||
label: 'Unclear Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_insufficient_fork',
|
||||
type: 'button',
|
||||
label: 'Insufficient Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/insufficient_fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_reupload',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_request_proof',
|
||||
type: 'button',
|
||||
label: 'Proof of permissions',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/proof_of_permissions.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'reupload_identity_verification',
|
||||
type: 'button',
|
||||
label: 'Verify Identity',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/identity_verification.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Where else can the project be found?',
|
||||
variable: 'PLATFORM',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Does the author have proper permissions to post this project?',
|
||||
id: 'reupload',
|
||||
icon: CopyrightIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules',
|
||||
actions: [
|
||||
{
|
||||
id: 'reupload_reupload',
|
||||
type: 'button',
|
||||
label: 'Re-upload',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/reupload.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'What is the title of the original project?',
|
||||
variable: 'ORIGINAL_PROJECT',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks'],
|
||||
},
|
||||
{
|
||||
label: 'What is the author of the original project?',
|
||||
variable: 'ORIGINAL_AUTHOR',
|
||||
required: true,
|
||||
suggestions: ['Vanilla Tweaks Team'],
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_unclear_fork',
|
||||
type: 'button',
|
||||
label: 'Unclear Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_insufficient_fork',
|
||||
type: 'button',
|
||||
label: 'Insufficient Fork',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/insufficient_fork.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_reupload',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_request_proof',
|
||||
type: 'button',
|
||||
label: 'Proof of permissions',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/proof_of_permissions.md?raw')).default,
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'reupload_identity_verification',
|
||||
type: 'button',
|
||||
label: 'Verify Identity',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/identity_verification.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Where else can the project be found?',
|
||||
variable: 'PLATFORM',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default reupload
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { ListBulletedIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const ruleFollowing: Stage = {
|
||||
title: 'Does this project violate the rules?',
|
||||
id: 'rule-following',
|
||||
icon: ListBulletedIcon,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Creator-Communication-Guide-1b65ee711bf080ec9337e3ccdded146c',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'rule_breaking_yes',
|
||||
type: 'button',
|
||||
label: 'Yes',
|
||||
weight: 0,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
message: async () => (await import('../messages/rule-breaking.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please explain to the user how it infringes on our content rules.',
|
||||
variable: 'MESSAGE',
|
||||
required: true,
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: 'Does this project violate the rules?',
|
||||
id: 'rule-following',
|
||||
icon: ListBulletedIcon,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Creator-Communication-Guide-1b65ee711bf080ec9337e3ccdded146c',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'rule_breaking_yes',
|
||||
type: 'button',
|
||||
label: 'Yes',
|
||||
weight: 0,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
message: async () => (await import('../messages/rule-breaking.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Please explain to the user how it infringes on our content rules.',
|
||||
variable: 'MESSAGE',
|
||||
required: true,
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default ruleFollowing
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { GlobeIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const sideTypes: Stage = {
|
||||
title: "Is the project's environment information accurate?",
|
||||
id: 'environment',
|
||||
icon: GlobeIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings',
|
||||
text: async () => (await import('../messages/checklist-text/side_types.md?raw')).default,
|
||||
actions: [
|
||||
{
|
||||
id: 'side_types_inaccurate_modpack',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/side-types/inaccurate-modpack.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'side_types_inaccurate_mod',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'mod',
|
||||
message: async () => (await import('../messages/side-types/inaccurate-mod.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: "Is the project's environment information accurate?",
|
||||
id: 'environment',
|
||||
icon: GlobeIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/settings',
|
||||
text: async () => (await import('../messages/checklist-text/side_types.md?raw')).default,
|
||||
actions: [
|
||||
{
|
||||
id: 'side_types_inaccurate_modpack',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/side-types/inaccurate-modpack.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'side_types_inaccurate_mod',
|
||||
type: 'button',
|
||||
label: 'Inaccurate',
|
||||
weight: 800,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => project.project_type === 'mod',
|
||||
message: async () => (await import('../messages/side-types/inaccurate-mod.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default sideTypes
|
||||
|
||||
@@ -1,88 +1,89 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import { TriangleAlertIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const statusAlerts: Stage = {
|
||||
title: `Is anything else affecting this project's status?`,
|
||||
id: 'status-alerts',
|
||||
icon: TriangleAlertIcon,
|
||||
text: async () => (await import('../messages/checklist-text/status-alerts/text.md?raw')).default,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Project-Modification-Guidelines-22e5ee711bf080628416f0471ba6af02',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'status_corrections_applied',
|
||||
type: 'button',
|
||||
label: 'Corrections applied',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'approved',
|
||||
disablesActions: ['status_private_use', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/fixed.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_private_use',
|
||||
type: 'button',
|
||||
label: 'Private use',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'flagged',
|
||||
disablesActions: ['status_corrections_applied', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/private.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_account_issues',
|
||||
type: 'button',
|
||||
label: 'Account issues',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'rejected',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/account_issues.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_tec_source_request',
|
||||
type: 'button',
|
||||
label: `Request Source`,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'mod' ||
|
||||
project.project_type === 'shader' ||
|
||||
project.project_type.toString() === 'plugin',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'status_tec_source_request_options',
|
||||
type: 'dropdown',
|
||||
label: 'Why are you requesting source?',
|
||||
options: [
|
||||
{
|
||||
label: 'Obfuscated',
|
||||
weight: 999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-obfs.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Binaries',
|
||||
weight: 999000,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-bins.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_automod_confusion',
|
||||
type: 'button',
|
||||
label: `Automod confusion`,
|
||||
weight: -999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/automod_confusion.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: `Is anything else affecting this project's status?`,
|
||||
id: 'status-alerts',
|
||||
icon: TriangleAlertIcon,
|
||||
text: async () => (await import('../messages/checklist-text/status-alerts/text.md?raw')).default,
|
||||
guidance_url:
|
||||
'https://www.notion.so/Project-Modification-Guidelines-22e5ee711bf080628416f0471ba6af02',
|
||||
navigate: '/moderation',
|
||||
actions: [
|
||||
{
|
||||
id: 'status_corrections_applied',
|
||||
type: 'button',
|
||||
label: 'Corrections applied',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'approved',
|
||||
disablesActions: ['status_private_use', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/fixed.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_private_use',
|
||||
type: 'button',
|
||||
label: 'Private use',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'flagged',
|
||||
disablesActions: ['status_corrections_applied', 'status_account_issues'],
|
||||
message: async () => (await import('../messages/status-alerts/private.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_account_issues',
|
||||
type: 'button',
|
||||
label: 'Account issues',
|
||||
weight: -999999,
|
||||
suggestedStatus: 'rejected',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/account_issues.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_tec_source_request',
|
||||
type: 'button',
|
||||
label: `Request Source`,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'mod' ||
|
||||
project.project_type === 'shader' ||
|
||||
project.project_type.toString() === 'plugin',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'status_tec_source_request_options',
|
||||
type: 'dropdown',
|
||||
label: 'Why are you requesting source?',
|
||||
options: [
|
||||
{
|
||||
label: 'Obfuscated',
|
||||
weight: 999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-obfs.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Binaries',
|
||||
weight: 999000,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-bins.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_automod_confusion',
|
||||
type: 'button',
|
||||
label: `Automod confusion`,
|
||||
weight: -999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/automod_confusion.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default statusAlerts
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import { AlignLeftIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const summary: Stage = {
|
||||
title: "Is the project's summary sufficient?",
|
||||
text: async () => (await import('../messages/checklist-text/summary/summary.md?raw')).default,
|
||||
id: 'summary',
|
||||
icon: AlignLeftIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'summary_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_repeat_title'],
|
||||
message: async () => (await import('../messages/summary/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_repeat_title',
|
||||
type: 'button',
|
||||
label: 'Repeat of title',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_insufficient'],
|
||||
message: async () => (await import('../messages/summary/repeat-title.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_formatting',
|
||||
type: 'button',
|
||||
label: 'Formatting',
|
||||
weight: 301,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/summary/formatting.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 302,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/summary/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: "Is the project's summary sufficient?",
|
||||
text: async () => (await import('../messages/checklist-text/summary/summary.md?raw')).default,
|
||||
id: 'summary',
|
||||
icon: AlignLeftIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'summary_insufficient',
|
||||
type: 'button',
|
||||
label: 'Insufficient',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_repeat_title'],
|
||||
message: async () => (await import('../messages/summary/insufficient.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_repeat_title',
|
||||
type: 'button',
|
||||
label: 'Repeat of title',
|
||||
weight: 300,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
disablesActions: ['summary_insufficient'],
|
||||
message: async () => (await import('../messages/summary/repeat-title.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_formatting',
|
||||
type: 'button',
|
||||
label: 'Formatting',
|
||||
weight: 301,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/summary/formatting.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'summary_non_english',
|
||||
type: 'button',
|
||||
label: 'Non-english',
|
||||
weight: 302,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/summary/non-english.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default summary
|
||||
|
||||
@@ -1,96 +1,97 @@
|
||||
import { BookOpenIcon } from '@modrinth/assets'
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { Project } from '@modrinth/utils'
|
||||
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
function hasCustomSlug(project: Project): boolean {
|
||||
return (
|
||||
project.slug !==
|
||||
project.title
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
)
|
||||
return (
|
||||
project.slug !==
|
||||
project.title
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
)
|
||||
}
|
||||
|
||||
const titleSlug: Stage = {
|
||||
title: 'Are the Name and URL accurate and appropriate?',
|
||||
id: 'title-&-slug',
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/title-slug/title.md?raw')).default
|
||||
if (hasCustomSlug(project))
|
||||
text += (await import('../messages/checklist-text/title-slug/slug.md?raw')).default
|
||||
return text
|
||||
},
|
||||
icon: BookOpenIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'title_useless_info',
|
||||
type: 'button',
|
||||
label: 'Contains useless info',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/title/useless-info.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_minecraft_branding',
|
||||
type: 'button',
|
||||
label: 'Minecraft title',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/minecraft-branding.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_similarities',
|
||||
type: 'button',
|
||||
label: 'Title similarities',
|
||||
weight: 110,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/similarities.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'title_similarities_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Similarities additional info',
|
||||
options: [
|
||||
{
|
||||
label: 'Modpack named after mod',
|
||||
weight: 111,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-modpack.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Forked project',
|
||||
weight: 112,
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slug_misused_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Slug issues?',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => hasCustomSlug(project),
|
||||
options: [
|
||||
{
|
||||
label: 'Misused',
|
||||
weight: 200,
|
||||
message: async () => (await import('../messages/slug/misused.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Are the Name and URL accurate and appropriate?',
|
||||
id: 'title-&-slug',
|
||||
text: async (project) => {
|
||||
let text = (await import('../messages/checklist-text/title-slug/title.md?raw')).default
|
||||
if (hasCustomSlug(project))
|
||||
text += (await import('../messages/checklist-text/title-slug/slug.md?raw')).default
|
||||
return text
|
||||
},
|
||||
icon: BookOpenIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
actions: [
|
||||
{
|
||||
id: 'title_useless_info',
|
||||
type: 'button',
|
||||
label: 'Contains useless info',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'low',
|
||||
message: async () => (await import('../messages/title/useless-info.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_minecraft_branding',
|
||||
type: 'button',
|
||||
label: 'Minecraft title',
|
||||
weight: 100,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/minecraft-branding.md?raw')).default,
|
||||
},
|
||||
{
|
||||
id: 'title_similarities',
|
||||
type: 'button',
|
||||
label: 'Title similarities',
|
||||
weight: 110,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () => (await import('../messages/title/similarities.md?raw')).default,
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'title_similarities_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Similarities additional info',
|
||||
options: [
|
||||
{
|
||||
label: 'Modpack named after mod',
|
||||
weight: 111,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-modpack.md?raw')).default,
|
||||
},
|
||||
{
|
||||
label: 'Forked project',
|
||||
weight: 112,
|
||||
message: async () =>
|
||||
(await import('../messages/title/similarities-fork.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slug_misused_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Slug issues?',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'low',
|
||||
shouldShow: (project) => hasCustomSlug(project),
|
||||
options: [
|
||||
{
|
||||
label: 'Misused',
|
||||
weight: 200,
|
||||
message: async () => (await import('../messages/slug/misused.md?raw')).default,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default titleSlug
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const undefinedProjectStage: Stage = {
|
||||
title: 'This project is undefined!',
|
||||
id: 'undefined-project',
|
||||
icon: XIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/versions',
|
||||
shouldShow: (project) => project.versions.length === 0,
|
||||
actions: [
|
||||
{
|
||||
id: 'undefined_no_versions',
|
||||
type: 'button',
|
||||
label: 'No Versions',
|
||||
weight: -100,
|
||||
suggestedStatus: 'rejected',
|
||||
message: async () =>
|
||||
(await import('../messages/undefined-project/no_versions.md?raw')).default,
|
||||
},
|
||||
],
|
||||
title: 'This project is undefined!',
|
||||
id: 'undefined-project',
|
||||
icon: XIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/versions',
|
||||
shouldShow: (project) => project.versions.length === 0,
|
||||
actions: [
|
||||
{
|
||||
id: 'undefined_no_versions',
|
||||
type: 'button',
|
||||
label: 'No Versions',
|
||||
weight: -100,
|
||||
suggestedStatus: 'rejected',
|
||||
message: async () =>
|
||||
(await import('../messages/undefined-project/no_versions.md?raw')).default,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default undefinedProjectStage
|
||||
|
||||
@@ -1,174 +1,175 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import { VersionIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import type { Stage } from '../../types/stage'
|
||||
|
||||
const versions: Stage = {
|
||||
title: "Are this project's files correct?",
|
||||
id: 'versions',
|
||||
icon: VersionIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/versions',
|
||||
actions: [
|
||||
{
|
||||
id: 'versions_incorrect_additional',
|
||||
type: 'button',
|
||||
label: 'Incorrect additional files',
|
||||
weight: 1000,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/incorrect_additional_files.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_incorrect_project_type',
|
||||
type: 'button',
|
||||
label: 'Incorrect Project Type',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_incorrect_project_type_options',
|
||||
type: 'dropdown',
|
||||
label: 'What type should this project be?',
|
||||
options: [
|
||||
{
|
||||
label: 'Modpack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => project.project_type !== 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-modpacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Resource Pack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => project.project_type !== 'resourcepack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-resourcepacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Data Pack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => !project.loaders.includes('datapack'),
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-datapacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_alternate_versions',
|
||||
type: 'button',
|
||||
label: 'Alternate Versions',
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_alternate_versions_options',
|
||||
type: 'dropdown',
|
||||
label: 'How are the alternate versions distributed?',
|
||||
options: [
|
||||
{
|
||||
label: 'Primary Files',
|
||||
weight: 1002,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-primary.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Additional Files',
|
||||
weight: 1002,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-additional.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Monofile',
|
||||
weight: 1002,
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'resourcepack' || project.loaders.includes('datapack'),
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-mono.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Server Files (Primary Files)',
|
||||
weight: 1002,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-server.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Server Files (Additional Files)',
|
||||
weight: 1002,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-server-additional.md?raw'))
|
||||
.default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'mods.zip',
|
||||
weight: 1002,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-zip.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_vanilla_assets',
|
||||
type: 'button',
|
||||
label: 'Vanilla Assets',
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1003,
|
||||
shouldShow: (project) => project.project_type === 'resourcepack',
|
||||
message: async () => (await import('../messages/versions/vanilla_assets.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_redist_libs',
|
||||
type: 'button',
|
||||
label: 'Packed Libs',
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1003,
|
||||
shouldShow: (project) => project.project_type === 'mod' || project.project_type === 'plugin',
|
||||
message: async () => (await import('../messages/versions/redist_libs.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_duplicate_primary_files',
|
||||
type: 'button',
|
||||
label: 'Duplicate Primary Files',
|
||||
suggestedStatus: 'flagged',
|
||||
severity: `medium`,
|
||||
weight: 1004,
|
||||
message: async () => (await import('../messages/versions/broken_version.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'unsupported_project_type',
|
||||
type: 'button',
|
||||
label: `Unsupported`,
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1005,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/unsupported_project.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Project Type',
|
||||
required: true,
|
||||
variable: 'INVALID_TYPE',
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
title: "Are this project's files correct?",
|
||||
id: 'versions',
|
||||
icon: VersionIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
navigate: '/versions',
|
||||
actions: [
|
||||
{
|
||||
id: 'versions_incorrect_additional',
|
||||
type: 'button',
|
||||
label: 'Incorrect additional files',
|
||||
weight: 1000,
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/incorrect_additional_files.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_incorrect_project_type',
|
||||
type: 'button',
|
||||
label: 'Incorrect Project Type',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_incorrect_project_type_options',
|
||||
type: 'dropdown',
|
||||
label: 'What type should this project be?',
|
||||
options: [
|
||||
{
|
||||
label: 'Modpack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => project.project_type !== 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-modpacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Resource Pack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => project.project_type !== 'resourcepack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-resourcepacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Data Pack',
|
||||
weight: 1001,
|
||||
shouldShow: (project) => !project.loaders.includes('datapack'),
|
||||
message: async () =>
|
||||
(await import('../messages/versions/invalid-datapacks.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_alternate_versions',
|
||||
type: 'button',
|
||||
label: 'Alternate Versions',
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_alternate_versions_options',
|
||||
type: 'dropdown',
|
||||
label: 'How are the alternate versions distributed?',
|
||||
options: [
|
||||
{
|
||||
label: 'Primary Files',
|
||||
weight: 1002,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-primary.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Additional Files',
|
||||
weight: 1002,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-additional.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Monofile',
|
||||
weight: 1002,
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'resourcepack' || project.loaders.includes('datapack'),
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-mono.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Server Files (Primary Files)',
|
||||
weight: 1002,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-server.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Server Files (Additional Files)',
|
||||
weight: 1002,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-server-additional.md?raw'))
|
||||
.default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'mods.zip',
|
||||
weight: 1002,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-zip.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_vanilla_assets',
|
||||
type: 'button',
|
||||
label: 'Vanilla Assets',
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1003,
|
||||
shouldShow: (project) => project.project_type === 'resourcepack',
|
||||
message: async () => (await import('../messages/versions/vanilla_assets.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_redist_libs',
|
||||
type: 'button',
|
||||
label: 'Packed Libs',
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1003,
|
||||
shouldShow: (project) => project.project_type === 'mod' || project.project_type === 'plugin',
|
||||
message: async () => (await import('../messages/versions/redist_libs.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'versions_duplicate_primary_files',
|
||||
type: 'button',
|
||||
label: 'Duplicate Primary Files',
|
||||
suggestedStatus: 'flagged',
|
||||
severity: `medium`,
|
||||
weight: 1004,
|
||||
message: async () => (await import('../messages/versions/broken_version.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'unsupported_project_type',
|
||||
type: 'button',
|
||||
label: `Unsupported`,
|
||||
suggestedStatus: `rejected`,
|
||||
severity: `medium`,
|
||||
weight: 1005,
|
||||
message: async () =>
|
||||
(await import('../messages/versions/unsupported_project.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Project Type',
|
||||
required: true,
|
||||
variable: 'INVALID_TYPE',
|
||||
},
|
||||
],
|
||||
} as ButtonAction,
|
||||
],
|
||||
}
|
||||
|
||||
export default versions
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
export * from './types/actions'
|
||||
export * from './types/messages'
|
||||
export * from './types/stage'
|
||||
export * from './types/keybinds'
|
||||
export * from './types/nags'
|
||||
export * from './types/reports'
|
||||
export * from './utils'
|
||||
|
||||
export * from './data/nags/index'
|
||||
export { default as nags } from './data/nags'
|
||||
export { finalPermissionMessages } from './data/modpack-permissions-stage'
|
||||
export { default as checklist } from './data/checklist'
|
||||
export { default as keybinds } from './data/keybinds'
|
||||
export { finalPermissionMessages } from './data/modpack-permissions-stage'
|
||||
export { default as nags } from './data/nags'
|
||||
export * from './data/nags/index'
|
||||
export { default as reportQuickReplies } from './data/report-quick-replies'
|
||||
export * from './types/actions'
|
||||
export * from './types/keybinds'
|
||||
export * from './types/messages'
|
||||
export * from './types/nags'
|
||||
export * from './types/reports'
|
||||
export * from './types/stage'
|
||||
export * from './utils'
|
||||
|
||||
@@ -1,264 +1,265 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
|
||||
import type { WeightedMessage } from './messages'
|
||||
|
||||
export type ActionType =
|
||||
| 'button'
|
||||
| 'dropdown'
|
||||
| 'multi-select-chips'
|
||||
| 'toggle'
|
||||
| 'conditional-button'
|
||||
| 'button'
|
||||
| 'dropdown'
|
||||
| 'multi-select-chips'
|
||||
| 'toggle'
|
||||
| 'conditional-button'
|
||||
|
||||
export type Action =
|
||||
| ButtonAction
|
||||
| DropdownAction
|
||||
| MultiSelectChipsAction
|
||||
| ToggleAction
|
||||
| ConditionalButtonAction
|
||||
| ButtonAction
|
||||
| DropdownAction
|
||||
| MultiSelectChipsAction
|
||||
| ToggleAction
|
||||
| ConditionalButtonAction
|
||||
|
||||
export type ModerationStatus = 'approved' | 'rejected' | 'flagged'
|
||||
export type ModerationSeverity = 'low' | 'medium' | 'high' | 'critical'
|
||||
|
||||
export interface BaseAction {
|
||||
/**
|
||||
* The type of action, which determines how the action is presented to the moderator and what it does.
|
||||
*/
|
||||
type: ActionType
|
||||
/**
|
||||
* The type of action, which determines how the action is presented to the moderator and what it does.
|
||||
*/
|
||||
type: ActionType
|
||||
|
||||
/**
|
||||
* Any additional text data that is required to complete the action.
|
||||
*/
|
||||
relevantExtraInput?: AdditionalTextInput[]
|
||||
/**
|
||||
* Any additional text data that is required to complete the action.
|
||||
*/
|
||||
relevantExtraInput?: AdditionalTextInput[]
|
||||
|
||||
/**
|
||||
* Suggested moderation status when this action is selected.
|
||||
*/
|
||||
suggestedStatus?: ModerationStatus
|
||||
/**
|
||||
* Suggested moderation status when this action is selected.
|
||||
*/
|
||||
suggestedStatus?: ModerationStatus
|
||||
|
||||
/**
|
||||
* Suggested severity level for this moderation action.
|
||||
*/
|
||||
severity?: ModerationSeverity
|
||||
/**
|
||||
* Suggested severity level for this moderation action.
|
||||
*/
|
||||
severity?: ModerationSeverity
|
||||
|
||||
/**
|
||||
* Actions that become available when this action is selected.
|
||||
*/
|
||||
enablesActions?: Action[]
|
||||
/**
|
||||
* Actions that become available when this action is selected.
|
||||
*/
|
||||
enablesActions?: Action[]
|
||||
|
||||
/**
|
||||
* Actions that become unavailable when this action is selected.
|
||||
*/
|
||||
disablesActions?: string[] // Array of action IDs
|
||||
/**
|
||||
* Actions that become unavailable when this action is selected.
|
||||
*/
|
||||
disablesActions?: string[] // Array of action IDs
|
||||
|
||||
/**
|
||||
* Unique identifier for this action, used for conditional logic.
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* Unique identifier for this action, used for conditional logic.
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* A function that determines whether this action should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the action is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
/**
|
||||
* A function that determines whether this action should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the action is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a conditional message that changes based on other selected actions.
|
||||
*/
|
||||
export interface ConditionalMessage extends WeightedMessage {
|
||||
/**
|
||||
* Conditions that must be met for this message to be used.
|
||||
*/
|
||||
conditions: {
|
||||
/**
|
||||
* Action IDs that must be selected for this message to apply.
|
||||
*/
|
||||
requiredActions?: string[]
|
||||
/**
|
||||
* Conditions that must be met for this message to be used.
|
||||
*/
|
||||
conditions: {
|
||||
/**
|
||||
* Action IDs that must be selected for this message to apply.
|
||||
*/
|
||||
requiredActions?: string[]
|
||||
|
||||
/**
|
||||
* Action IDs that must NOT be selected for this message to apply.
|
||||
*/
|
||||
excludedActions?: string[]
|
||||
}
|
||||
/**
|
||||
* Action IDs that must NOT be selected for this message to apply.
|
||||
*/
|
||||
excludedActions?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a button action, which is a simple toggle button that can be used to append a message to the final moderation message.
|
||||
*/
|
||||
export interface ButtonAction extends BaseAction, WeightedMessage {
|
||||
type: 'button'
|
||||
type: 'button'
|
||||
|
||||
/**
|
||||
* The label of the button, which is displayed to the moderator. The text on the button.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the button, which is displayed to the moderator. The text on the button.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Alternative messages based on other selected actions.
|
||||
*/
|
||||
conditionalMessages?: ConditionalMessage[]
|
||||
/**
|
||||
* Alternative messages based on other selected actions.
|
||||
*/
|
||||
conditionalMessages?: ConditionalMessage[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a simple toggle/checkbox action with separate layout handling.
|
||||
*/
|
||||
export interface ToggleAction extends BaseAction, WeightedMessage {
|
||||
type: 'toggle'
|
||||
type: 'toggle'
|
||||
|
||||
/**
|
||||
* The label of the toggle, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the toggle, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Description text that appears below the toggle.
|
||||
*/
|
||||
description?: string
|
||||
/**
|
||||
* Description text that appears below the toggle.
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* Whether the toggle is checked by default.
|
||||
*/
|
||||
defaultChecked?: boolean
|
||||
/**
|
||||
* Whether the toggle is checked by default.
|
||||
*/
|
||||
defaultChecked?: boolean
|
||||
|
||||
/**
|
||||
* Alternative messages based on other selected actions.
|
||||
*/
|
||||
conditionalMessages?: ConditionalMessage[]
|
||||
/**
|
||||
* Alternative messages based on other selected actions.
|
||||
*/
|
||||
conditionalMessages?: ConditionalMessage[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a button that has different behavior based on other selected actions.
|
||||
*/
|
||||
export interface ConditionalButtonAction extends BaseAction {
|
||||
type: 'conditional-button'
|
||||
type: 'conditional-button'
|
||||
|
||||
/**
|
||||
* The label of the button, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the button, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Different message configurations based on conditions.
|
||||
*/
|
||||
messageVariants: ConditionalMessage[]
|
||||
/**
|
||||
* Different message configurations based on conditions.
|
||||
*/
|
||||
messageVariants: ConditionalMessage[]
|
||||
|
||||
/**
|
||||
* Global fallback message if no variants match their conditions.
|
||||
*/
|
||||
fallbackMessage?: () => Promise<string>
|
||||
/**
|
||||
* Global fallback message if no variants match their conditions.
|
||||
*/
|
||||
fallbackMessage?: () => Promise<string>
|
||||
|
||||
/**
|
||||
* The weight of the action's fallback message, used to determine the place where the message is placed in the final moderation message.
|
||||
*/
|
||||
fallbackWeight?: number
|
||||
/**
|
||||
* The weight of the action's fallback message, used to determine the place where the message is placed in the final moderation message.
|
||||
*/
|
||||
fallbackWeight?: number
|
||||
}
|
||||
|
||||
export interface DropdownActionOption extends WeightedMessage {
|
||||
/**
|
||||
* The label of the option, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the option, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* A function that determines whether this option should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the option is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
/**
|
||||
* A function that determines whether this option should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the option is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
}
|
||||
|
||||
export interface DropdownAction extends BaseAction {
|
||||
type: 'dropdown'
|
||||
type: 'dropdown'
|
||||
|
||||
/**
|
||||
* The label associated with the dropdown.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label associated with the dropdown.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* The options available in the dropdown.
|
||||
*/
|
||||
options: DropdownActionOption[]
|
||||
/**
|
||||
* The options available in the dropdown.
|
||||
*/
|
||||
options: DropdownActionOption[]
|
||||
|
||||
/**
|
||||
* The default option selected in the dropdown, by index.
|
||||
*/
|
||||
defaultOption?: number
|
||||
/**
|
||||
* The default option selected in the dropdown, by index.
|
||||
*/
|
||||
defaultOption?: number
|
||||
}
|
||||
|
||||
export interface MultiSelectChipsOption extends WeightedMessage {
|
||||
/**
|
||||
* The label of the chip, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the chip, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* A function that determines whether this option should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the option is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
/**
|
||||
* A function that determines whether this option should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the option is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
}
|
||||
|
||||
export interface MultiSelectChipsAction extends BaseAction {
|
||||
type: 'multi-select-chips'
|
||||
type: 'multi-select-chips'
|
||||
|
||||
/**
|
||||
* The label associated with the multi-select chips.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label associated with the multi-select chips.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* The options available in the multi-select chips.
|
||||
*/
|
||||
options: MultiSelectChipsOption[]
|
||||
/**
|
||||
* The options available in the multi-select chips.
|
||||
*/
|
||||
options: MultiSelectChipsOption[]
|
||||
}
|
||||
|
||||
export interface AdditionalTextInput {
|
||||
/**
|
||||
* The label of the text input, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* The label of the text input, which is displayed to the moderator.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* The placeholder text for the text input.
|
||||
*/
|
||||
placeholder?: string
|
||||
/**
|
||||
* The placeholder text for the text input.
|
||||
*/
|
||||
placeholder?: string
|
||||
|
||||
/**
|
||||
* Whether the text input is required to be filled out before the action can be completed.
|
||||
*/
|
||||
required?: boolean
|
||||
/**
|
||||
* Whether the text input is required to be filled out before the action can be completed.
|
||||
*/
|
||||
required?: boolean
|
||||
|
||||
/**
|
||||
* Whether the text input should use the full markdown editor rather than a simple text input.
|
||||
*/
|
||||
large?: boolean
|
||||
/**
|
||||
* Whether the text input should use the full markdown editor rather than a simple text input.
|
||||
*/
|
||||
large?: boolean
|
||||
|
||||
/**
|
||||
* The variable name that will be replaced in the message with the input value.
|
||||
* For example, if variable is "MESSAGE", then "%MESSAGE%" in the action message
|
||||
* will be replaced with the input value.
|
||||
*/
|
||||
variable?: string
|
||||
/**
|
||||
* The variable name that will be replaced in the message with the input value.
|
||||
* For example, if variable is "MESSAGE", then "%MESSAGE%" in the action message
|
||||
* will be replaced with the input value.
|
||||
*/
|
||||
variable?: string
|
||||
|
||||
/**
|
||||
* Conditions that determine when this input is shown.
|
||||
*/
|
||||
showWhen?: {
|
||||
/**
|
||||
* Action IDs that must be selected for this input to be shown.
|
||||
*/
|
||||
requiredActions?: string[]
|
||||
/**
|
||||
* Conditions that determine when this input is shown.
|
||||
*/
|
||||
showWhen?: {
|
||||
/**
|
||||
* Action IDs that must be selected for this input to be shown.
|
||||
*/
|
||||
requiredActions?: string[]
|
||||
|
||||
/**
|
||||
* Action IDs that must NOT be selected for this input to be shown.
|
||||
*/
|
||||
excludedActions?: string[]
|
||||
}
|
||||
/**
|
||||
* Action IDs that must NOT be selected for this input to be shown.
|
||||
*/
|
||||
excludedActions?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional suggestions for the input. Useful for repeating phrases or common responses.
|
||||
*/
|
||||
suggestions?: string[]
|
||||
/**
|
||||
* Optional suggestions for the input. Useful for repeating phrases or common responses.
|
||||
*/
|
||||
suggestions?: string[]
|
||||
}
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
|
||||
export interface ModerationActions {
|
||||
tryGoNext: () => void
|
||||
tryGoBack: () => void
|
||||
tryGenerateMessage: () => void
|
||||
trySkipProject: () => void
|
||||
tryGoNext: () => void
|
||||
tryGoBack: () => void
|
||||
tryGenerateMessage: () => void
|
||||
trySkipProject: () => void
|
||||
|
||||
tryToggleCollapse: () => void
|
||||
tryResetProgress: () => void
|
||||
tryExitModeration: () => void
|
||||
tryToggleCollapse: () => void
|
||||
tryResetProgress: () => void
|
||||
tryExitModeration: () => void
|
||||
|
||||
tryApprove: () => void
|
||||
tryReject: () => void
|
||||
tryWithhold: () => void
|
||||
tryEditMessage: () => void
|
||||
tryApprove: () => void
|
||||
tryReject: () => void
|
||||
tryWithhold: () => void
|
||||
tryEditMessage: () => void
|
||||
|
||||
tryToggleAction: (actionIndex: number) => void
|
||||
trySelectDropdownOption: (actionIndex: number, optionIndex: number) => void
|
||||
tryToggleChip: (actionIndex: number, chipIndex: number) => void
|
||||
tryToggleAction: (actionIndex: number) => void
|
||||
trySelectDropdownOption: (actionIndex: number, optionIndex: number) => void
|
||||
tryToggleChip: (actionIndex: number, chipIndex: number) => void
|
||||
|
||||
tryFocusNextAction: () => void
|
||||
tryFocusPreviousAction: () => void
|
||||
tryActivateFocusedAction: () => void
|
||||
tryFocusNextAction: () => void
|
||||
tryFocusPreviousAction: () => void
|
||||
tryActivateFocusedAction: () => void
|
||||
}
|
||||
|
||||
export interface ModerationState {
|
||||
currentStage: number
|
||||
totalStages: number
|
||||
currentStageId: string | undefined
|
||||
currentStageTitle: string
|
||||
currentStage: number
|
||||
totalStages: number
|
||||
currentStageId: string | undefined
|
||||
currentStageTitle: string
|
||||
|
||||
isCollapsed: boolean
|
||||
isDone: boolean
|
||||
hasGeneratedMessage: boolean
|
||||
isLoadingMessage: boolean
|
||||
isModpackPermissionsStage: boolean
|
||||
isCollapsed: boolean
|
||||
isDone: boolean
|
||||
hasGeneratedMessage: boolean
|
||||
isLoadingMessage: boolean
|
||||
isModpackPermissionsStage: boolean
|
||||
|
||||
futureProjectCount: number
|
||||
visibleActionsCount: number
|
||||
futureProjectCount: number
|
||||
visibleActionsCount: number
|
||||
|
||||
focusedActionIndex: number | null
|
||||
focusedActionType: 'button' | 'toggle' | 'dropdown' | 'multi-select' | null
|
||||
focusedActionIndex: number | null
|
||||
focusedActionType: 'button' | 'toggle' | 'dropdown' | 'multi-select' | null
|
||||
}
|
||||
|
||||
export interface ModerationContext {
|
||||
project: Project
|
||||
state: ModerationState
|
||||
actions: ModerationActions
|
||||
project: Project
|
||||
state: ModerationState
|
||||
actions: ModerationActions
|
||||
}
|
||||
|
||||
export interface KeybindDefinition {
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
alt?: boolean
|
||||
meta?: boolean
|
||||
preventDefault?: boolean
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
alt?: boolean
|
||||
meta?: boolean
|
||||
preventDefault?: boolean
|
||||
}
|
||||
|
||||
export interface KeybindListener {
|
||||
id: string
|
||||
keybind: KeybindDefinition | KeybindDefinition[] | string | string[]
|
||||
description: string
|
||||
enabled?: (ctx: ModerationContext) => boolean
|
||||
action: (ctx: ModerationContext) => void
|
||||
id: string
|
||||
keybind: KeybindDefinition | KeybindDefinition[] | string | string[]
|
||||
description: string
|
||||
enabled?: (ctx: ModerationContext) => boolean
|
||||
action: (ctx: ModerationContext) => void
|
||||
}
|
||||
|
||||
export function parseKeybind(keybindString: string): KeybindDefinition {
|
||||
const parts = keybindString.split('+').map((p) => p.trim().toLowerCase())
|
||||
const parts = keybindString.split('+').map((p) => p.trim().toLowerCase())
|
||||
|
||||
return {
|
||||
key: parts.find((p) => !['ctrl', 'shift', 'alt', 'meta', 'cmd'].includes(p)) || '',
|
||||
ctrl: parts.includes('ctrl') || parts.includes('cmd'),
|
||||
shift: parts.includes('shift'),
|
||||
alt: parts.includes('alt'),
|
||||
meta: parts.includes('meta') || parts.includes('cmd'),
|
||||
preventDefault: true,
|
||||
}
|
||||
return {
|
||||
key: parts.find((p) => !['ctrl', 'shift', 'alt', 'meta', 'cmd'].includes(p)) || '',
|
||||
ctrl: parts.includes('ctrl') || parts.includes('cmd'),
|
||||
shift: parts.includes('shift'),
|
||||
alt: parts.includes('alt'),
|
||||
meta: parts.includes('meta') || parts.includes('cmd'),
|
||||
preventDefault: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeKeybind(keybind: KeybindDefinition | string): KeybindDefinition {
|
||||
return typeof keybind === 'string' ? parseKeybind(keybind) : keybind
|
||||
return typeof keybind === 'string' ? parseKeybind(keybind) : keybind
|
||||
}
|
||||
|
||||
export function matchesKeybind(event: KeyboardEvent, keybind: KeybindDefinition | string): boolean {
|
||||
const def = normalizeKeybind(keybind)
|
||||
return (
|
||||
event.key.toLowerCase() === def.key.toLowerCase() &&
|
||||
event.ctrlKey === (def.ctrl ?? false) &&
|
||||
event.shiftKey === (def.shift ?? false) &&
|
||||
event.altKey === (def.alt ?? false) &&
|
||||
event.metaKey === (def.meta ?? false)
|
||||
)
|
||||
const def = normalizeKeybind(keybind)
|
||||
return (
|
||||
event.key.toLowerCase() === def.key.toLowerCase() &&
|
||||
event.ctrlKey === (def.ctrl ?? false) &&
|
||||
event.shiftKey === (def.shift ?? false) &&
|
||||
event.altKey === (def.alt ?? false) &&
|
||||
event.metaKey === (def.meta ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
export function handleKeybind(
|
||||
event: KeyboardEvent,
|
||||
ctx: ModerationContext,
|
||||
keybinds: KeybindListener[],
|
||||
event: KeyboardEvent,
|
||||
ctx: ModerationContext,
|
||||
keybinds: KeybindListener[],
|
||||
): boolean {
|
||||
if (
|
||||
event.target instanceof HTMLInputElement ||
|
||||
event.target instanceof HTMLTextAreaElement ||
|
||||
(event.target as HTMLElement)?.closest('.cm-editor') ||
|
||||
(event.target as HTMLElement)?.classList?.contains('cm-content') ||
|
||||
(event.target as HTMLElement)?.classList?.contains('cm-line')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
event.target instanceof HTMLInputElement ||
|
||||
event.target instanceof HTMLTextAreaElement ||
|
||||
(event.target as HTMLElement)?.closest('.cm-editor') ||
|
||||
(event.target as HTMLElement)?.classList?.contains('cm-content') ||
|
||||
(event.target as HTMLElement)?.classList?.contains('cm-line')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const keybind of keybinds) {
|
||||
if (keybind.enabled && !keybind.enabled(ctx)) {
|
||||
continue
|
||||
}
|
||||
for (const keybind of keybinds) {
|
||||
if (keybind.enabled && !keybind.enabled(ctx)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const keybindDefs = Array.isArray(keybind.keybind)
|
||||
? keybind.keybind.map(normalizeKeybind)
|
||||
: [normalizeKeybind(keybind.keybind)]
|
||||
const keybindDefs = Array.isArray(keybind.keybind)
|
||||
? keybind.keybind.map(normalizeKeybind)
|
||||
: [normalizeKeybind(keybind.keybind)]
|
||||
|
||||
const matches = keybindDefs.some((def) => matchesKeybind(event, def))
|
||||
const matches = keybindDefs.some((def) => matchesKeybind(event, def))
|
||||
|
||||
if (matches) {
|
||||
keybind.action(ctx)
|
||||
if (matches) {
|
||||
keybind.action(ctx)
|
||||
|
||||
const shouldPrevent = keybindDefs.some((def) => def.preventDefault !== false)
|
||||
if (shouldPrevent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
const shouldPrevent = keybindDefs.some((def) => def.preventDefault !== false)
|
||||
if (shouldPrevent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export interface WeightedMessage {
|
||||
/**
|
||||
* The weight of the action's active message, used to determine the place where the message is placed in the final moderation message.
|
||||
*/
|
||||
weight: number
|
||||
/**
|
||||
* The weight of the action's active message, used to determine the place where the message is placed in the final moderation message.
|
||||
*/
|
||||
weight: number
|
||||
|
||||
/**
|
||||
* The message which is appended to the final moderation message if the button is active.
|
||||
* @returns A function that lazily loads the message which is appended if the button is active.
|
||||
* @example async () => (await import('../messages/example.md?raw')).default,
|
||||
*/
|
||||
message: () => Promise<string>
|
||||
/**
|
||||
* The message which is appended to the final moderation message if the button is active.
|
||||
* @returns A function that lazily loads the message which is appended if the button is active.
|
||||
* @example async () => (await import('../messages/example.md?raw')).default,
|
||||
*/
|
||||
message: () => Promise<string>
|
||||
}
|
||||
|
||||
@@ -17,80 +17,80 @@ export type NagStatus = 'required' | 'warning' | 'suggestion' | 'special-submit-
|
||||
* This context is used to determine whether a nag or it's link should be shown and how it should be presented.
|
||||
*/
|
||||
export interface NagContext {
|
||||
/**
|
||||
* The project associated with the nag.
|
||||
*/
|
||||
project: Project
|
||||
/**
|
||||
* The versions associated with the project.
|
||||
*/
|
||||
versions: Version[]
|
||||
/**
|
||||
* The current project member viewing the nag.
|
||||
*/
|
||||
currentMember: User
|
||||
/**
|
||||
* The current route in the application.
|
||||
*/
|
||||
currentRoute: string
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
tags: any
|
||||
submitProject: (...any: any) => any
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* The project associated with the nag.
|
||||
*/
|
||||
project: Project
|
||||
/**
|
||||
* The versions associated with the project.
|
||||
*/
|
||||
versions: Version[]
|
||||
/**
|
||||
* The current project member viewing the nag.
|
||||
*/
|
||||
currentMember: User
|
||||
/**
|
||||
* The current route in the application.
|
||||
*/
|
||||
currentRoute: string
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
tags: any
|
||||
submitProject: (...any: any) => any
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing a nag's link.
|
||||
*/
|
||||
export interface NagLink {
|
||||
/**
|
||||
* A relative path to the nag's link, e.g. '/settings'.
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* The text to display for the nag's link.
|
||||
*/
|
||||
title: MessageDescriptor | string
|
||||
/**
|
||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
||||
*/
|
||||
shouldShow?: (context: NagContext) => boolean
|
||||
/**
|
||||
* A relative path to the nag's link, e.g. '/settings'.
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* The text to display for the nag's link.
|
||||
*/
|
||||
title: MessageDescriptor | string
|
||||
/**
|
||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
||||
*/
|
||||
shouldShow?: (context: NagContext) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing a nag.
|
||||
*/
|
||||
export interface Nag {
|
||||
/**
|
||||
* A unique identifier for the nag.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The title of the nag.
|
||||
*/
|
||||
title: MessageDescriptor | string
|
||||
/**
|
||||
* A function that returns the description of the nag.
|
||||
* It can accept a context to provide dynamic descriptions.
|
||||
*/
|
||||
description: MessageDescriptor | ((context: NagContext) => string)
|
||||
/**
|
||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
||||
*/
|
||||
status: NagStatus
|
||||
/**
|
||||
* An optional icon for the nag, usually from `@modrinth/assets`.
|
||||
* If not specified it will use the default icon associated with the nag status.
|
||||
*/
|
||||
icon?: FunctionalComponent<SVGAttributes>
|
||||
/**
|
||||
* A unique identifier for the nag.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The title of the nag.
|
||||
*/
|
||||
title: MessageDescriptor | string
|
||||
/**
|
||||
* A function that returns the description of the nag.
|
||||
* It can accept a context to provide dynamic descriptions.
|
||||
*/
|
||||
description: MessageDescriptor | ((context: NagContext) => string)
|
||||
/**
|
||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
||||
*/
|
||||
status: NagStatus
|
||||
/**
|
||||
* An optional icon for the nag, usually from `@modrinth/assets`.
|
||||
* If not specified it will use the default icon associated with the nag status.
|
||||
*/
|
||||
icon?: FunctionalComponent<SVGAttributes>
|
||||
|
||||
/**
|
||||
* A function that determines whether the nag should be shown based on the context.
|
||||
*/
|
||||
shouldShow: (context: NagContext) => boolean
|
||||
/**
|
||||
* An optional link associated with the nag.
|
||||
* If provided, it should be displayed alongside the nag.
|
||||
*/
|
||||
link?: NagLink
|
||||
/**
|
||||
* A function that determines whether the nag should be shown based on the context.
|
||||
*/
|
||||
shouldShow: (context: NagContext) => boolean
|
||||
/**
|
||||
* An optional link associated with the nag.
|
||||
* If provided, it should be displayed alongside the nag.
|
||||
*/
|
||||
link?: NagLink
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import type { Project, Report, Thread, User, Version, DelphiReport } from '@modrinth/utils'
|
||||
import type { DelphiReport, Project, Report, Thread, User, Version } from '@modrinth/utils'
|
||||
|
||||
export interface OwnershipTarget {
|
||||
name: string
|
||||
slug: string
|
||||
avatar_url?: string
|
||||
type: 'user' | 'organization'
|
||||
name: string
|
||||
slug: string
|
||||
avatar_url?: string
|
||||
type: 'user' | 'organization'
|
||||
}
|
||||
|
||||
export interface ExtendedReport extends Report {
|
||||
thread: Thread
|
||||
reporter_user: User
|
||||
project?: Project
|
||||
user?: User
|
||||
version?: Version
|
||||
target?: OwnershipTarget
|
||||
thread: Thread
|
||||
reporter_user: User
|
||||
project?: Project
|
||||
user?: User
|
||||
version?: Version
|
||||
target?: OwnershipTarget
|
||||
}
|
||||
|
||||
export interface ExtendedDelphiReport extends DelphiReport {
|
||||
target?: OwnershipTarget
|
||||
target?: OwnershipTarget
|
||||
}
|
||||
|
||||
export interface ReportQuickReply {
|
||||
label: string
|
||||
message: string | ((report: ExtendedReport) => Promise<string> | string)
|
||||
shouldShow?: (report: ExtendedReport) => boolean
|
||||
private?: boolean
|
||||
label: string
|
||||
message: string | ((report: ExtendedReport) => Promise<string> | string)
|
||||
shouldShow?: (report: ExtendedReport) => boolean
|
||||
private?: boolean
|
||||
}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
import type { Action } from './actions'
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
import type { Action } from './actions'
|
||||
|
||||
/**
|
||||
* Represents a moderation stage with associated actions and optional navigation logic.
|
||||
*/
|
||||
export interface Stage {
|
||||
/**
|
||||
* The title of the stage, displayed to the moderator.
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* The title of the stage, displayed to the moderator.
|
||||
*/
|
||||
title: string
|
||||
|
||||
/**
|
||||
* An optional description or additional text for the stage.
|
||||
*/
|
||||
text?: (project: Project) => Promise<string>
|
||||
/**
|
||||
* An optional description or additional text for the stage.
|
||||
*/
|
||||
text?: (project: Project) => Promise<string>
|
||||
|
||||
/**
|
||||
* Optional id for the stage, used for identification in the checklist. Will be used in the stage list as well instead of the title.
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* Optional id for the stage, used for identification in the checklist. Will be used in the stage list as well instead of the title.
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* Optional icon for the stage, displayed in the stage list and next to the title.
|
||||
*/
|
||||
icon?: FunctionalComponent<SVGAttributes>
|
||||
/**
|
||||
* Optional icon for the stage, displayed in the stage list and next to the title.
|
||||
*/
|
||||
icon?: FunctionalComponent<SVGAttributes>
|
||||
|
||||
/**
|
||||
* URL to the guidance document for this stage.
|
||||
*/
|
||||
guidance_url: string
|
||||
/**
|
||||
* URL to the guidance document for this stage.
|
||||
*/
|
||||
guidance_url: string
|
||||
|
||||
/**
|
||||
* An array of actions that can be taken in this stage.
|
||||
*/
|
||||
actions: Action[]
|
||||
/**
|
||||
* An array of actions that can be taken in this stage.
|
||||
*/
|
||||
actions: Action[]
|
||||
|
||||
/**
|
||||
* Optional navigation path to redirect the moderator when this stage is shown.
|
||||
*
|
||||
* This is relative to the project page. For example, `/settings#side-types` would navigate to `https://modrinth.com/project/:id/settings#side-types`.
|
||||
*/
|
||||
navigate?: string
|
||||
/**
|
||||
* Optional navigation path to redirect the moderator when this stage is shown.
|
||||
*
|
||||
* This is relative to the project page. For example, `/settings#side-types` would navigate to `https://modrinth.com/project/:id/settings#side-types`.
|
||||
*/
|
||||
navigate?: string
|
||||
|
||||
/**
|
||||
* A function that determines whether this stage should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the stage is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
/**
|
||||
* A function that determines whether this stage should be shown for a given project.
|
||||
*
|
||||
* By default, it returns `true`, meaning the stage is always shown.
|
||||
*/
|
||||
shouldShow?: (project: Project) => boolean
|
||||
}
|
||||
|
||||
@@ -1,345 +1,346 @@
|
||||
import type { Project } from '@modrinth/utils'
|
||||
|
||||
import type {
|
||||
Action,
|
||||
AdditionalTextInput,
|
||||
ButtonAction,
|
||||
ConditionalMessage,
|
||||
ToggleAction,
|
||||
Action,
|
||||
AdditionalTextInput,
|
||||
ButtonAction,
|
||||
ConditionalMessage,
|
||||
ToggleAction,
|
||||
} from './types/actions'
|
||||
|
||||
export interface ActionState {
|
||||
selected: boolean
|
||||
value?: Set<number> | number | string | unknown
|
||||
selected: boolean
|
||||
value?: Set<number> | number | string | unknown
|
||||
}
|
||||
|
||||
export interface MessagePart {
|
||||
weight: number
|
||||
content: string
|
||||
actionId: string
|
||||
stageIndex: number
|
||||
weight: number
|
||||
content: string
|
||||
actionId: string
|
||||
stageIndex: number
|
||||
}
|
||||
|
||||
export type SerializedActionState = {
|
||||
isSet?: boolean
|
||||
isSet?: boolean
|
||||
} & ActionState
|
||||
|
||||
export function getActionIdForStage(
|
||||
action: Action,
|
||||
stageIndex: number,
|
||||
actionIndex?: number,
|
||||
enabledIndex?: number,
|
||||
action: Action,
|
||||
stageIndex: number,
|
||||
actionIndex?: number,
|
||||
enabledIndex?: number,
|
||||
): string {
|
||||
if (action.id) {
|
||||
return `stage-${stageIndex}-${action.id}`
|
||||
}
|
||||
const suffix = enabledIndex !== undefined ? `-enabled-${enabledIndex}` : ''
|
||||
return `stage-${stageIndex}-action-${actionIndex}${suffix}`
|
||||
if (action.id) {
|
||||
return `stage-${stageIndex}-${action.id}`
|
||||
}
|
||||
const suffix = enabledIndex !== undefined ? `-enabled-${enabledIndex}` : ''
|
||||
return `stage-${stageIndex}-action-${actionIndex}${suffix}`
|
||||
}
|
||||
|
||||
export function getActionId(action: Action, currentStage: number, index?: number): string {
|
||||
return getActionIdForStage(action, currentStage, index)
|
||||
return getActionIdForStage(action, currentStage, index)
|
||||
}
|
||||
|
||||
export function getActionKey(
|
||||
action: Action,
|
||||
currentStage: number,
|
||||
visibleActions: Action[],
|
||||
action: Action,
|
||||
currentStage: number,
|
||||
visibleActions: Action[],
|
||||
): string {
|
||||
const index = visibleActions.indexOf(action)
|
||||
return `${currentStage}-${index}-${getActionId(action, currentStage)}`
|
||||
const index = visibleActions.indexOf(action)
|
||||
return `${currentStage}-${index}-${getActionId(action, currentStage)}`
|
||||
}
|
||||
|
||||
export function serializeActionStates(states: Record<string, ActionState>): string {
|
||||
const serializable: Record<string, SerializedActionState> = {}
|
||||
for (const [key, state] of Object.entries(states)) {
|
||||
serializable[key] = {
|
||||
selected: state.selected,
|
||||
value: state.value instanceof Set ? Array.from(state.value) : state.value,
|
||||
isSet: state.value instanceof Set,
|
||||
}
|
||||
}
|
||||
return JSON.stringify(serializable)
|
||||
const serializable: Record<string, SerializedActionState> = {}
|
||||
for (const [key, state] of Object.entries(states)) {
|
||||
serializable[key] = {
|
||||
selected: state.selected,
|
||||
value: state.value instanceof Set ? Array.from(state.value) : state.value,
|
||||
isSet: state.value instanceof Set,
|
||||
}
|
||||
}
|
||||
return JSON.stringify(serializable)
|
||||
}
|
||||
|
||||
export function deserializeActionStates(data: string): Record<string, ActionState> {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
const states: Record<string, ActionState> = {}
|
||||
for (const [key, state] of Object.entries(parsed as Record<string, SerializedActionState>)) {
|
||||
states[key] = {
|
||||
selected: state.selected,
|
||||
value: state.isSet ? new Set(state.value as unknown[]) : state.value,
|
||||
}
|
||||
}
|
||||
return states
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
const states: Record<string, ActionState> = {}
|
||||
for (const [key, state] of Object.entries(parsed as Record<string, SerializedActionState>)) {
|
||||
states[key] = {
|
||||
selected: state.selected,
|
||||
value: state.isSet ? new Set(state.value as unknown[]) : state.value,
|
||||
}
|
||||
}
|
||||
return states
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeActionState(action: Action): ActionState {
|
||||
if (action.type === 'toggle') {
|
||||
return {
|
||||
selected: action.defaultChecked || false,
|
||||
}
|
||||
} else if (action.type === 'dropdown') {
|
||||
return {
|
||||
selected: true,
|
||||
value: action.defaultOption || 0,
|
||||
}
|
||||
} else if (action.type === 'multi-select-chips') {
|
||||
return {
|
||||
selected: false,
|
||||
value: new Set<number>(),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
if (action.type === 'toggle') {
|
||||
return {
|
||||
selected: action.defaultChecked || false,
|
||||
}
|
||||
} else if (action.type === 'dropdown') {
|
||||
return {
|
||||
selected: true,
|
||||
value: action.defaultOption || 0,
|
||||
}
|
||||
} else if (action.type === 'multi-select-chips') {
|
||||
return {
|
||||
selected: false,
|
||||
value: new Set<number>(),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function processMessage(
|
||||
message: string,
|
||||
action: Action,
|
||||
stageIndex: number,
|
||||
textInputValues: Record<string, string>,
|
||||
message: string,
|
||||
action: Action,
|
||||
stageIndex: number,
|
||||
textInputValues: Record<string, string>,
|
||||
): string {
|
||||
let processedMessage = message
|
||||
let processedMessage = message
|
||||
|
||||
if (action.relevantExtraInput) {
|
||||
action.relevantExtraInput.forEach((input, index) => {
|
||||
if (input.variable) {
|
||||
const inputKey = `stage-${stageIndex}-${action.id || `action-${index}`}-${index}`
|
||||
const value = textInputValues[inputKey] || ''
|
||||
if (action.relevantExtraInput) {
|
||||
action.relevantExtraInput.forEach((input, index) => {
|
||||
if (input.variable) {
|
||||
const inputKey = `stage-${stageIndex}-${action.id || `action-${index}`}-${index}`
|
||||
const value = textInputValues[inputKey] || ''
|
||||
|
||||
const regex = new RegExp(`%${input.variable}%`, 'g')
|
||||
processedMessage = processedMessage.replace(regex, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
const regex = new RegExp(`%${input.variable}%`, 'g')
|
||||
processedMessage = processedMessage.replace(regex, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return processedMessage
|
||||
return processedMessage
|
||||
}
|
||||
|
||||
export function findMatchingVariant(
|
||||
variants: ConditionalMessage[],
|
||||
selectedActionIds: string[],
|
||||
allValidActionIds?: string[],
|
||||
currentStageIndex?: number,
|
||||
variants: ConditionalMessage[],
|
||||
selectedActionIds: string[],
|
||||
allValidActionIds?: string[],
|
||||
currentStageIndex?: number,
|
||||
): ConditionalMessage | null {
|
||||
for (const variant of variants) {
|
||||
const conditions = variant.conditions
|
||||
for (const variant of variants) {
|
||||
const conditions = variant.conditions
|
||||
|
||||
const meetsRequired =
|
||||
!conditions.requiredActions ||
|
||||
conditions.requiredActions.every((id) => {
|
||||
let fullId = id
|
||||
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
|
||||
fullId = `stage-${currentStageIndex}-${id}`
|
||||
}
|
||||
const meetsRequired =
|
||||
!conditions.requiredActions ||
|
||||
conditions.requiredActions.every((id) => {
|
||||
let fullId = id
|
||||
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
|
||||
fullId = `stage-${currentStageIndex}-${id}`
|
||||
}
|
||||
|
||||
if (allValidActionIds && !allValidActionIds.includes(fullId)) {
|
||||
return false
|
||||
}
|
||||
return selectedActionIds.includes(fullId)
|
||||
})
|
||||
if (allValidActionIds && !allValidActionIds.includes(fullId)) {
|
||||
return false
|
||||
}
|
||||
return selectedActionIds.includes(fullId)
|
||||
})
|
||||
|
||||
const meetsExcluded =
|
||||
!conditions.excludedActions ||
|
||||
!conditions.excludedActions.some((id) => {
|
||||
let fullId = id
|
||||
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
|
||||
fullId = `stage-${currentStageIndex}-${id}`
|
||||
}
|
||||
return selectedActionIds.includes(fullId)
|
||||
})
|
||||
const meetsExcluded =
|
||||
!conditions.excludedActions ||
|
||||
!conditions.excludedActions.some((id) => {
|
||||
let fullId = id
|
||||
if (currentStageIndex !== undefined && !id.startsWith('stage-')) {
|
||||
fullId = `stage-${currentStageIndex}-${id}`
|
||||
}
|
||||
return selectedActionIds.includes(fullId)
|
||||
})
|
||||
|
||||
if (meetsRequired && meetsExcluded) {
|
||||
return variant
|
||||
}
|
||||
}
|
||||
if (meetsRequired && meetsExcluded) {
|
||||
return variant
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getActionMessage(
|
||||
action: ButtonAction | ToggleAction,
|
||||
selectedActionIds: string[],
|
||||
allValidActionIds?: string[],
|
||||
action: ButtonAction | ToggleAction,
|
||||
selectedActionIds: string[],
|
||||
allValidActionIds?: string[],
|
||||
): Promise<string> {
|
||||
if (action.conditionalMessages && action.conditionalMessages.length > 0) {
|
||||
const matchingConditional = findMatchingVariant(
|
||||
action.conditionalMessages,
|
||||
selectedActionIds,
|
||||
allValidActionIds,
|
||||
)
|
||||
if (matchingConditional) {
|
||||
return (await matchingConditional.message()) as string
|
||||
}
|
||||
}
|
||||
if (action.conditionalMessages && action.conditionalMessages.length > 0) {
|
||||
const matchingConditional = findMatchingVariant(
|
||||
action.conditionalMessages,
|
||||
selectedActionIds,
|
||||
allValidActionIds,
|
||||
)
|
||||
if (matchingConditional) {
|
||||
return (await matchingConditional.message()) as string
|
||||
}
|
||||
}
|
||||
|
||||
return (await action.message()) as string
|
||||
return (await action.message()) as string
|
||||
}
|
||||
|
||||
export function getVisibleInputs(
|
||||
action: Action,
|
||||
actionStates: Record<string, ActionState>,
|
||||
action: Action,
|
||||
actionStates: Record<string, ActionState>,
|
||||
): AdditionalTextInput[] {
|
||||
if (!action.relevantExtraInput) return []
|
||||
if (!action.relevantExtraInput) return []
|
||||
|
||||
const selectedActionIds = Object.entries(actionStates)
|
||||
.filter(([, state]) => state.selected)
|
||||
.map(([id]) => id)
|
||||
const selectedActionIds = Object.entries(actionStates)
|
||||
.filter(([, state]) => state.selected)
|
||||
.map(([id]) => id)
|
||||
|
||||
return action.relevantExtraInput.filter((input) => {
|
||||
if (!input.showWhen) return true
|
||||
return action.relevantExtraInput.filter((input) => {
|
||||
if (!input.showWhen) return true
|
||||
|
||||
const meetsRequired =
|
||||
!input.showWhen.requiredActions ||
|
||||
input.showWhen.requiredActions.every((id) => selectedActionIds.includes(id))
|
||||
const meetsRequired =
|
||||
!input.showWhen.requiredActions ||
|
||||
input.showWhen.requiredActions.every((id) => selectedActionIds.includes(id))
|
||||
|
||||
const meetsExcluded =
|
||||
!input.showWhen.excludedActions ||
|
||||
!input.showWhen.excludedActions.some((id) => selectedActionIds.includes(id))
|
||||
const meetsExcluded =
|
||||
!input.showWhen.excludedActions ||
|
||||
!input.showWhen.excludedActions.some((id) => selectedActionIds.includes(id))
|
||||
|
||||
return meetsRequired && meetsExcluded
|
||||
})
|
||||
return meetsRequired && meetsExcluded
|
||||
})
|
||||
}
|
||||
|
||||
export function expandVariables(
|
||||
template: string,
|
||||
project: Project,
|
||||
variables?: Record<string, string>,
|
||||
template: string,
|
||||
project: Project,
|
||||
variables?: Record<string, string>,
|
||||
): string {
|
||||
if (!variables) {
|
||||
variables = flattenProjectVariables(project)
|
||||
}
|
||||
if (!variables) {
|
||||
variables = flattenProjectVariables(project)
|
||||
}
|
||||
|
||||
return Object.entries(variables).reduce((result, [key, value]) => {
|
||||
const variable = `%${key}%`
|
||||
return result.replace(new RegExp(variable, 'g'), value)
|
||||
}, template)
|
||||
return Object.entries(variables).reduce((result, [key, value]) => {
|
||||
const variable = `%${key}%`
|
||||
return result.replace(new RegExp(variable, 'g'), value)
|
||||
}, template)
|
||||
}
|
||||
|
||||
export function kebabToTitleCase(input: string): string {
|
||||
return input
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
return input
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function arrayOrNone(arr: string[]): string {
|
||||
return arr.length > 0 ? arr.join(', ') : 'None'
|
||||
return arr.length > 0 ? arr.join(', ') : 'None'
|
||||
}
|
||||
|
||||
export function flattenProjectVariables(project: Project): Record<string, string> {
|
||||
const vars: Record<string, string> = {}
|
||||
const vars: Record<string, string> = {}
|
||||
|
||||
vars['PROJECT_ID'] = project.id
|
||||
vars['PROJECT_TYPE'] = project.project_type
|
||||
vars['PROJECT_SLUG'] = project.slug
|
||||
vars['PROJECT_TITLE'] = project.title
|
||||
vars['PROJECT_SUMMARY'] = project.description
|
||||
vars['PROJECT_STATUS'] = project.status
|
||||
vars['PROJECT_REQUESTED_STATUS'] = project.requested_status
|
||||
vars['PROJECT_MONETIZATION_STATUS'] = project.monetization_status
|
||||
vars['PROJECT_BODY'] = project.body
|
||||
vars['PROJECT_ID'] = project.id
|
||||
vars['PROJECT_TYPE'] = project.project_type
|
||||
vars['PROJECT_SLUG'] = project.slug
|
||||
vars['PROJECT_TITLE'] = project.title
|
||||
vars['PROJECT_SUMMARY'] = project.description
|
||||
vars['PROJECT_STATUS'] = project.status
|
||||
vars['PROJECT_REQUESTED_STATUS'] = project.requested_status
|
||||
vars['PROJECT_MONETIZATION_STATUS'] = project.monetization_status
|
||||
vars['PROJECT_BODY'] = project.body
|
||||
|
||||
vars['PROJECT_ICON_URL'] = project.icon_url || ''
|
||||
vars['PROJECT_ISSUES_URL'] = project.issues_url || 'None'
|
||||
vars['PROJECT_SOURCE_URL'] = project.source_url || 'None'
|
||||
vars['PROJECT_WIKI_URL'] = project.wiki_url || 'None'
|
||||
vars['PROJECT_DISCORD_URL'] = project.discord_url || 'None'
|
||||
vars['PROJECT_ICON_URL'] = project.icon_url || ''
|
||||
vars['PROJECT_ISSUES_URL'] = project.issues_url || 'None'
|
||||
vars['PROJECT_SOURCE_URL'] = project.source_url || 'None'
|
||||
vars['PROJECT_WIKI_URL'] = project.wiki_url || 'None'
|
||||
vars['PROJECT_DISCORD_URL'] = project.discord_url || 'None'
|
||||
|
||||
vars['PROJECT_DOWNLOADS'] = project.downloads.toString()
|
||||
vars['PROJECT_FOLLOWERS'] = project.followers.toString()
|
||||
vars['PROJECT_COLOR'] = project.color?.toString() || ''
|
||||
vars['PROJECT_DOWNLOADS'] = project.downloads.toString()
|
||||
vars['PROJECT_FOLLOWERS'] = project.followers.toString()
|
||||
vars['PROJECT_COLOR'] = project.color?.toString() || ''
|
||||
|
||||
vars['PROJECT_CLIENT_SIDE'] = project.client_side
|
||||
vars['PROJECT_SERVER_SIDE'] = project.server_side
|
||||
vars['PROJECT_CLIENT_SIDE'] = project.client_side
|
||||
vars['PROJECT_SERVER_SIDE'] = project.server_side
|
||||
|
||||
vars['PROJECT_TEAM'] = project.team || 'None'
|
||||
vars['PROJECT_THREAD_ID'] = project.thread_id
|
||||
vars['PROJECT_ORGANIZATION'] = project.organization
|
||||
vars['PROJECT_TEAM'] = project.team || 'None'
|
||||
vars['PROJECT_THREAD_ID'] = project.thread_id
|
||||
vars['PROJECT_ORGANIZATION'] = project.organization
|
||||
|
||||
vars['PROJECT_PUBLISHED'] = project.published
|
||||
vars['PROJECT_UPDATED'] = project.updated
|
||||
vars['PROJECT_APPROVED'] = project.approved
|
||||
vars['PROJECT_QUEUED'] = project.queued
|
||||
vars['PROJECT_PUBLISHED'] = project.published
|
||||
vars['PROJECT_UPDATED'] = project.updated
|
||||
vars['PROJECT_APPROVED'] = project.approved
|
||||
vars['PROJECT_QUEUED'] = project.queued
|
||||
|
||||
vars['PROJECT_LICENSE_ID'] = project.license.id
|
||||
vars['PROJECT_LICENSE_NAME'] = project.license.name
|
||||
vars['PROJECT_LICENSE_URL'] = project.license.url || 'None'
|
||||
vars['PROJECT_LICENSE_ID'] = project.license.id
|
||||
vars['PROJECT_LICENSE_NAME'] = project.license.name
|
||||
vars['PROJECT_LICENSE_URL'] = project.license.url || 'None'
|
||||
|
||||
vars['PROJECT_CATEGORIES'] = arrayOrNone(project.categories)
|
||||
vars['PROJECT_ADDITIONAL_CATEGORIES'] = arrayOrNone(project.additional_categories)
|
||||
vars['PROJECT_GAME_VERSIONS'] = arrayOrNone(project.game_versions)
|
||||
vars['PROJECT_LOADERS'] = arrayOrNone(project.loaders)
|
||||
vars['PROJECT_VERSIONS'] = arrayOrNone(project.versions)
|
||||
vars['PROJECT_CATEGORIES'] = arrayOrNone(project.categories)
|
||||
vars['PROJECT_ADDITIONAL_CATEGORIES'] = arrayOrNone(project.additional_categories)
|
||||
vars['PROJECT_GAME_VERSIONS'] = arrayOrNone(project.game_versions)
|
||||
vars['PROJECT_LOADERS'] = arrayOrNone(project.loaders)
|
||||
vars['PROJECT_VERSIONS'] = arrayOrNone(project.versions)
|
||||
|
||||
vars['PROJECT_CATEGORIES_COUNT'] = project.categories.length.toString()
|
||||
vars['PROJECT_GAME_VERSIONS_COUNT'] = project.game_versions.length.toString()
|
||||
vars['PROJECT_LOADERS_COUNT'] = project.loaders.length.toString()
|
||||
vars['PROJECT_VERSIONS_COUNT'] = project.versions.length.toString()
|
||||
vars['PROJECT_GALLERY_COUNT'] = (project.gallery?.length || 0).toString()
|
||||
vars['PROJECT_DONATION_URLS_COUNT'] = project.donation_urls.length.toString()
|
||||
vars['PROJECT_CATEGORIES_COUNT'] = project.categories.length.toString()
|
||||
vars['PROJECT_GAME_VERSIONS_COUNT'] = project.game_versions.length.toString()
|
||||
vars['PROJECT_LOADERS_COUNT'] = project.loaders.length.toString()
|
||||
vars['PROJECT_VERSIONS_COUNT'] = project.versions.length.toString()
|
||||
vars['PROJECT_GALLERY_COUNT'] = (project.gallery?.length || 0).toString()
|
||||
vars['PROJECT_DONATION_URLS_COUNT'] = project.donation_urls.length.toString()
|
||||
|
||||
project.donation_urls.forEach((donation, index) => {
|
||||
vars[`PROJECT_DONATION_${index}_ID`] = donation.id
|
||||
vars[`PROJECT_DONATION_${index}_PLATFORM`] = donation.platform
|
||||
vars[`PROJECT_DONATION_${index}_URL`] = donation.url
|
||||
})
|
||||
project.donation_urls.forEach((donation, index) => {
|
||||
vars[`PROJECT_DONATION_${index}_ID`] = donation.id
|
||||
vars[`PROJECT_DONATION_${index}_PLATFORM`] = donation.platform
|
||||
vars[`PROJECT_DONATION_${index}_URL`] = donation.url
|
||||
})
|
||||
|
||||
project.gallery?.forEach((image, index) => {
|
||||
vars[`PROJECT_GALLERY_${index}_URL`] = image.url
|
||||
vars[`PROJECT_GALLERY_${index}_TITLE`] = image.title || ''
|
||||
vars[`PROJECT_GALLERY_${index}_DESCRIPTION`] = image.description || ''
|
||||
vars[`PROJECT_GALLERY_${index}_FEATURED`] = image.featured.toString()
|
||||
})
|
||||
project.gallery?.forEach((image, index) => {
|
||||
vars[`PROJECT_GALLERY_${index}_URL`] = image.url
|
||||
vars[`PROJECT_GALLERY_${index}_TITLE`] = image.title || ''
|
||||
vars[`PROJECT_GALLERY_${index}_DESCRIPTION`] = image.description || ''
|
||||
vars[`PROJECT_GALLERY_${index}_FEATURED`] = image.featured.toString()
|
||||
})
|
||||
|
||||
// Static time saving stuff
|
||||
vars[`RULES`] = `[Modrinth's Content Rules](https://modrinth.com/legal/rules)`
|
||||
vars[`TOS`] = `[Terms of Use](https://modrinth.com/legal/terms)`
|
||||
vars[`COPYRIGHT_POLICY`] = `[Copyright Policy](https://modrinth.com/legal/copyright)`
|
||||
vars[`SUPPORT`] =
|
||||
`please visit the [Modrinth Help Center](https://support.modrinth.com/) and click the green bubble to contact support.`
|
||||
vars[`MODPACK_PERMISSIONS_GUIDE`] =
|
||||
`our guide to [Obtaining Modpack Permissions](https://support.modrinth.com/en/articles/8797527-obtaining-modpack-permissions)`
|
||||
vars[`MODPACKS_ON_MODRINTH`] =
|
||||
`[Modpacks on Modrinth](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth)`
|
||||
vars[`ADVANCED_MARKDOWN`] =
|
||||
`[Markdown Formatting Guide](https://support.modrinth.com/en/articles/8801962-advanced-markdown-formatting)`
|
||||
vars[`LICENSING_GUIDE`] =
|
||||
`our guide to [Licensing your Mods](https://modrinth.com/news/article/licensing-guide)`
|
||||
// Static time saving stuff
|
||||
vars[`RULES`] = `[Modrinth's Content Rules](https://modrinth.com/legal/rules)`
|
||||
vars[`TOS`] = `[Terms of Use](https://modrinth.com/legal/terms)`
|
||||
vars[`COPYRIGHT_POLICY`] = `[Copyright Policy](https://modrinth.com/legal/copyright)`
|
||||
vars[`SUPPORT`] =
|
||||
`please visit the [Modrinth Help Center](https://support.modrinth.com/) and click the green bubble to contact support.`
|
||||
vars[`MODPACK_PERMISSIONS_GUIDE`] =
|
||||
`our guide to [Obtaining Modpack Permissions](https://support.modrinth.com/en/articles/8797527-obtaining-modpack-permissions)`
|
||||
vars[`MODPACKS_ON_MODRINTH`] =
|
||||
`[Modpacks on Modrinth](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth)`
|
||||
vars[`ADVANCED_MARKDOWN`] =
|
||||
`[Markdown Formatting Guide](https://support.modrinth.com/en/articles/8801962-advanced-markdown-formatting)`
|
||||
vars[`LICENSING_GUIDE`] =
|
||||
`our guide to [Licensing your Mods](https://modrinth.com/news/article/licensing-guide)`
|
||||
|
||||
// Navigation related variables
|
||||
vars[`PROJECT_PERMANENT_LINK`] = `https://modrinth.com/project/${project.id}`
|
||||
vars[`PROJECT_SETTINGS_LINK`] = `https://modrinth.com/project/${project.id}/settings`
|
||||
vars[`PROJECT_SETTINGS_FLINK`] = `[Settings](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_TITLE_FLINK`] = `[Name](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_SLUG_FLINK`] = `[URL](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_SUMMARY_FLINK`] = `[Summary](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_ENVIRONMENT_FLINK`] =
|
||||
`[Environment Information](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_TAGS_LINK`] = `https://modrinth.com/project/${project.id}/settings/tags`
|
||||
vars[`PROJECT_TAGS_FLINK`] = `[Tags](https://modrinth.com/project/${project.id}/settings/tags)`
|
||||
vars[`PROJECT_DESCRIPTION_LINK`] =
|
||||
`https://modrinth.com/project/${project.id}/settings/description`
|
||||
vars[`PROJECT_DESCRIPTION_FLINK`] =
|
||||
`[Description](https://modrinth.com/project/${project.id}/settings/description)`
|
||||
vars[`PROJECT_LICENSE_LINK`] = `https://modrinth.com/project/${project.id}/license`
|
||||
vars[`PROJECT_LICENSE_FLINK`] = `[License](https://modrinth.com/project/${project.id}/license)`
|
||||
vars[`PROJECT_LINKS_LINK`] = `https://modrinth.com/project/${project.id}/settings/links`
|
||||
vars[`PROJECT_LINKS_FLINK`] =
|
||||
`[External Links](https://modrinth.com/project/${project.id}/settings/links)`
|
||||
vars[`PROJECT_GALLERY_LINK`] = `https://modrinth.com/project/${project.id}/gallery`
|
||||
vars[`PROJECT_GALLERY_FLINK`] = `[Gallery](https://modrinth.com/project/${project.id}/gallery)`
|
||||
vars[`PROJECT_VERSIONS_LINK`] = `https://modrinth.com/project/${project.id}/versions`
|
||||
vars[`PROJECT_VERSIONS_FLINK`] = `[Versions](https://modrinth.com/project/${project.id}/versions)`
|
||||
vars[`PROJECT_MODERATION_LINK`] = `https://modrinth.com/project/${project.id}/moderation`
|
||||
vars[`PROJECT_MODERATION_FLINK`] =
|
||||
`[moderation tab](https://modrinth.com/project/${project.id}/moderation)`
|
||||
// Navigation related variables
|
||||
vars[`PROJECT_PERMANENT_LINK`] = `https://modrinth.com/project/${project.id}`
|
||||
vars[`PROJECT_SETTINGS_LINK`] = `https://modrinth.com/project/${project.id}/settings`
|
||||
vars[`PROJECT_SETTINGS_FLINK`] = `[Settings](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_TITLE_FLINK`] = `[Name](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_SLUG_FLINK`] = `[URL](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_SUMMARY_FLINK`] = `[Summary](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_ENVIRONMENT_FLINK`] =
|
||||
`[Environment Information](https://modrinth.com/project/${project.id}/settings)`
|
||||
vars[`PROJECT_TAGS_LINK`] = `https://modrinth.com/project/${project.id}/settings/tags`
|
||||
vars[`PROJECT_TAGS_FLINK`] = `[Tags](https://modrinth.com/project/${project.id}/settings/tags)`
|
||||
vars[`PROJECT_DESCRIPTION_LINK`] =
|
||||
`https://modrinth.com/project/${project.id}/settings/description`
|
||||
vars[`PROJECT_DESCRIPTION_FLINK`] =
|
||||
`[Description](https://modrinth.com/project/${project.id}/settings/description)`
|
||||
vars[`PROJECT_LICENSE_LINK`] = `https://modrinth.com/project/${project.id}/license`
|
||||
vars[`PROJECT_LICENSE_FLINK`] = `[License](https://modrinth.com/project/${project.id}/license)`
|
||||
vars[`PROJECT_LINKS_LINK`] = `https://modrinth.com/project/${project.id}/settings/links`
|
||||
vars[`PROJECT_LINKS_FLINK`] =
|
||||
`[External Links](https://modrinth.com/project/${project.id}/settings/links)`
|
||||
vars[`PROJECT_GALLERY_LINK`] = `https://modrinth.com/project/${project.id}/gallery`
|
||||
vars[`PROJECT_GALLERY_FLINK`] = `[Gallery](https://modrinth.com/project/${project.id}/gallery)`
|
||||
vars[`PROJECT_VERSIONS_LINK`] = `https://modrinth.com/project/${project.id}/versions`
|
||||
vars[`PROJECT_VERSIONS_FLINK`] = `[Versions](https://modrinth.com/project/${project.id}/versions)`
|
||||
vars[`PROJECT_MODERATION_LINK`] = `https://modrinth.com/project/${project.id}/moderation`
|
||||
vars[`PROJECT_MODERATION_FLINK`] =
|
||||
`[moderation tab](https://modrinth.com/project/${project.id}/moderation)`
|
||||
|
||||
return vars
|
||||
return vars
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
{
|
||||
"extends": "tsconfig/vue.json",
|
||||
"include": [".", ".eslintrc.js"],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom"],
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"types": ["@stripe/stripe-js"]
|
||||
"extends": "@modrinth/tooling-config/typescript/vue.json"
|
||||
}
|
||||
|
||||
4
packages/moderation/vite-env.d.ts
vendored
4
packages/moderation/vite-env.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module '*.md?raw' {
|
||||
const content: string
|
||||
export default content
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
20
packages/tooling-config/eslint/base.mjs
Normal file
20
packages/tooling-config/eslint/base.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import eslint from '@eslint/js'
|
||||
import prettierEslint from 'eslint-plugin-prettier/recommended'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
prettierEslint,
|
||||
...common,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
warnOnUnsupportedTypeScriptVersion: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['node_modules/', 'dist/', 'build/'],
|
||||
},
|
||||
)
|
||||
20
packages/tooling-config/eslint/common.mjs
Normal file
20
packages/tooling-config/eslint/common.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||
|
||||
export default [
|
||||
{
|
||||
plugins: {
|
||||
'simple-import-sort': simpleImportSort,
|
||||
},
|
||||
rules: {
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-type-alias': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/prefer-literal-enum-member': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
31
packages/tooling-config/eslint/nuxt.mjs
Normal file
31
packages/tooling-config/eslint/nuxt.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { fixupPluginRules } from '@eslint/compat'
|
||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
||||
import turboPlugin from 'eslint-plugin-turbo'
|
||||
import common from './common.mjs'
|
||||
|
||||
export const configurationNuxtToAppend = [
|
||||
...common,
|
||||
{
|
||||
name: 'turbo',
|
||||
plugins: {
|
||||
turbo: fixupPluginRules(turboPlugin),
|
||||
},
|
||||
rules: {
|
||||
'turbo/no-undeclared-env-vars': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'modrinth',
|
||||
rules: {
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
warnOnUnsupportedTypeScriptVersion: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default createConfigForNuxt().append(configurationNuxtToAppend)
|
||||
35
packages/tooling-config/package.json
Normal file
35
packages/tooling-config/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@modrinth/tooling-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Unified tooling configuration for Modrinth monorepo",
|
||||
"main": "./index.js",
|
||||
"exports": {
|
||||
"./eslint/*": "./eslint/*",
|
||||
"./typescript/*": "./typescript/*",
|
||||
"./prettier.config.js": "./prettier.config.js",
|
||||
"./prettier.nuxt.config.js": "./prettier.nuxt.config.js"
|
||||
},
|
||||
"files": [
|
||||
"eslint/",
|
||||
"typescript/",
|
||||
"prettier/",
|
||||
"index.js"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": ">=5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"globals": "^16.3.0",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
}
|
||||
}
|
||||
10
packages/tooling-config/prettier.config.js
Normal file
10
packages/tooling-config/prettier.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @see https://prettier.io/docs/configuration
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
11
packages/tooling-config/prettier.nuxt.config.js
Normal file
11
packages/tooling-config/prettier.nuxt.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @see https://prettier.io/docs/configuration
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
plugins: ["prettier-plugin-tailwindcss"]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user