diff --git a/src/app/skills/[slug]/page.tsx b/src/app/skills/[slug]/page.tsx index f9cb851f..c393a6d3 100644 --- a/src/app/skills/[slug]/page.tsx +++ b/src/app/skills/[slug]/page.tsx @@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { NextBestActionPanel } from "@/components/next-best-action-panel"; import { SkillShareActions } from "@/components/skill-share-actions"; +import { RelatedKits } from "@/components/related-kits"; import { buildSkillIssueBodyTemplate } from "@/lib/reportIssue"; import Link from "next/link"; @@ -150,28 +151,8 @@ export default async function SkillPage({ - {/* Other Skills */} - {allSkills.length > 0 && ( -
-

Other Skills

-
- {allSkills.map((other) => ( - -

- {other.name} -

-

- {other.description} -

- - ))} -
-
- )} + {/* Related Kits */} + {/* Footer */} diff --git a/src/components/related-kits.tsx b/src/components/related-kits.tsx new file mode 100644 index 00000000..90de2fc3 --- /dev/null +++ b/src/components/related-kits.tsx @@ -0,0 +1,91 @@ +import Link from "next/link"; +import { Skill } from "@/lib/data"; +import { Badge } from "@/components/ui/badge"; + +type Props = { + currentSkill: Skill; + allSkills: Skill[]; + maxResults?: number; +}; + +/** + * Calculates the number of overlapping tags between two skills + */ +function calculateTagOverlap(skill1Tags: string[], skill2Tags: string[]): number { + const tags1 = new Set(skill1Tags); + return skill2Tags.filter(tag => tags1.has(tag)).length; +} + +/** + * Finds and displays related skills based on tag overlap + */ +export function RelatedKits({ currentSkill, allSkills, maxResults = 5 }: Props) { + // Filter out current skill and calculate tag overlap + const relatedSkills = allSkills + .filter((skill) => skill.slug !== currentSkill.slug) + .map((skill) => ({ + skill, + overlap: calculateTagOverlap(currentSkill.tags, skill.tags), + })) + .filter((item) => item.overlap > 0) // Only show skills with at least 1 shared tag + .sort((a, b) => { + // Sort by overlap (descending), then alphabetically by name + if (b.overlap !== a.overlap) { + return b.overlap - a.overlap; + } + return a.skill.name.localeCompare(b.skill.name); + }) + .slice(0, maxResults); + + // If no related skills found, don't render anything + if (relatedSkills.length === 0) { + return null; + } + + return ( +
+

+ 🔗 Works well with +

+
+ {relatedSkills.map(({ skill, overlap }) => { + // Find the shared tags + const sharedTags = skill.tags.filter(tag => + currentSkill.tags.includes(tag) + ); + + return ( + +
+

+ {skill.name} +

+ + {overlap} shared tag{overlap > 1 ? 's' : ''} + +
+

+ {skill.description} +

+
+ {sharedTags.map((tag) => ( + + {tag} + + ))} +
+ + ); + })} +
+
+ ); +}