diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc43f..f964fe0cf 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1 @@ /// -/// \ No newline at end of file diff --git a/src/components/CustomRoadmap/CustomRoadmap.tsx b/src/components/CustomRoadmap/CustomRoadmap.tsx index b8bba0b13..a4c94da60 100644 --- a/src/components/CustomRoadmap/CustomRoadmap.tsx +++ b/src/components/CustomRoadmap/CustomRoadmap.tsx @@ -118,6 +118,7 @@ export function CustomRoadmap(props: CustomRoadmapProps) { resourceId={roadmap!._id} resourceTitle={roadmap!.title} resourceType="roadmap" + renderer='editor' isEmbed={isEmbed} canSubmitContribution={false} /> diff --git a/src/components/GenerateCourse/AICourseLessonChat.tsx b/src/components/GenerateCourse/AICourseLessonChat.tsx index 66e1c674f..ae4c3f317 100644 --- a/src/components/GenerateCourse/AICourseLessonChat.tsx +++ b/src/components/GenerateCourse/AICourseLessonChat.tsx @@ -399,7 +399,7 @@ type AIChatCardProps = { html?: string; }; -function AIChatCard(props: AIChatCardProps) { +export function AIChatCard(props: AIChatCardProps) { const { role, content, html: defaultHtml } = props; const html = useMemo(() => { diff --git a/src/components/GenerateCourse/GenerateAICourse.tsx b/src/components/GenerateCourse/GenerateAICourse.tsx index 092cc0964..f7e57f8b2 100644 --- a/src/components/GenerateCourse/GenerateAICourse.tsx +++ b/src/components/GenerateCourse/GenerateAICourse.tsx @@ -54,6 +54,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) { const params = getUrlParams(); const paramsTerm = params?.term; const paramsDifficulty = params?.difficulty; + const paramsSrc = params?.src || 'search'; if (!paramsTerm || !paramsDifficulty) { return; } @@ -87,6 +88,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) { instructions: paramsCustomInstructions, goal: paramsGoal, about: paramsAbout, + src: paramsSrc, }); }, [term, difficulty]); @@ -98,9 +100,18 @@ export function GenerateAICourse(props: GenerateAICourseProps) { about?: string; isForce?: boolean; prompt?: string; + src?: string; }) => { - const { term, difficulty, isForce, prompt, instructions, goal, about } = - options; + const { + term, + difficulty, + isForce, + prompt, + instructions, + goal, + about, + src, + } = options; if (!isLoggedIn()) { window.location.href = '/ai'; @@ -121,6 +132,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) { about, isForce, prompt, + src, }); }; diff --git a/src/components/TopicDetail/PredefinedActionGroup.tsx b/src/components/TopicDetail/PredefinedActionGroup.tsx new file mode 100644 index 000000000..adc718e55 --- /dev/null +++ b/src/components/TopicDetail/PredefinedActionGroup.tsx @@ -0,0 +1,54 @@ +import type { LucideIcon } from 'lucide-react'; +import { useState, useRef } from 'react'; +import { useOutsideClick } from '../../hooks/use-outside-click'; +import { + type PredefinedActionType, + PredefinedActionButton, +} from './PredefinedActions'; + +type PredefinedActionGroupProps = { + label: string; + icon: LucideIcon; + actions: PredefinedActionType[]; + onSelect: (action: PredefinedActionType) => void; +}; + +export function PredefinedActionGroup(props: PredefinedActionGroupProps) { + const { label, icon: Icon, actions, onSelect } = props; + + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + useOutsideClick(containerRef, () => { + setIsOpen(false); + }); + + return ( +
+ setIsOpen(!isOpen)} + isGroup={true} + /> + + {isOpen && ( +
+ {actions.map((action) => { + return ( + { + onSelect(action); + setIsOpen(false); + }} + /> + ); + })} +
+ )} +
+ ); +} diff --git a/src/components/TopicDetail/PredefinedActions.tsx b/src/components/TopicDetail/PredefinedActions.tsx new file mode 100644 index 000000000..b1f90b930 --- /dev/null +++ b/src/components/TopicDetail/PredefinedActions.tsx @@ -0,0 +1,144 @@ +import { + BabyIcon, + BookOpenTextIcon, + BrainIcon, + ChevronDownIcon, + ListIcon, + NotebookPenIcon, + PencilLine, + Star, + type LucideIcon +} from 'lucide-react'; +import { cn } from '../../lib/classname'; +import { PredefinedActionGroup } from './PredefinedActionGroup'; + +export type PredefinedActionType = { + icon: LucideIcon; + label: string; + prompt?: string; + children?: PredefinedActionType[]; +}; + +export const actions: PredefinedActionType[] = [ + { + icon: BookOpenTextIcon, + label: 'Explain', + children: [ + { + icon: NotebookPenIcon, + label: 'Explain the topic', + prompt: 'Explain this topic in detail and include examples', + }, + { + icon: ListIcon, + label: 'List the key points', + prompt: 'List the key points to remember from this topic', + }, + { + icon: PencilLine, + label: 'Summarize the topic', + prompt: + 'Briefly explain the topic in a few sentences. Treat it as a brief answer to an interview question. Your response should just be the answer to the question, nothing else.', + }, + { + icon: BabyIcon, + label: 'Explain like I am five', + prompt: 'Explain this topic like I am a 5 years old', + }, + { + icon: Star, + label: 'Why is it important?', + prompt: + 'Why is this topic important? What are the real world applications (only add if appropriate)?', + }, + ], + }, + { + icon: BrainIcon, + label: 'Test my Knowledge', + prompt: + "Act as an interviewer and test my understanding of this topic. Ask me a single question at a time and evaluate my answer. Question number should be bold. After evaluating my answer, immediately proceed to the next question without asking if I'm ready or want another question. Continue asking questions until I explicitly tell you to stop.", + }, +]; + +export const promptLabelMapping = actions.reduce( + (acc, action) => { + if (action.prompt) { + acc[action.prompt] = action.label; + } + + if (action.children) { + action.children.forEach((child) => { + if (child.prompt) { + acc[child.prompt] = child.label; + } + }); + } + + return acc; + }, + {} as Record, +); + +type PredefinedActionsProps = { + onSelect: (action: PredefinedActionType) => void; +}; + +export function PredefinedActions(props: PredefinedActionsProps) { + const { onSelect } = props; + + return ( +
+ {actions.map((action) => { + if (!action.children) { + return ( + { + onSelect(action); + }} + /> + ); + } + + return ( + + ); + })} +
+ ); +} + +type PredefinedActionButtonProps = { + label: string; + icon?: LucideIcon; + onClick: () => void; + isGroup?: boolean; + className?: string; +}; + +export function PredefinedActionButton(props: PredefinedActionButtonProps) { + const { label, icon: Icon, onClick, isGroup = false, className } = props; + + return ( + + ); +} diff --git a/src/components/TopicDetail/ResourceListSeparator.tsx b/src/components/TopicDetail/ResourceListSeparator.tsx index 1b8d8d60f..ce07255e4 100644 --- a/src/components/TopicDetail/ResourceListSeparator.tsx +++ b/src/components/TopicDetail/ResourceListSeparator.tsx @@ -27,7 +27,7 @@ export function ResourceListSeparator(props: ResourceSeparatorProps) { {Icon && } {text} -
+

); } diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index 3db935010..a58fcab87 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -14,7 +14,6 @@ import { updateResourceProgress as updateResourceProgressApi, } from '../../lib/resource-progress'; import { pageProgressMessage } from '../../stores/page'; -import { TopicProgressButton } from './TopicProgressButton'; import { showLoginPopup } from '../../lib/popup'; import { useToast } from '../../hooks/use-toast'; import type { @@ -22,20 +21,33 @@ import type { RoadmapContentDocument, } from '../CustomRoadmap/CustomRoadmap'; import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown'; -import { Ban, Coins, FileText, HeartHandshake, Star, X } from 'lucide-react'; +import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react'; import { getUrlParams, parseUrl } from '../../lib/browser'; import { Spinner } from '../ReactIcons/Spinner'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; -import { resourceTitleFromId } from '../../lib/roadmap.ts'; +import { + resourceTitleFromId, + type AllowedRoadmapRenderer, +} from '../../lib/roadmap.ts'; import { lockBodyScroll } from '../../lib/dom.ts'; import { TopicDetailLink } from './TopicDetailLink.tsx'; import { ResourceListSeparator } from './ResourceListSeparator.tsx'; import { PaidResourceDisclaimer } from './PaidResourceDisclaimer.tsx'; +import { + TopicDetailsTabs, + type AllowedTopicDetailsTabs, +} from './TopicDetailsTabs.tsx'; +import { TopicDetailAI } from './TopicDetailAI.tsx'; +import { cn } from '../../lib/classname.ts'; +import type { AIChatHistoryType } from '../GenerateCourse/AICourseLessonChat.tsx'; +import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx'; +import { TopicProgressButton } from './TopicProgressButton.tsx'; type TopicDetailProps = { resourceId?: string; resourceTitle?: string; resourceType?: ResourceType; + renderer?: AllowedRoadmapRenderer; isEmbed?: boolean; canSubmitContribution: boolean; @@ -51,6 +63,14 @@ type PaidResourceType = { const paidResourcesCache: Record = {}; +export const defaultChatHistory: AIChatHistoryType[] = [ + { + role: 'assistant', + content: 'Hey, I am your AI instructor. How can I help you today? 🤖', + isDefault: true, + }, +]; + async function fetchRoadmapPaidResources(roadmapId: string) { if (paidResourcesCache[roadmapId]) { return paidResourcesCache[roadmapId]; @@ -77,6 +97,7 @@ export function TopicDetail(props: TopicDetailProps) { canSubmitContribution, resourceId: defaultResourceId, isEmbed = false, + renderer = 'balsamiq', resourceTitle, } = props; @@ -91,6 +112,13 @@ export function TopicDetail(props: TopicDetailProps) { const [topicTitle, setTopicTitle] = useState(''); const [topicHtmlTitle, setTopicHtmlTitle] = useState(''); const [links, setLinks] = useState([]); + const [activeTab, setActiveTab] = + useState('content'); + const [aiChatHistory, setAiChatHistory] = + useState(defaultChatHistory); + const [showUpgradeModal, setShowUpgradeModal] = useState(false); + const [isCustomResource, setIsCustomResource] = useState(false); + const toast = useToast(); const [showPaidResourceDisclaimer, setShowPaidResourceDisclaimer] = @@ -106,14 +134,16 @@ export function TopicDetail(props: TopicDetailProps) { const [resourceType, setResourceType] = useState('roadmap'); const [paidResources, setPaidResources] = useState([]); - // Close the topic detail when user clicks outside the topic detail - useOutsideClick(topicRef, () => { + const handleClose = () => { setIsActive(false); - }); + setShowUpgradeModal(false); + setAiChatHistory(defaultChatHistory); + setActiveTab('content'); + }; - useKeydown('Escape', () => { - setIsActive(false); - }); + // Close the topic detail when user clicks outside the topic detail + useOutsideClick(topicRef, handleClose); + useKeydown('Escape', handleClose); useEffect(() => { if (resourceType !== 'roadmap' || !defaultResourceId) { @@ -177,6 +207,7 @@ export function TopicDetail(props: TopicDetailProps) { setTopicId(topicId); setResourceType(resourceType); setResourceId(resourceId); + setIsCustomResource(isCustomResource); const topicPartial = topicId.replaceAll(':', '/'); let topicUrl = @@ -335,15 +366,21 @@ export function TopicDetail(props: TopicDetailProps) { (resource) => resource?.url?.toLowerCase().indexOf('scrimba') !== -1, ); + const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap'; + return (
+ {showUpgradeModal && ( + setShowUpgradeModal(false)} /> + )} + {isLoading && ( -
+
-
- {/* Actions for the topic */} -
- {!isEmbed && ( - { - setIsActive(false); - }} +
+
+ {shouldShowAiTab && ( + )} - - -
- - {/* Topic Content */} - {hasContent ? ( - <> -
- {topicTitle &&

{topicTitle}

} -
-
- - ) : ( - <> - {!canSubmitContribution && ( -
- -

- Empty Content -

-
)} - {canSubmitContribution && ( -
- -

- Help us write this content -

-

- Write a brief introduction to this topic and submit a - link to a good article, podcast, video, or any other - self-vetted resource that helped you understand this - topic better. -

- - - Help us Write this Content - -
- )} - - )} + +
+
- {links.length > 0 && ( - <> - -
    - {links.map((link) => { - return ( -
  • - { - // if it is one of our roadmaps, we want to track the click - if (canSubmitContribution) { - const parsedUrl = parseUrl(link.url); - - window.fireEvent({ - category: 'TopicResourceClick', - action: `Click: ${parsedUrl.hostname}`, - label: `${resourceType} / ${resourceId} / ${topicId} / ${link.url}`, - }); - } - }} - /> -
  • - ); - })} -
- + {activeTab === 'ai' && shouldShowAiTab && ( + setShowUpgradeModal(true)} + onLogin={() => { + handleClose(); + showLoginPopup(); + }} + /> )} - {paidResourcesForTopic.length > 0 && ( + {activeTab === 'content' && ( <> - - -
    - {paidResourcesForTopic.map((resource) => { - return ( -
  • - -
  • - ); - })} -
- - {hasPaidScrimbaLinks && ( -
-
- - - Scrimba is offering{' '} - 20% off on - all courses for roadmap.sh users. - + {hasContent ? ( + <> +
+ {topicTitle &&

{topicTitle}

} +
-
+ + ) : ( + <> + {!canSubmitContribution && ( +
+ +

+ Empty Content +

+
+ )} + {canSubmitContribution && ( +
+ +

+ Help us write this content +

+

+ Write a brief introduction to this topic and submit + a link to a good article, podcast, video, or any + other self-vetted resource that helped you + understand this topic better. +

+ + + Help us Write this Content + +
+ )} + )} - {showPaidResourceDisclaimer && ( - { - localStorage.setItem( - PAID_RESOURCE_DISCLAIMER_HIDDEN, - 'true', - ); - setShowPaidResourceDisclaimer(false); - }} - /> + {links.length > 0 && ( + <> + +
    + {links.map((link) => { + return ( +
  • + { + // if it is one of our roadmaps, we want to track the click + if (canSubmitContribution) { + const parsedUrl = parseUrl(link.url); + + window.fireEvent({ + category: 'TopicResourceClick', + action: `Click: ${parsedUrl.hostname}`, + label: `${resourceType} / ${resourceId} / ${topicId} / ${link.url}`, + }); + } + }} + /> +
  • + ); + })} +
+ + )} + + {paidResourcesForTopic.length > 0 && ( + <> + + +
    + {paidResourcesForTopic.map((resource) => { + return ( +
  • + +
  • + ); + })} +
+ + {showPaidResourceDisclaimer && ( + { + localStorage.setItem( + PAID_RESOURCE_DISCLAIMER_HIDDEN, + 'true', + ); + setShowPaidResourceDisclaimer(false); + }} + /> + )} + )} )} - - {/* Contribution */} - {canSubmitContribution && - !hasEnoughLinks && - contributionUrl && - hasContent && ( -
-

- Help us add learning resources -

-

- This popup should be a brief introductory paragraph for - the topic and a few links to good articles, videos, or any - other self-vetted learning resources. Please consider submitting a - PR to improve this content. -

- - - Help us Improve this Content - -
- )}
- {resourceId === 'devops' && ( - - )} + + {canSubmitContribution && + contributionUrl && + activeTab === 'content' && + hasContent && ( + + )} )} - {/* Error */} {!isContributing && !isLoading && error && ( <> + )} + + {!isPaidUser && ( + <> + + + + )} +
+ )} +
+ + { + if (!isLoggedIn()) { + onLogin(); + return; + } + + if (isLimitExceeded) { + onUpgrade(); + return; + } + + if (!action?.prompt) { + toast.error('Something went wrong'); + return; + } + + setMessage(action.prompt); + handleChatSubmit(action.prompt); + }} + /> + +
+
+
+
+ {aiChatHistory.map((chat, index) => { + let content = chat.content; + + if (chat.role === 'user' && promptLabelMapping[chat.content]) { + content = promptLabelMapping[chat.content]; + } + + return ( + + + + ); + })} + + {isStreamingMessage && !streamedMessage && ( + + )} + + {streamedMessage && ( + + )} +
+
+
+
+ +
{ + e.preventDefault(); + handleChatSubmit(); + }} + > + {isLimitExceeded && isLoggedIn() && ( +
+ +

+ Limit reached for today + {isPaidUser ? '. Please wait until tomorrow.' : ''} +

+ {!isPaidUser && ( + + )} +
+ )} + {!isLoggedIn() && ( +
+ +

Please login to continue

+ +
+ )} + + {isDataLoading && ( +
+ +

Loading...

+
+ )} + + setMessage(e.target.value)} + autoFocus={true} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleChatSubmit(); + } + }} + ref={textareaRef} + /> + + +
+ ); +} diff --git a/src/components/TopicDetail/TopicDetailLink.tsx b/src/components/TopicDetail/TopicDetailLink.tsx index d5b05c95e..60f3ea5e7 100644 --- a/src/components/TopicDetail/TopicDetailLink.tsx +++ b/src/components/TopicDetail/TopicDetailLink.tsx @@ -1,7 +1,7 @@ import { cn } from '../../lib/classname.ts'; import type { AllowedLinkTypes } from '../CustomRoadmap/CustomRoadmap.tsx'; -const linkTypes: Record = { +const linkTypes: Record = { article: 'bg-yellow-300', course: 'bg-green-400', opensource: 'bg-black text-white', @@ -18,6 +18,34 @@ const paidLinkTypes: Record = { course: 'bg-yellow-300', }; +type TopicLinkBadgeProps = { + isPaid: boolean; + discountText?: string; + type: AllowedLinkTypes | string; + className?: string; +}; + +function TopicLinkBadge(props: TopicLinkBadgeProps) { + const { isPaid, type, className } = props; + + const linkType = type === 'opensource' ? 'OpenSource' : type; + const isDiscount = type.includes('% off'); + + return ( + + + {linkType} + + + ); +} + type TopicDetailLinkProps = { url: string; onClick?: () => void; @@ -29,7 +57,7 @@ type TopicDetailLinkProps = { export function TopicDetailLink(props: TopicDetailLinkProps) { const { url, onClick, type, title, isPaid = false } = props; - const linkType = type === 'opensource' ? 'OpenSource' : type; + const isScrimbaLink = url.toLowerCase().includes('scrimba.com'); return ( - - {linkType} - + + {isScrimbaLink && } + {title} ); diff --git a/src/components/TopicDetail/TopicDetailsTabs.tsx b/src/components/TopicDetail/TopicDetailsTabs.tsx new file mode 100644 index 000000000..91618122b --- /dev/null +++ b/src/components/TopicDetail/TopicDetailsTabs.tsx @@ -0,0 +1,68 @@ +import { Earth, WandSparkles, type LucideIcon } from 'lucide-react'; + +export type AllowedTopicDetailsTabs = 'content' | 'ai'; + +type TopicDetailsTabsProps = { + activeTab: AllowedTopicDetailsTabs; + setActiveTab: (tab: AllowedTopicDetailsTabs) => void; + hasAITutor?: boolean; +}; + +export function TopicDetailsTabs(props: TopicDetailsTabsProps) { + const { activeTab, setActiveTab, hasAITutor = true } = props; + + return ( +
+ setActiveTab('content')} + /> + setActiveTab('ai')} + /> +
+ ); +} + +type TopicDetailsTabProps = { + isActive: boolean; + icon: LucideIcon; + label: string; + isNew?: boolean; + isDisabled?: boolean; + onClick: () => void; +}; + +function TopicDetailsTab(props: TopicDetailsTabProps) { + const { isActive, icon: Icon, label, isNew, isDisabled, onClick } = props; + + return ( + + ); +} diff --git a/src/components/TopicDetail/TopicProgressButton.tsx b/src/components/TopicDetail/TopicProgressButton.tsx index 8cd1866a6..2b0d39816 100644 --- a/src/components/TopicDetail/TopicProgressButton.tsx +++ b/src/components/TopicDetail/TopicProgressButton.tsx @@ -16,25 +16,54 @@ import { showLoginPopup } from '../../lib/popup'; import { useToast } from '../../hooks/use-toast'; import { Spinner } from '../ReactIcons/Spinner'; import { ChevronDown } from 'lucide-react'; +import { cn } from '../../lib/classname'; + +const statusColors: Record = { + done: 'bg-green-500', + learning: 'bg-yellow-500', + pending: 'bg-gray-300', + skipped: 'bg-black', + removed: '', +}; type TopicProgressButtonProps = { topicId: string; resourceId: string; resourceType: ResourceType; + dropdownClassName?: string; onClose: () => void; }; -const statusColors: Record = { - done: 'bg-green-500', - learning: 'bg-yellow-500', - pending: 'bg-gray-300', - skipped: 'bg-black', - removed: '', +type ProgressDropdownItemProps = { + status: ResourceProgressType; + shortcutKey: string; + label: string; + onClick: () => void; }; +function ProgressDropdownItem(props: ProgressDropdownItemProps) { + const { status, shortcutKey, label, onClick } = props; + + return ( + + ); +} + export function TopicProgressButton(props: TopicProgressButtonProps) { - const { topicId, resourceId, resourceType, onClose } = props; + const { topicId, resourceId, resourceType, onClose, dropdownClassName } = + props; const toast = useToast(); const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); @@ -66,7 +95,15 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { // Mark as done useKeydown( 'd', - () => { + (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + if (progress === 'done') { onClose(); return; @@ -80,7 +117,15 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { // Mark as learning useKeydown( 'l', - () => { + (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + if (progress === 'learning') { return; } @@ -93,7 +138,15 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { // Mark as learning useKeydown( 's', - () => { + (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + if (progress === 'skipped') { onClose(); return; @@ -107,9 +160,16 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { // Mark as pending useKeydown( 'r', - () => { + (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + if (progress === 'pending') { - onClose(); return; } @@ -147,6 +207,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { console.error(err); }) .finally(() => { + setShowChangeStatus(false); setIsUpdatingProgress(false); }); }; @@ -167,15 +228,20 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { if (isUpdatingProgress) { return ( ); } return ( -
- +
+ {showChangeStatus && (
button:first-child]:rounded-t-md [&>button:last-child]:rounded-b-md', + dropdownClassName, + )} ref={changeStatusRef!} > {allowMarkingDone && ( - + /> )} {allowMarkingLearning && ( - + /> )} {allowMarkingPending && ( - + /> )} {allowMarkingSkipped && ( - + /> )}
)} diff --git a/src/data/roadmaps/frontend/content/how-does-the-internet-work@yCnn-NfSxIybUQ2iTuUGq.md b/src/data/roadmaps/frontend/content/how-does-the-internet-work@yCnn-NfSxIybUQ2iTuUGq.md index 774d46faa..a9b391000 100644 --- a/src/data/roadmaps/frontend/content/how-does-the-internet-work@yCnn-NfSxIybUQ2iTuUGq.md +++ b/src/data/roadmaps/frontend/content/how-does-the-internet-work@yCnn-NfSxIybUQ2iTuUGq.md @@ -1,10 +1,10 @@ # How Does The Internet Work -The Internet works through a global network of interconnected computers and servers, communicating via standardized protocols. Data is broken into packets and routed through various network nodes using the Internet Protocol (IP). These packets travel across different physical infrastructures, including fiber optic cables, satellites, and wireless networks. The Transmission Control Protocol (TCP) ensures reliable delivery and reassembly of packets at their destination. Domain Name System (DNS) servers translate human-readable website names into IP addresses. When you access a website, your device sends a request to the appropriate server, which responds with the requested data. This process, facilitated by routers, switches, and other networking equipment, enables the seamless exchange of information across vast distances, forming the backbone of our digital communications. +The internet is a global network that connects computers and devices so they can share information with each other. It’s how you browse websites, send emails, watch videos, and use apps. Think of it like a giant web that links everything together. Visit the following resources to learn more: -- [@roadmap@Introduction to Internet](https://roadmap.sh/guides/what-is-internet) +- [@article@Introduction to Internet](https://roadmap.sh/guides/what-is-internet) - [@article@How does the Internet Work?](https://cs.fyi/guide/how-does-internet-work) - [@article@How Does the Internet Work? MDN Docs](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/How_does_the_Internet_work) - [@video@How the Internet Works in 5 Minutes](https://www.youtube.com/watch?v=7_LPdttKXPc) diff --git a/src/data/roadmaps/frontend/content/internet@VlNNwIEDWqQXtqkHWJYzC.md b/src/data/roadmaps/frontend/content/internet@VlNNwIEDWqQXtqkHWJYzC.md index de067dbaa..f34a4636f 100644 --- a/src/data/roadmaps/frontend/content/internet@VlNNwIEDWqQXtqkHWJYzC.md +++ b/src/data/roadmaps/frontend/content/internet@VlNNwIEDWqQXtqkHWJYzC.md @@ -4,5 +4,5 @@ The Internet is a global network of interconnected computer networks that use th Visit the following resources to learn more: -- [@roadmap@Introduction to Internet](https://roadmap.sh/guides/what-is-internet) +- [@article@Introduction to Internet](https://roadmap.sh/guides/what-is-internet) - [@article@The Internet](https://en.wikipedia.org/wiki/Internet) diff --git a/src/helper/generate-ai-course.ts b/src/helper/generate-ai-course.ts index 713e77e9a..fcd19150b 100644 --- a/src/helper/generate-ai-course.ts +++ b/src/helper/generate-ai-course.ts @@ -20,6 +20,7 @@ type GenerateCourseOptions = { onCourseChange?: (course: AiCourse, rawData: string) => void; onLoadingChange?: (isLoading: boolean) => void; onError?: (error: string) => void; + src?: string; }; export async function generateCourse(options: GenerateCourseOptions) { @@ -37,6 +38,7 @@ export async function generateCourse(options: GenerateCourseOptions) { instructions, goal, about, + src = 'search', } = options; onLoadingChange?.(true); @@ -85,6 +87,7 @@ export async function generateCourse(options: GenerateCourseOptions) { instructions, goal, about, + src, }), credentials: 'include', }, diff --git a/src/hooks/use-keydown.ts b/src/hooks/use-keydown.ts index c4f18b00c..1b20668e1 100644 --- a/src/hooks/use-keydown.ts +++ b/src/hooks/use-keydown.ts @@ -7,14 +7,14 @@ export function useKeydown(keyName: string, callback: any, deps: any[] = []) { !keyName.startsWith('mod_') && event.key.toLowerCase() === keyName.toLowerCase() ) { - callback(); + callback(event); } else if ( keyName.startsWith('mod_') && event.metaKey && event.key.toLowerCase() === keyName.replace('mod_', '').toLowerCase() ) { event.preventDefault(); - callback(); + callback(event); } }; diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index c57c1fe03..a46a14ef0 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -18,6 +18,7 @@ import RoadmapNote from '../../components/RoadmapNote.astro'; import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion'; import ResourceProgressStats from '../../components/ResourceProgressStats.astro'; import { getProjectsByRoadmapId } from '../../lib/project'; +import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; export const prerender = true; @@ -101,6 +102,7 @@ const courses = roadmapData.courses || []; resourceTitle={roadmapData.title} resourceId={roadmapId} resourceType='roadmap' + renderer={roadmapData.renderer} client:idle canSubmitContribution={true} /> @@ -183,5 +185,6 @@ const courses = roadmapData.courses || [];
+
diff --git a/src/pages/best-practices/[bestPracticeId]/index.astro b/src/pages/best-practices/[bestPracticeId]/index.astro index d577a913c..08af742a4 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.astro +++ b/src/pages/best-practices/[bestPracticeId]/index.astro @@ -14,6 +14,7 @@ import { type BestPracticeFrontmatter, getAllBestPractices, } from '../../../lib/best-practice'; +import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification'; export const prerender = true; @@ -107,6 +108,7 @@ const ogImageUrl = getOpenGraphImageUrl({ resourceId={bestPracticeId} resourceTitle={bestPracticeData.title} resourceType='best-practice' + renderer={'balsamiq'} client:idle canSubmitContribution={true} /> @@ -136,6 +138,6 @@ const ogImageUrl = getOpenGraphImageUrl({ /> {bestPracticeData.isUpcoming && } - +
diff --git a/src/queries/roadmap-tree.ts b/src/queries/roadmap-tree.ts new file mode 100644 index 000000000..a94259ceb --- /dev/null +++ b/src/queries/roadmap-tree.ts @@ -0,0 +1,26 @@ +import { queryOptions } from '@tanstack/react-query'; +import { httpGet } from '../lib/query-http'; + +export interface RoadmapTreeDocument { + _id?: string; + roadmapId: string; + mapping: { + _id?: string; + nodeId: string; + text: string; + subjects: string[]; + }[]; + createdAt: Date; + updatedAt: Date; +} + +export function roadmapTreeMappingOptions(roadmapId: string) { + return queryOptions({ + queryKey: ['roadmap-tree-mapping', { roadmapId }], + queryFn: () => { + return httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-roadmap-tree-mapping/${roadmapId}`, + ); + }, + }); +}