fix: links page + add more validations

This commit is contained in:
Calum 2025-07-13 14:34:56 +01:00
parent 729d584757
commit 27caf336cc
5 changed files with 238 additions and 5 deletions

View File

@ -1360,8 +1360,6 @@ const currentMember = computed(() => {
}; };
} }
console.log("Current member:", val);
return val; return val;
}); });

View File

@ -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);
}); });

View File

@ -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',

View File

@ -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',

View 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',
},
},
]