fix: links page + add more validations
This commit is contained in:
parent
729d584757
commit
27caf336cc
@ -1360,8 +1360,6 @@ const currentMember = computed(() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Current member:", val);
|
|
||||||
|
|
||||||
return val;
|
return val;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -170,17 +170,17 @@ const wikiUrl = ref(props.project.wiki_url);
|
|||||||
const discordUrl = ref(props.project.discord_url);
|
const discordUrl = ref(props.project.discord_url);
|
||||||
|
|
||||||
const isIssuesUrlCommon = computed(() => {
|
const isIssuesUrlCommon = computed(() => {
|
||||||
if (issuesUrl.value.trim() === "") return true;
|
if (issuesUrl.value?.trim().length ?? 0 === 0) return true;
|
||||||
return isCommonUrl(issuesUrl.value, commonLinkDomains.issues);
|
return isCommonUrl(issuesUrl.value, commonLinkDomains.issues);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSourceUrlCommon = computed(() => {
|
const isSourceUrlCommon = computed(() => {
|
||||||
if (sourceUrl.value.trim() === "") return true;
|
if (sourceUrl.value?.trim().length ?? 0 === 0) return true;
|
||||||
return isCommonUrl(sourceUrl.value, commonLinkDomains.source);
|
return isCommonUrl(sourceUrl.value, commonLinkDomains.source);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDiscordUrlCommon = computed(() => {
|
const isDiscordUrlCommon = computed(() => {
|
||||||
if (discordUrl.value.trim() === "") return true;
|
if (discordUrl.value?.trim().length ?? 0 === 0) return true;
|
||||||
return isCommonUrl(discordUrl.value, commonLinkDomains.discord);
|
return isCommonUrl(discordUrl.value, commonLinkDomains.discord);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,36 @@ import type { Nag, NagContext } from '../../types/nags'
|
|||||||
export const MIN_DESCRIPTION_CHARS = 500
|
export const MIN_DESCRIPTION_CHARS = 500
|
||||||
export const MIN_SUMMARY_CHARS = 125
|
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 > 100
|
||||||
|
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 } {
|
function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyAltText: boolean } {
|
||||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
||||||
|
|
||||||
@ -53,6 +83,26 @@ export const descriptionNags: Nag[] = [
|
|||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'long-headers',
|
||||||
|
title: 'Headers are too long',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||||
|
const count = longHeaders.length
|
||||||
|
|
||||||
|
return `${count} header${count > 1 ? 's' : ''} in your description ${count > 1 ? 'are' : 'is'} too long. Headers should be concise and act as section titles, not full sentences.`
|
||||||
|
},
|
||||||
|
status: 'warning',
|
||||||
|
shouldShow: (context: NagContext) => {
|
||||||
|
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
||||||
|
return hasLongHeaders
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
path: 'settings/description',
|
||||||
|
title: 'Edit description',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'summary-too-short',
|
id: 'summary-too-short',
|
||||||
title: 'Summary may be insufficient',
|
title: 'Summary may be insufficient',
|
||||||
@ -69,6 +119,64 @@ export const descriptionNags: Nag[] = [
|
|||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'minecraft-title-clause',
|
||||||
|
title: 'Title contains "Minecraft"',
|
||||||
|
description: (context: NagContext) =>
|
||||||
|
`Please remove "Minecraft" from your title. You cannot use "Minecraft" in your title for legal reasons.`,
|
||||||
|
status: 'required',
|
||||||
|
shouldShow: (context: NagContext) => {
|
||||||
|
const title = context.project.title?.toLowerCase() || ''
|
||||||
|
return title.includes('minecraft') && title.length > 0
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
path: 'settings',
|
||||||
|
title: 'Edit title',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'title-contains-technical-info',
|
||||||
|
title: 'Title contains loader or version info',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
return `Removing these helps keep titles clean and makes your project 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: any) => 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: 'Edit title',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'summary-same-as-title',
|
||||||
|
title: 'Summary is project name',
|
||||||
|
description: (context: NagContext) =>
|
||||||
|
`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.`,
|
||||||
|
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: 'Edit summary',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'image-heavy-description',
|
id: 'image-heavy-description',
|
||||||
title: 'Description is mostly images',
|
title: 'Description is mostly images',
|
||||||
|
|||||||
@ -5,6 +5,25 @@ export const commonLinkDomains = {
|
|||||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
||||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org'],
|
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org'],
|
||||||
discord: ['discord.gg', 'discord.com'],
|
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 {
|
export function isCommonUrl(url: string | undefined, commonDomains: string[]): boolean {
|
||||||
@ -18,6 +37,16 @@ export function isCommonUrl(url: string | undefined, commonDomains: string[]): b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[] = [
|
export const linksNags: Nag[] = [
|
||||||
{
|
{
|
||||||
id: 'verify-external-links',
|
id: 'verify-external-links',
|
||||||
@ -38,6 +67,40 @@ export const linksNags: Nag[] = [
|
|||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'invalid-license-url',
|
||||||
|
title: 'Invalid license URL',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
const licenseUrl = context.project.license.url
|
||||||
|
if (!licenseUrl) return 'License URL is invalid.'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
||||||
|
return `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.`
|
||||||
|
} catch {
|
||||||
|
return '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, commonLinkDomains.licenseBlocklist)
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(licenseUrl)
|
||||||
|
return isBlocklisted
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
path: 'settings',
|
||||||
|
title: 'Edit license',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'gpl-license-source-required',
|
id: 'gpl-license-source-required',
|
||||||
title: 'GPL license requires source',
|
title: 'GPL license requires source',
|
||||||
|
|||||||
64
packages/moderation/data/nags/tags.ts
Normal file
64
packages/moderation/data/nags/tags.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { Nag, NagContext } from '../../types/nags'
|
||||||
|
|
||||||
|
export const tagsNags: Nag[] = [
|
||||||
|
{
|
||||||
|
id: 'too-many-tags',
|
||||||
|
title: 'Too many tags selected',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
const tagCount = context.project.categories.length
|
||||||
|
return `You've selected ${tagCount} tags. Consider reducing to 3 or fewer to keep your project focused and easier to discover.`
|
||||||
|
},
|
||||||
|
status: 'warning',
|
||||||
|
shouldShow: (context: NagContext) => {
|
||||||
|
return context.project.categories.length > 3
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
path: 'settings/tags',
|
||||||
|
title: 'Edit tags',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multiple-resolution-tags',
|
||||||
|
title: 'Multiple resolution tags selected',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
const resolutionTags = context.project.categories.filter((tag: string) =>
|
||||||
|
['16x', '32x', '48x', '64x', '128x', '256x', '512x', '1024x'].includes(tag),
|
||||||
|
)
|
||||||
|
return `You've selected ${resolutionTags.length} resolution tags (${resolutionTags.join(', ')}). Resource packs should typically only have one resolution tag that matches their primary resolution.`
|
||||||
|
},
|
||||||
|
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: 'Edit tags',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'all-tags-selected',
|
||||||
|
title: 'All tags selected',
|
||||||
|
description: (context: NagContext) => {
|
||||||
|
const totalAvailableTags = context.tags.categories?.length || 0
|
||||||
|
return `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.`
|
||||||
|
},
|
||||||
|
status: 'required',
|
||||||
|
shouldShow: (context: NagContext) => {
|
||||||
|
const totalAvailableTags = context.tags.categories?.length || 0
|
||||||
|
const selectedTags = context.project.categories.length
|
||||||
|
return totalAvailableTags > 0 && selectedTags === totalAvailableTags
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
path: 'settings/tags',
|
||||||
|
title: 'Edit tags',
|
||||||
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
Loading…
x
Reference in New Issue
Block a user