diff --git a/.astro/settings.json b/.astro/settings.json index 0124d4092..97ac9af57 100644 --- a/.astro/settings.json +++ b/.astro/settings.json @@ -3,6 +3,6 @@ "enabled": false }, "_variables": { - "lastUpdateCheck": 1762257454897 + "lastUpdateCheck": 1763378528944 } } \ No newline at end of file diff --git a/.astro/types.d.ts b/.astro/types.d.ts index f964fe0cf..03d7cc43f 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1 +1,2 @@ /// +/// \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 6ecea3c3f..ecd628f5d 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -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'], + }, }, }); diff --git a/public/pdfs/roadmaps/api-security-best-practices.pdf b/public/pdfs/roadmaps/api-security-best-practices.pdf new file mode 100644 index 000000000..1937f2ddc Binary files /dev/null and b/public/pdfs/roadmaps/api-security-best-practices.pdf differ diff --git a/public/pdfs/roadmaps/aws-best-practices.pdf b/public/pdfs/roadmaps/aws-best-practices.pdf new file mode 100644 index 000000000..4527f12eb Binary files /dev/null and b/public/pdfs/roadmaps/aws-best-practices.pdf differ diff --git a/public/pdfs/roadmaps/backend-performance-best-practices.pdf b/public/pdfs/roadmaps/backend-performance-best-practices.pdf new file mode 100644 index 000000000..8fd9007e8 Binary files /dev/null and b/public/pdfs/roadmaps/backend-performance-best-practices.pdf differ diff --git a/public/pdfs/roadmaps/frontend-performance-best-practices.pdf b/public/pdfs/roadmaps/frontend-performance-best-practices.pdf new file mode 100644 index 000000000..5a5bd332f Binary files /dev/null and b/public/pdfs/roadmaps/frontend-performance-best-practices.pdf differ diff --git a/public/roadmaps/api-security-best-practices.png b/public/roadmaps/api-security-best-practices.png new file mode 100644 index 000000000..cee65e724 Binary files /dev/null and b/public/roadmaps/api-security-best-practices.png differ diff --git a/public/roadmaps/aws-best-practices.png b/public/roadmaps/aws-best-practices.png new file mode 100644 index 000000000..8baff4bfe Binary files /dev/null and b/public/roadmaps/aws-best-practices.png differ diff --git a/public/roadmaps/backend-performance-best-practices.png b/public/roadmaps/backend-performance-best-practices.png new file mode 100644 index 000000000..0efdbcf23 Binary files /dev/null and b/public/roadmaps/backend-performance-best-practices.png differ diff --git a/public/roadmaps/frontend-performance-best-practices.png b/public/roadmaps/frontend-performance-best-practices.png new file mode 100644 index 000000000..03af889a4 Binary files /dev/null and b/public/roadmaps/frontend-performance-best-practices.png differ diff --git a/src/components/CommandMenu/CommandMenu.tsx b/src/components/CommandMenu/CommandMenu.tsx index 7ddc3ea1f..4e848d2ef 100644 --- a/src/components/CommandMenu/CommandMenu.tsx +++ b/src/components/CommandMenu/CommandMenu.tsx @@ -78,13 +78,6 @@ const defaultPages: PageType[] = [ icon: , isProtected: true, }, - { - id: 'best-practices', - url: '/best-practices', - title: 'Best Practices', - group: 'Pages', - icon: , - }, { id: 'questions', url: '/questions', diff --git a/src/components/Dashboard/PersonalDashboard.tsx b/src/components/Dashboard/PersonalDashboard.tsx index 2a19b9704..974468d61 100644 --- a/src/components/Dashboard/PersonalDashboard.tsx +++ b/src/components/Dashboard/PersonalDashboard.tsx @@ -453,12 +453,12 @@ export function PersonalDashboard(props: PersonalDashboardProps) { ); })} diff --git a/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx b/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx index 24a73e5e8..4213d89fd 100644 --- a/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx +++ b/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx @@ -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; diff --git a/src/components/FrameRenderer/RoadmapFloatingChat.tsx b/src/components/FrameRenderer/RoadmapFloatingChat.tsx index f4f7d5f21..06dcd69d9 100644 --- a/src/components/FrameRenderer/RoadmapFloatingChat.tsx +++ b/src/components/FrameRenderer/RoadmapFloatingChat.tsx @@ -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', diff --git a/src/components/Navigation/AccountDropdownList.tsx b/src/components/Navigation/AccountDropdownList.tsx index f597c3a15..d29c184d7 100644 --- a/src/components/Navigation/AccountDropdownList.tsx +++ b/src/components/Navigation/AccountDropdownList.tsx @@ -81,9 +81,6 @@ export function AccountDropdownList(props: AccountDropdownListProps) { My Profile - - New -
  • diff --git a/src/components/Roadmaps/RoadmapsPage.tsx b/src/components/Roadmaps/RoadmapsPage.tsx index 2afd0dfaf..425079dd6 100644 --- a/src/components/Roadmaps/RoadmapsPage.tsx +++ b/src/components/Roadmaps/RoadmapsPage.tsx @@ -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() { diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 88af1687a..e1d642a22 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -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) => { 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(); ({ - 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} + />