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,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',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user