Revert "Author Validation Improvements (#3970)" (#4024)

This reverts commit 44267619b6.
This commit is contained in:
Prospector
2025-07-19 15:04:47 -07:00
committed by GitHub
parent bcf46d440b
commit 0f755b94ce
23 changed files with 661 additions and 2213 deletions

View File

@@ -1,7 +0,0 @@
import type { Nag } from '../types/nags'
import { coreNags } from './nags/core'
import { descriptionNags } from './nags/description'
import { linksNags } from './nags/links'
import { tagsNags } from './nags/tags'
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]

View File

@@ -1,116 +0,0 @@
import { defineMessages } from '@vintl/vintl'
export default defineMessages({
moderatorFeedbackTitle: {
id: 'nags.moderator-feedback.title',
defaultMessage: 'Review moderator feedback',
},
moderatorFeedbackDescription: {
id: 'nags.moderator-feedback.description',
defaultMessage:
'Review any feedback from moderators regarding your project before resubmitting.',
},
moderationTitle: {
id: 'nags.moderation.title',
defaultMessage: 'Visit moderation thread',
},
uploadVersionTitle: {
id: 'nags.upload-version.title',
defaultMessage: 'Upload a version',
},
uploadVersionDescription: {
id: 'nags.upload-version.description',
defaultMessage: 'At least one version is required for a project to be submitted for review.',
},
versionsTitle: {
id: 'nags.versions.title',
defaultMessage: 'Visit versions page',
},
addDescriptionTitle: {
id: 'nags.add-description.title',
defaultMessage: 'Add a description',
},
addDescriptionDescription: {
id: 'nags.add-description.description',
defaultMessage:
"A description that clearly describes the project's purpose and function is required.",
},
settingsDescriptionTitle: {
id: 'nags.settings.description.title',
defaultMessage: 'Visit description settings',
},
addIconTitle: {
id: 'nags.add-icon.title',
defaultMessage: 'Add an icon',
},
addIconDescription: {
id: 'nags.add-icon.description',
defaultMessage:
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
},
settingsTitle: {
id: 'nags.settings.title',
defaultMessage: 'Visit general settings',
},
featureGalleryImageTitle: {
id: 'nags.feature-gallery-image.title',
defaultMessage: 'Feature a gallery image',
},
featureGalleryImageDescription: {
id: 'nags.feature-gallery-image.description',
defaultMessage: 'Featured gallery images may be the first impression of many users.',
},
galleryTitle: {
id: 'nags.gallery.title',
defaultMessage: 'Visit gallery page',
},
selectTagsTitle: {
id: 'nags.select-tags.title',
defaultMessage: 'Select tags',
},
selectTagsDescription: {
id: 'nags.select-tags.description',
defaultMessage: 'Select all tags that apply to your project.',
},
settingsTagsTitle: {
id: 'nags.settings.tags.title',
defaultMessage: 'Visit tag settings',
},
addLinksTitle: {
id: 'nags.add-links.title',
defaultMessage: 'Add external links',
},
addLinksDescription: {
id: 'nags.add-links.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
},
settingsLinksTitle: {
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
},
selectEnvironmentsTitle: {
id: 'nags.select-environments.title',
defaultMessage: 'Select supported environments',
},
selectEnvironmentsDescription: {
id: 'nags.select-environments.description',
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
},
settingsEnvironmentsTitle: {
id: 'nags.settings.environments.title',
defaultMessage: 'Visit general settings',
},
selectLicenseTitle: {
id: 'nags.select-license.title',
defaultMessage: 'Select license',
},
selectLicenseDescription: {
id: 'nags.select-license.description',
defaultMessage: 'Select the license your {projectType} is distributed under.',
},
settingsLicenseTitle: {
id: 'nags.settings.license.title',
defaultMessage: 'Visit license settings',
},
})

View File

@@ -1,151 +0,0 @@
import type { Nag, NagContext } from '../../types/nags'
import { formatProjectType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import messages from './core.i18n'
export const coreNags: Nag[] = [
{
id: 'moderator-feedback',
title: messages.moderatorFeedbackTitle,
description: messages.moderatorFeedbackDescription,
status: 'suggestion',
shouldShow: (context: NagContext) =>
context.tags.rejectedStatuses.includes(context.project.status),
link: {
path: 'moderation',
title: messages.moderationTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
},
},
{
id: 'upload-version',
title: messages.uploadVersionTitle,
description: messages.uploadVersionDescription,
status: 'required',
shouldShow: (context: NagContext) => context.versions.length < 1,
link: {
path: 'versions',
title: messages.versionsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
},
},
{
id: 'add-description',
title: messages.addDescriptionTitle,
description: messages.addDescriptionDescription,
status: 'required',
shouldShow: (context: NagContext) =>
context.project.body === '' || context.project.body.startsWith('# Placeholder description'),
link: {
path: 'settings/description',
title: messages.settingsDescriptionTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
{
id: 'add-icon',
title: messages.addIconTitle,
description: messages.addIconDescription,
status: 'suggestion',
shouldShow: (context: NagContext) => !context.project.icon_url,
link: {
path: 'settings',
title: messages.settingsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'feature-gallery-image',
title: messages.featureGalleryImageTitle,
description: messages.featureGalleryImageDescription,
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: messages.galleryTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
},
},
{
id: 'select-tags',
title: messages.selectTagsTitle,
description: messages.selectTagsDescription,
status: 'suggestion',
shouldShow: (context: NagContext) =>
context.project.versions.length > 0 && context.project.categories.length < 1,
link: {
path: 'settings/tags',
title: messages.settingsTagsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'add-links',
title: messages.addLinksTitle,
description: messages.addLinksDescription,
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: messages.settingsLinksTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'select-environments',
title: messages.selectEnvironmentsTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(messages.selectEnvironmentsDescription, {
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: messages.settingsEnvironmentsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'select-license',
title: messages.selectLicenseTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(messages.selectLicenseDescription, {
projectType: formatProjectType(context.project.project_type).toLowerCase(),
})
},
status: 'required',
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
link: {
path: 'settings/license',
title: messages.settingsLicenseTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
},
},
]

View File

@@ -1,88 +0,0 @@
import { defineMessages } from '@vintl/vintl'
export default defineMessages({
descriptionTooShortTitle: {
id: 'nags.description-too-short.title',
defaultMessage: 'Description may be insufficient',
},
descriptionTooShortDescription: {
id: 'nags.description-too-short.description',
defaultMessage:
"Your description is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project.",
},
longHeadersTitle: {
id: 'nags.long-headers.title',
defaultMessage: 'Headers are too long',
},
longHeadersDescription: {
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.',
},
summaryTooShortTitle: {
id: 'nags.summary-too-short.title',
defaultMessage: 'Summary may be insufficient',
},
summaryTooShortDescription: {
id: 'nags.summary-too-short.description',
defaultMessage:
"Your summary is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project.",
},
minecraftTitleClauseTitle: {
id: 'nags.minecraft-title-clause.title',
defaultMessage: 'Title contains "Minecraft"',
},
minecraftTitleClauseDescription: {
id: 'nags.minecraft-title-clause.description',
defaultMessage:
'Please remove "Minecraft" from your title. You cannot use "Minecraft" in your title for legal reasons.',
},
titleContainsTechnicalInfoTitle: {
id: 'nags.title-contains-technical-info.title',
defaultMessage: 'Title contains loader or version info',
},
titleContainsTechnicalInfoDescription: {
id: 'nags.title-contains-technical-info.description',
defaultMessage:
'Removing these helps keep titles clean and makes your project easier to find. Version and loader information is automatically displayed alongside your project.',
},
summarySameAsTitleTitle: {
id: 'nags.summary-same-as-title.title',
defaultMessage: 'Summary is project name',
},
summarySameAsTitleDescription: {
id: 'nags.summary-same-as-title.description',
defaultMessage:
"Your summary is the same as your project name. Please change it. It's recommended to have a unique summary to provide more context about your project.",
},
imageHeavyDescriptionTitle: {
id: 'nags.image-heavy-description.title',
defaultMessage: 'Description is mostly images',
},
imageHeavyDescriptionDescription: {
id: 'nags.image-heavy-description.description',
defaultMessage:
'Please add more descriptive text to help users understand your project, especially those using screen readers or with slow internet connections.',
},
missingAltTextTitle: {
id: 'nags.missing-alt-text.title',
defaultMessage: 'Images missing alt text',
},
missingAltTextDescription: {
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.',
},
editDescriptionTitle: {
id: 'nags.edit-description.title',
defaultMessage: 'Edit description',
},
editSummaryTitle: {
id: 'nags.edit-summary.title',
defaultMessage: 'Edit summary',
},
editTitleTitle: {
id: 'nags.edit-title.title',
defaultMessage: 'Edit title',
},
})

View File

@@ -1,226 +0,0 @@
import type { Nag, NagContext } from '../../types/nags'
import { useVIntl } from '@vintl/vintl'
import messages from './description.i18n'
export const MIN_DESCRIPTION_CHARS = 500
export const MAX_HEADER_LENGTH = 100
export const MIN_SUMMARY_CHARS = 125
function analyzeHeaderLength(markdown: string): { hasLongHeaders: boolean; longHeaders: string[] } {
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
const headerRegex = /^(#{1,3})\s+(.+)$/gm
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
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)
const hasSentenceEnders = sentenceEnders.test(headerText)
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
const hasMultipleSentences = sentences.length > 1
if (hasSentenceEnders || isVeryLong || hasMultipleSentences) {
longHeaders.push(headerText)
}
})
return {
hasLongHeaders: longHeaders.length > 0,
longHeaders,
}
}
function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyAltText: boolean } {
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
const htmlImageRegex = /<img[^>]*>/gi
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
const totalImages = images.length + htmlImages.length
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
const textWithoutImages = withoutCodeBlocks
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
.replace(/<img[^>]*>/gi, '')
.replace(/\s+/g, ' ')
.trim()
const textLength = textWithoutImages.length
const imageHeavy = textLength < 100 || (totalImages >= 3 && textLength < 200)
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 }
}
export const descriptionNags: Nag[] = [
{
id: 'description-too-short',
title: messages.descriptionTooShortTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(messages.descriptionTooShortDescription, {
length: context.project.body?.length || 0,
minChars: MIN_DESCRIPTION_CHARS,
})
},
status: 'warning',
shouldShow: (context: NagContext) => {
const bodyLength = context.project.body?.trim()?.length || 0
return bodyLength < MIN_DESCRIPTION_CHARS && bodyLength !== 0
},
link: {
path: 'settings/description',
title: messages.editDescriptionTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
{
id: 'long-headers',
title: messages.longHeadersTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
const count = longHeaders.length
return formatMessage(messages.longHeadersDescription, {
count,
})
},
status: 'warning',
shouldShow: (context: NagContext) => {
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
return hasLongHeaders
},
link: {
path: 'settings/description',
title: messages.editDescriptionTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
{
id: 'summary-too-short',
title: messages.summaryTooShortTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(messages.summaryTooShortDescription, {
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: messages.editSummaryTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'minecraft-title-clause',
title: messages.minecraftTitleClauseTitle,
description: messages.minecraftTitleClauseDescription,
status: 'required',
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: messages.editTitleTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'title-contains-technical-info',
title: messages.titleContainsTechnicalInfoTitle,
description: messages.titleContainsTechnicalInfoDescription,
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))
return hasLoader || hasVersionPattern
},
link: {
path: 'settings',
title: messages.editTitleTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'summary-same-as-title',
title: messages.summarySameAsTitleTitle,
description: messages.summarySameAsTitleDescription,
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: messages.editSummaryTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'image-heavy-description',
title: messages.imageHeavyDescriptionTitle,
description: messages.imageHeavyDescriptionDescription,
status: 'warning',
shouldShow: (context: NagContext) => {
const { imageHeavy } = analyzeImageContent(context.project.body || '')
return imageHeavy
},
link: {
path: 'settings/description',
title: messages.editDescriptionTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
{
id: 'missing-alt-text',
title: messages.missingAltTextTitle,
description: messages.missingAltTextDescription,
status: 'warning',
shouldShow: (context: NagContext) => {
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
return hasEmptyAltText
},
link: {
path: 'settings/description',
title: messages.editDescriptionTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
]

View File

@@ -1,4 +0,0 @@
export * from './core'
export * from './links'
export * from './description'
export * from './tags'

View File

@@ -1,48 +0,0 @@
import { defineMessages } from '@vintl/vintl'
export default defineMessages({
verifyExternalLinksTitle: {
id: 'nags.verify-external-links.title',
defaultMessage: 'Verify external links',
},
verifyExternalLinksDescription: {
id: 'nags.verify-external-links.description',
defaultMessage:
"Some of your external links may be using domains that aren't recognized as common for their link type.",
},
invalidLicenseUrlTitle: {
id: 'nags.invalid-license-url.title',
defaultMessage: 'Invalid license URL',
},
invalidLicenseUrlDescriptionDefault: {
id: 'nags.invalid-license-url.description.default',
defaultMessage: 'License URL is invalid.',
},
invalidLicenseUrlDescriptionDomain: {
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 to the actual license text or legal documentation, not social media, gaming platforms etc.',
},
invalidLicenseUrlDescriptionMalformed: {
id: 'nags.invalid-license-url.description.malformed',
defaultMessage:
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
},
gplLicenseSourceRequiredTitle: {
id: 'nags.gpl-license-source-required.title',
defaultMessage: 'GPL license requires source',
},
gplLicenseSourceRequiredDescription: {
id: 'nags.gpl-license-source-required.description',
defaultMessage:
'Your {projectType} uses a GPL license which requires source code to be available. Please provide a source code link or consider using a different license.',
},
visitLinksSettingsTitle: {
id: 'nags.visit-links-settings.title',
defaultMessage: 'Visit links settings',
},
editLicenseTitle: {
id: 'nags.edit-license.title',
defaultMessage: 'Edit license',
},
})

View File

@@ -1,155 +0,0 @@
import type { Nag, NagContext } from '../../types/nags'
import { formatProjectType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import messages from './links.i18n'
export const commonLinkDomains = {
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org'],
discord: ['discord.gg', 'discord.com'],
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',
],
}
export function isCommonUrl(url: string | undefined, commonDomains: string[]): boolean {
if (!url) return false
try {
const domain = new URL(url).hostname.toLowerCase()
return commonDomains.some((allowed) => domain.includes(allowed))
} catch {
return false
}
}
export function isUncommonLicenseUrl(url: string | undefined, domains: string[]): boolean {
if (!url) return false
try {
const domain = new URL(url).hostname.toLowerCase()
return domains.some((uncommonDomain) => domain.includes(uncommonDomain))
} catch {
return false
}
}
export const linksNags: Nag[] = [
{
id: 'verify-external-links',
title: messages.verifyExternalLinksTitle,
description: messages.verifyExternalLinksDescription,
status: 'warning',
shouldShow: (context: NagContext) => {
return (
!isCommonUrl(context.project.source_url, commonLinkDomains.source) ||
!isCommonUrl(context.project.issues_url, commonLinkDomains.issues) ||
!isCommonUrl(context.project.discord_url, commonLinkDomains.discord)
)
},
link: {
path: 'settings/links',
title: messages.visitLinksSettingsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'invalid-license-url',
title: messages.invalidLicenseUrlTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const licenseUrl = context.project.license.url
if (!licenseUrl) {
return formatMessage(messages.invalidLicenseUrlDescriptionDefault)
}
try {
const domain = new URL(licenseUrl).hostname.toLowerCase()
return formatMessage(messages.invalidLicenseUrlDescriptionDomain, { domain })
} catch {
return formatMessage(messages.invalidLicenseUrlDescriptionMalformed)
}
},
status: 'required',
shouldShow: (context: NagContext) => {
const licenseUrl = context.project.license.url
if (!licenseUrl) return false
const isBlocklisted = isUncommonLicenseUrl(licenseUrl, commonLinkDomains.licenseBlocklist)
try {
new URL(licenseUrl)
return isBlocklisted
} catch {
return true
}
},
link: {
path: 'settings',
title: messages.editLicenseTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'gpl-license-source-required',
title: messages.gplLicenseSourceRequiredTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
return formatMessage(messages.gplLicenseSourceRequiredDescription, {
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',
]
const isGplLicense = gplLicenses.includes(context.project.license.id)
const hasSourceUrl = !!context.project.source_url
return isGplLicense && !hasSourceUrl
},
link: {
path: 'settings/links',
title: messages.visitLinksSettingsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
]

View File

@@ -1,35 +0,0 @@
import { defineMessages } from '@vintl/vintl'
export default defineMessages({
tooManyTagsTitle: {
id: 'nags.too-many-tags.title',
defaultMessage: 'Too many tags selected',
},
tooManyTagsDescription: {
id: 'nags.too-many-tags.description',
defaultMessage:
"You've selected {tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover.",
},
multipleResolutionTagsTitle: {
id: 'nags.multiple-resolution-tags.title',
defaultMessage: 'Multiple resolution tags selected',
},
multipleResolutionTagsDescription: {
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.",
},
allTagsSelectedTitle: {
id: 'nags.all-tags-selected.title',
defaultMessage: 'All tags selected',
},
allTagsSelectedDescription: {
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 truly apply to your project.",
},
editTagsTitle: {
id: 'nags.edit-tags.title',
defaultMessage: 'Edit tags',
},
})

View File

@@ -1,107 +0,0 @@
import type { Project } from '@modrinth/utils'
import type { Nag, NagContext } from '../../types/nags'
import { useVIntl } from '@vintl/vintl'
import messages from './tags.i18n'
function getCategories(
project: Project & { actualProjectType: string },
tags: {
categories?: {
project_type: string
}[]
},
) {
return (
tags.categories?.filter(
(category: { project_type: string }) => category.project_type === project.actualProjectType,
) ?? []
)
}
export const tagsNags: Nag[] = [
{
id: 'too-many-tags',
title: messages.tooManyTagsTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const tagCount =
context.project.categories.length + (context.project.additional_categories?.length || 0)
return formatMessage(messages.tooManyTagsDescription, {
tagCount,
})
},
status: 'warning',
shouldShow: (context: NagContext) => {
const tagCount =
context.project.categories.length + (context.project.additional_categories?.length || 0)
return tagCount > 5
},
link: {
path: 'settings/tags',
title: messages.editTagsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'multiple-resolution-tags',
title: messages.multipleResolutionTagsTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const resolutionTags = context.project.categories.filter((tag: string) =>
['16x', '32x', '48x', '64x', '128x', '256x', '512x', '1024x'].includes(tag),
)
return formatMessage(messages.multipleResolutionTagsDescription, {
count: resolutionTags.length,
tags: resolutionTags.join(', '),
})
},
status: 'warning',
shouldShow: (context: NagContext) => {
if (context.project.project_type !== 'resourcepack') return false
const resolutionTags = context.project.categories.filter((tag: string) =>
['16x', '32x', '48x', '64x', '128x', '256x', '512x', '1024x'].includes(tag),
)
return resolutionTags.length > 1
},
link: {
path: 'settings/tags',
title: messages.editTagsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'all-tags-selected',
title: messages.allTagsSelectedTitle,
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const categoriesForProjectType = getCategories(
context.project as Project & { actualProjectType: string },
context.tags,
)
const totalAvailableTags = categoriesForProjectType.length
return formatMessage(messages.allTagsSelectedDescription, {
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
},
link: {
path: 'settings/tags',
title: messages.editTagsTitle,
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
]