Migrate best practices to our editor (#9362)

* Add best practices

* Migrate best practices to editor
This commit is contained in:
Kamran Ahmed
2025-11-17 13:21:16 +00:00
committed by GitHub
parent 89895fce27
commit b99cdda32c
20 changed files with 198 additions and 40 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1762257454897
"lastUpdateCheck": 1763378528944
}
}

1
.astro/types.d.ts vendored
View File

@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -20,10 +20,29 @@ export default defineConfig({
status: 301,
destination: '/ai',
},
},
vite: {
server: {
allowedHosts: ['roadmap.sh', 'port3k.kamranahmed.info'],
'/best-practices': {
status: 301,
destination: '/roadmaps',
},
'/best-practices/aws': {
status: 301,
destination: '/aws-best-practices',
},
'/best-practices/backend-performance': {
status: 301,
destination: '/backend-performance-best-practices',
},
'/best-practices/frontend-performance': {
status: 301,
destination: '/frontend-performance-best-practices',
},
'/best-practices/api-security': {
status: 301,
destination: '/api-security-best-practices',
},
'/best-practices/code-review': {
status: 301,
destination: '/code-review-best-practices',
},
},
markdown: {
@@ -72,5 +91,8 @@ export default defineConfig({
ssr: {
noExternal: [/^@roadmapsh\/editor.*$/],
},
server: {
allowedHosts: ['roadmap.sh', 'port3k.kamranahmed.info'],
},
},
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View File

@@ -78,13 +78,6 @@ const defaultPages: PageType[] = [
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
id: 'best-practices',
url: '/best-practices',
title: 'Best Practices',
group: 'Pages',
icon: <BestPracticesIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'questions',
url: '/questions',

View File

@@ -453,12 +453,12 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="best-practice"
resourceType="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/best-practices/${roadmap.id}`}
url={`/${roadmap.id}`}
/>
);
})}

View File

@@ -176,6 +176,51 @@ export function EditorRoadmapRenderer(props: RoadmapRendererProps) {
return;
}
if (nodeType === 'checklist-item' && (target.tagName === 'text' || target.tagName === 'tspan')) {
e.preventDefault();
const textElement = target.tagName === 'tspan' ? (target.closest('text') as SVGTextElement) : target;
const clickedText = textElement?.textContent?.trim();
if (!clickedText) {
return;
}
const parentChecklistId = targetGroup?.dataset?.parentId;
if (!parentChecklistId) {
return;
}
const parentChecklistGroup = roadmapRef.current?.querySelector(
`g[data-node-id="${parentChecklistId}"][data-type="checklist"]`
);
if (!parentChecklistGroup) {
return;
}
const labelGroup = parentChecklistGroup.querySelector(
'g[data-type="checklist-label"]'
);
if (!labelGroup) {
return;
}
const labelText = labelGroup.querySelector('text')?.textContent?.trim();
if (!labelText) {
return;
}
window.dispatchEvent(
new CustomEvent('roadmap.checklist.click', {
detail: {
roadmapId: resourceId,
labelText,
clickedText,
},
}),
);
return;
}
// we don't have the topic popup for checklist-item
if (nodeType === 'checklist-item') {
return;

View File

@@ -306,6 +306,53 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
};
}, [isOpen, isPersonalizeOpen]);
useEffect(() => {
const handleChecklistClick = (e: CustomEvent) => {
const { roadmapId: eventRoadmapId, labelText, clickedText } = e.detail;
if (eventRoadmapId !== roadmapId) {
return;
}
if (!isLoggedIn()) {
setIsOpen(false);
showLoginPopup();
return;
}
const roadmapTitle = roadmapDetail?.json?.title?.page || roadmapDetail?.json?.title?.card || 'this roadmap';
const message = `Explain the '${roadmapTitle}' best practice '${labelText} > ${clickedText}'`;
flushSync(() => {
setIsOpen(true);
});
setTimeout(() => {
sendMessage(
{ text: message, metadata: { json: textToJSON(message) } },
{
body: {
roadmapId,
...(activeChatHistoryId
? { chatHistoryId: activeChatHistoryId }
: {}),
},
},
);
setTimeout(() => {
scrollToBottom('smooth');
inputRef.current?.focus();
}, 0);
}, 100);
};
window.addEventListener('roadmap.checklist.click', handleChecklistClick as EventListener);
return () => {
window.removeEventListener('roadmap.checklist.click', handleChecklistClick as EventListener);
};
}, [roadmapId, roadmapDetail, sendMessage, activeChatHistoryId, scrollToBottom]);
function textToJSON(text: string): JSONContent {
return {
type: 'doc',

View File

@@ -81,9 +81,6 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<SquareUserRound className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
My Profile
</span>
<span className="rounded-xs bg-yellow-300 px-1 text-xs tracking-wide text-black uppercase">
New
</span>
</a>
</li>
<li className="px-1">

View File

@@ -55,7 +55,7 @@ export type GroupType = {
roadmaps: {
title: string;
link: string;
type: 'role' | 'skill';
type: 'role' | 'skill' | 'best-practice';
otherGroups?: AllowGroupNames[];
}[];
};
@@ -196,7 +196,12 @@ const groups: GroupType[] = [
title: 'JavaScript',
link: '/javascript',
type: 'skill',
otherGroups: ['Web Development', 'DevOps', 'Mobile Development', 'Absolute Beginners'],
otherGroups: [
'Web Development',
'DevOps',
'Mobile Development',
'Absolute Beginners',
],
},
{
title: 'Kotlin',
@@ -538,6 +543,41 @@ const groups: GroupType[] = [
},
],
},
{
group: 'Best Practices',
roadmaps: [
{
title: 'Backend Performance',
link: '/backend-performance-best-practices',
type: 'best-practice',
otherGroups: ['Web Development'],
},
{
title: 'Frontend Performance',
link: '/frontend-performance-best-practices',
type: 'best-practice',
otherGroups: ['Web Development'],
},
{
title: 'Code Review',
link: '/code-review-best-practices',
type: 'best-practice',
otherGroups: ['Web Development'],
},
{
title: 'AWS',
link: '/aws-review-best-practices',
type: 'best-practice',
otherGroups: ['Web Development', 'DevOps'],
},
{
title: 'API Security',
link: '/api-security-best-practices',
type: 'best-practice',
otherGroups: ['Web Development'],
},
],
},
];
const roleRoadmaps = groups.flatMap((group) =>
@@ -546,6 +586,9 @@ const roleRoadmaps = groups.flatMap((group) =>
const skillRoadmaps = groups.flatMap((group) =>
group.roadmaps.filter((roadmap) => roadmap.type === 'skill'),
);
const bestPracticeRoadmaps = groups.flatMap((group) =>
group.roadmaps.filter((roadmap) => roadmap.type === 'best-practice'),
);
const allGroups = [
{
@@ -556,6 +599,10 @@ const allGroups = [
group: 'Skill Based Roadmaps',
roadmaps: skillRoadmaps,
},
{
group: 'Best Practices',
roadmaps: bestPracticeRoadmaps,
},
];
export function RoadmapsPage() {

View File

@@ -15,7 +15,7 @@ import type { BuiltInRoadmap } from '../components/Dashboard/PersonalDashboard';
const roadmaps = await listOfficialRoadmaps();
const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
const bestPractices = await getAllBestPractices();
const bestPractices = roadmaps.filter(roadmap => roadmap.type === 'best-practice');
const questionGroups = await getAllQuestionGroups();
const guides = await listOfficialGuides();
const videos = await getAllVideos();
@@ -48,15 +48,18 @@ const enrichedSkillRoadmaps: BuiltInRoadmap[] = skillRoadmaps.map((roadmap) => {
},
};
});
const enrichedBestPractices = bestPractices.map((bestPractice) => {
const { frontmatter } = bestPractice;
const enrichedBestPracticeRoadmaps: BuiltInRoadmap[] = bestPractices.map((roadmap) => {
return {
id: bestPractice.id,
url: `/best-practices/${bestPractice.id}`,
title: frontmatter.briefTitle,
description: frontmatter.briefDescription,
id: roadmap.slug,
url: `/${roadmap.slug}`,
title: roadmap.title.card,
description: roadmap.description,
relatedRoadmapIds: roadmap.relatedRoadmaps,
renderer: 'editor',
isNew: isNewRoadmap(roadmap.createdAt),
metadata: {
tags: ['best-practice-roadmap'],
},
};
});
---
@@ -65,7 +68,7 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => {
<DashboardPage
builtInRoleRoadmaps={enrichedRoleRoadmaps}
builtInSkillRoadmaps={enrichedSkillRoadmaps}
builtInBestPractices={enrichedBestPractices}
builtInBestPractices={enrichedBestPracticeRoadmaps}
questionGroups={questionGroups}
guides={guides.slice(0, 10)}
videos={videos.slice(0, 10)}

View File

@@ -6,7 +6,6 @@ import FeaturedItems from '../components/FeaturedItems/FeaturedItems.astro';
import { FeaturedVideoList } from '../components/FeaturedVideos/FeaturedVideoList';
import HeroSection from '../components/HeroSection/HeroSection.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getAllBestPractices } from '../lib/best-practice';
import { getAllVideos } from '../lib/video';
import { listOfficialGuides } from '../queries/official-guide';
import {
@@ -17,7 +16,9 @@ import {
const roadmaps = await listOfficialRoadmaps();
const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
const bestPractices = await getAllBestPractices();
const bestPractices = roadmaps.filter(
(roadmap) => roadmap.type === 'best-practice',
);
export const projectGroups = [
{
@@ -96,14 +97,17 @@ const videos = await getAllVideos();
<FeaturedItems
heading='Best Practices'
featuredItems={bestPractices.map((bestPractice) => ({
text: bestPractice.frontmatter.briefTitle,
url: `/best-practices/${bestPractice.id}`,
isNew: bestPractice.frontmatter.isNew,
isUpcoming: bestPractice.frontmatter.isUpcoming,
}))}
/>
featuredItems={bestPractices.map((bestPracticeItem) => {
const isNew = isNewRoadmap(bestPracticeItem.createdAt);
return {
text: bestPracticeItem.title.card,
url: `/${bestPracticeItem.slug}`,
isNew,
};
})}
showCreateRoadmap={false}
/>
<div class='grid grid-cols-1 gap-7 bg-gray-50 py-7 sm:gap-16 sm:py-16'>
<FeaturedGuideList
heading='Guides'

View File

@@ -1,9 +1,8 @@
import { queryOptions } from '@tanstack/react-query';
import { FetchError, httpGet } from '../lib/query-http';
import type { Node, Edge } from '@roadmapsh/editor';
import { DateTime } from 'luxon';
export const allowedOfficialRoadmapType = ['skill', 'role'] as const;
export const allowedOfficialRoadmapType = ['skill', 'role', 'best-practice'] as const;
export type AllowedOfficialRoadmapType =
(typeof allowedOfficialRoadmapType)[number];